summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/extensions/test
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/extensions/test')
-rw-r--r--comm/mail/components/extensions/test/AppUiTestDelegate.sys.mjs6
-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
-rw-r--r--comm/mail/components/extensions/test/xpcshell/.eslintrc.js13
-rw-r--r--comm/mail/components/extensions/test/xpcshell/data/utils.js124
-rw-r--r--comm/mail/components/extensions/test/xpcshell/head-imap.js12
-rw-r--r--comm/mail/components/extensions/test/xpcshell/head-nntp.js12
-rw-r--r--comm/mail/components/extensions/test/xpcshell/head.js298
-rw-r--r--comm/mail/components/extensions/test/xpcshell/images/redPixel.pngbin0 -> 119 bytes
-rw-r--r--comm/mail/components/extensions/test/xpcshell/images/whitePixel.pngbin0 -> 69 bytes
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/alternative.eml23
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/attachedMessageWithMissingHeaders.eml35
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/nestedMessages.eml127
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample01.eml11
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample02.eml121
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample03.eml43
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample04.eml10
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample05.eml10
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample06.eml8
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample07.eml24
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_accounts.js1089
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_accounts_mv3_event_pages.js220
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js2043
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_provider.js139
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_quickSearch.js238
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_readonly.js148
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_remote.js101
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_alias.js123
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_browserAction_unifiedtoolbar_restart.js350
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_experiments.js279
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_folders.js560
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_folders_mv3_event_pages.js374
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_identities_mv3_event_pages.js146
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages.js730
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_attachments.js499
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_get.js1073
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_id.js256
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_import.js121
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_move_copy_delete.js656
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_onNewMailReceived.js153
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_query.js333
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_update.js415
-rw-r--r--comm/mail/components/extensions/test/xpcshell/xpcshell-imap.ini7
-rw-r--r--comm/mail/components/extensions/test/xpcshell/xpcshell-local.ini23
-rw-r--r--comm/mail/components/extensions/test/xpcshell/xpcshell-nntp.ini7
-rw-r--r--comm/mail/components/extensions/test/xpcshell/xpcshell.ini17
147 files changed, 45832 insertions, 0 deletions
diff --git a/comm/mail/components/extensions/test/AppUiTestDelegate.sys.mjs b/comm/mail/components/extensions/test/AppUiTestDelegate.sys.mjs
new file mode 100644
index 0000000000..5320b0b6d7
--- /dev/null
+++ b/comm/mail/components/extensions/test/AppUiTestDelegate.sys.mjs
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// TODO bug 1836863: Implement AppUiTestDelegate.
+
+export var AppUiTestDelegate = {};
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();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/.eslintrc.js b/comm/mail/components/extensions/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..60d784b53c
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/.eslintrc.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ env: {
+ // The tests in this folder are testing based on WebExtensions, so lets
+ // just define the webextensions environment here.
+ webextensions: true,
+ // Many parts of WebExtensions test definitions (e.g. content scripts) also
+ // interact with the browser environment, so define that here as we don't
+ // have an easy way to handle per-function/scope usage yet.
+ browser: true,
+ },
+};
diff --git a/comm/mail/components/extensions/test/xpcshell/data/utils.js b/comm/mail/components/extensions/test/xpcshell/data/utils.js
new file mode 100644
index 0000000000..9025982e33
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/data/utils.js
@@ -0,0 +1,124 @@
+/* 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/. */
+
+// Functions for extensions to use, so that we avoid repeating ourselves.
+
+function assertDeepEqual(
+ expected,
+ actual,
+ description = "Values should be equal",
+ options = {}
+) {
+ let ok;
+ let strict = !!options?.strict;
+ try {
+ ok = assertDeepEqualNested(expected, actual, strict);
+ } catch (e) {
+ ok = false;
+ }
+ if (!ok) {
+ browser.test.fail(
+ `Deep equal test. \n Expected value: ${JSON.stringify(
+ expected
+ )} \n Actual value: ${JSON.stringify(actual)},
+ ${description}`
+ );
+ }
+}
+
+function assertDeepEqualNested(expected, actual, strict) {
+ if (expected === null) {
+ browser.test.assertTrue(actual === null);
+ return actual === null;
+ }
+
+ if (expected === undefined) {
+ browser.test.assertTrue(actual === undefined);
+ return actual === undefined;
+ }
+
+ if (["boolean", "number", "string"].includes(typeof expected)) {
+ browser.test.assertEq(typeof expected, typeof actual);
+ browser.test.assertEq(expected, actual);
+ return typeof expected == typeof actual && expected == actual;
+ }
+
+ if (Array.isArray(expected)) {
+ browser.test.assertTrue(Array.isArray(actual));
+ browser.test.assertEq(expected.length, actual.length);
+ let ok = 0;
+ let all = 0;
+ for (let i = 0; i < expected.length; i++) {
+ all++;
+ if (assertDeepEqualNested(expected[i], actual[i], strict)) {
+ ok++;
+ }
+ }
+ return (
+ Array.isArray(actual) && expected.length == actual.length && all == ok
+ );
+ }
+
+ let expectedKeys = Object.keys(expected);
+ let actualKeys = Object.keys(actual);
+ // Ignore any extra keys on the actual object in non-strict mode (default).
+ let lengthOk = strict
+ ? expectedKeys.length == actualKeys.length
+ : expectedKeys.length <= actualKeys.length;
+ browser.test.assertTrue(lengthOk);
+
+ let ok = 0;
+ let all = 0;
+ for (let key of expectedKeys) {
+ all++;
+ browser.test.assertTrue(actualKeys.includes(key), `Key ${key} exists`);
+ if (assertDeepEqualNested(expected[key], actual[key], strict)) {
+ ok++;
+ }
+ }
+ return all == ok && lengthOk;
+}
+
+function waitForMessage() {
+ return waitForEvent("test.onMessage");
+}
+
+function waitForEvent(eventName) {
+ let [namespace, name] = eventName.split(".");
+ return new Promise(resolve => {
+ browser[namespace][name].addListener(function listener(...args) {
+ browser[namespace][name].removeListener(listener);
+ resolve(args);
+ });
+ });
+}
+
+async function waitForCondition(condition, msg, interval = 100, maxTries = 50) {
+ let conditionPassed = false;
+ let tries = 0;
+ for (; tries < maxTries && !conditionPassed; tries++) {
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ window.setTimeout(resolve, interval)
+ );
+ try {
+ conditionPassed = await condition();
+ } catch (e) {
+ throw Error(`${msg} - threw exception: ${e}`);
+ }
+ }
+ if (conditionPassed) {
+ browser.test.succeed(
+ `waitForCondition succeeded after ${tries} retries - ${msg}`
+ );
+ } else {
+ browser.test.fail(`${msg} - timed out after ${maxTries} retries`);
+ }
+}
+
+function sendMessage(...args) {
+ let replyPromise = waitForMessage();
+ browser.test.sendMessage(...args);
+ return replyPromise;
+}
diff --git a/comm/mail/components/extensions/test/xpcshell/head-imap.js b/comm/mail/components/extensions/test/xpcshell/head-imap.js
new file mode 100644
index 0000000000..ac85c52b64
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/head-imap.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from head.js */
+
+var IS_IMAP = true;
+
+let wrappedCreateAccount = createAccount;
+createAccount = function (type = "imap") {
+ return wrappedCreateAccount(type);
+};
diff --git a/comm/mail/components/extensions/test/xpcshell/head-nntp.js b/comm/mail/components/extensions/test/xpcshell/head-nntp.js
new file mode 100644
index 0000000000..0b4a56d0dc
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/head-nntp.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from head.js */
+
+var IS_NNTP = true;
+
+let wrappedCreateAccount = createAccount;
+createAccount = function (type = "nntp") {
+ return wrappedCreateAccount(type);
+};
diff --git a/comm/mail/components/extensions/test/xpcshell/head.js b/comm/mail/components/extensions/test/xpcshell/head.js
new file mode 100644
index 0000000000..f8c0c0e7b9
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/head.js
@@ -0,0 +1,298 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { fsDebugAll, gThreadManager, nsMailServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// Persistent Listener test functionality
+var { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
+
+ExtensionTestUtils.init(this);
+
+var IS_IMAP = false;
+var IS_NNTP = false;
+
+function formatVCard(strings, ...values) {
+ let arr = [];
+ for (let str of strings) {
+ arr.push(str);
+ arr.push(values.shift());
+ }
+ let lines = arr.join("").split("\n");
+ let indent = lines[1].length - lines[1].trimLeft().length;
+ let outLines = [];
+ for (let line of lines) {
+ if (line.length > 0) {
+ outLines.push(line.substring(indent) + "\r\n");
+ }
+ }
+ return outLines.join("");
+}
+
+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
+ );
+ }
+
+ if (type == "imap") {
+ IMAPServer.open();
+ account.incomingServer.port = IMAPServer.port;
+ account.incomingServer.username = "user";
+ account.incomingServer.password = "password";
+ }
+
+ if (type == "nntp") {
+ NNTPServer.open();
+ account.incomingServer.port = NNTPServer.port;
+ }
+ info(`Created account ${account.toString()}`);
+ return account;
+}
+
+function cleanUpAccount(account) {
+ 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) {}
+}
+
+registerCleanupFunction(() => {
+ MailServices.accounts.accounts.forEach(cleanUpAccount);
+});
+
+function addIdentity(account, email = "xpcshell@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) {
+ if (parent.server.type == "nntp") {
+ createNewsgroup(name);
+ let account = MailServices.accounts.FindAccountForServer(parent.server);
+ subscribeNewsgroup(account, name);
+ return parent.getChildNamed(name);
+ }
+
+ let promiseAdded = PromiseTestUtils.promiseFolderAdded(name);
+ parent.createSubfolder(name, null);
+ await promiseAdded;
+ 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);
+ return addGeneratedMessages(folder, messages);
+}
+
+class FakeGeneratedMessage {
+ constructor(msg) {
+ this.msg = msg;
+ }
+ toMessageString() {
+ return this.msg;
+ }
+ toMboxString() {
+ // A cheap hack. It works for existing uses but may not work for future uses.
+ let fromAddress = this.msg.match(/From: .* <(.*@.*)>/)[0];
+ let mBoxString = `From ${fromAddress}\r\n${this.msg}`;
+ // Ensure a trailing empty line.
+ if (!mBoxString.endsWith("\r\n")) {
+ mBoxString = mBoxString + "\r\n";
+ }
+ return mBoxString;
+ }
+}
+
+async function createMessageFromFile(folder, path) {
+ let message = await IOUtils.readUTF8(path);
+ return addGeneratedMessages(folder, [new FakeGeneratedMessage(message)]);
+}
+
+async function createMessageFromString(folder, message) {
+ return addGeneratedMessages(folder, [new FakeGeneratedMessage(message)]);
+}
+
+async function addGeneratedMessages(folder, messages) {
+ if (folder.server.type == "imap") {
+ return IMAPServer.addMessages(folder, messages);
+ }
+ if (folder.server.type == "nntp") {
+ return NNTPServer.addMessages(folder, messages);
+ }
+
+ let messageStrings = messages.map(message => message.toMboxString());
+ folder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ folder.addMessageBatch(messageStrings);
+ folder.callFilterPlugins(null);
+ return Promise.resolve();
+}
+
+async function getUtilsJS() {
+ return IOUtils.readUTF8(do_get_file("data/utils.js").path);
+}
+
+var IMAPServer = {
+ open() {
+ let { ImapDaemon, ImapMessage, IMAP_RFC3501_handler } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Imapd.jsm"
+ );
+ IMAPServer.ImapMessage = ImapMessage;
+
+ this.daemon = new ImapDaemon();
+ this.server = new nsMailServer(
+ daemon => new IMAP_RFC3501_handler(daemon),
+ this.daemon
+ );
+ this.server.start();
+
+ registerCleanupFunction(() => this.close());
+ },
+ close() {
+ this.server.stop();
+ },
+ get port() {
+ return this.server.port;
+ },
+
+ addMessages(folder, messages) {
+ let fakeFolder = IMAPServer.daemon.getMailbox(folder.name);
+ messages.forEach(message => {
+ if (typeof message != "string") {
+ message = message.toMessageString();
+ }
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(message)
+ );
+ let imapMsg = new IMAPServer.ImapMessage(
+ msgURI.spec,
+ fakeFolder.uidnext++,
+ []
+ );
+ fakeFolder.addMessage(imapMsg);
+ });
+
+ return new Promise(resolve =>
+ mailTestUtils.updateFolderAndNotify(folder, resolve)
+ );
+ },
+};
+
+function subscribeNewsgroup(account, group) {
+ account.incomingServer.QueryInterface(Ci.nsINntpIncomingServer);
+ account.incomingServer.subscribeToNewsgroup(group);
+ account.incomingServer.maximumConnectionsNumber = 1;
+}
+
+function createNewsgroup(group) {
+ if (!NNTPServer.hasGroup(group)) {
+ NNTPServer.addGroup(group);
+ }
+}
+
+var NNTPServer = {
+ open() {
+ let { NNTP_RFC977_handler, NntpDaemon } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Nntpd.jsm"
+ );
+
+ this.daemon = new NntpDaemon();
+ this.server = new nsMailServer(
+ daemon => new NNTP_RFC977_handler(daemon),
+ this.daemon
+ );
+ this.server.start();
+
+ registerCleanupFunction(() => this.close());
+ },
+
+ close() {
+ this.server.stop();
+ },
+ get port() {
+ return this.server.port;
+ },
+
+ addGroup(group) {
+ return this.daemon.addGroup(group);
+ },
+
+ hasGroup(group) {
+ return this.daemon.getGroup(group) != null;
+ },
+
+ addMessages(folder, messages) {
+ let { NewsArticle } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Nntpd.jsm"
+ );
+
+ let group = folder.name;
+ messages.forEach(message => {
+ if (typeof message != "string") {
+ message = message.toMessageString();
+ }
+ // The NNTP daemon needs a trailing empty line.
+ if (!message.endsWith("\r\n")) {
+ message = message + "\r\n";
+ }
+ let article = new NewsArticle(message);
+ article.groups = [group];
+ this.daemon.addArticle(article);
+ });
+
+ return new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(folder, resolve);
+ });
+ },
+};
diff --git a/comm/mail/components/extensions/test/xpcshell/images/redPixel.png b/comm/mail/components/extensions/test/xpcshell/images/redPixel.png
new file mode 100644
index 0000000000..abda018027
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/images/redPixel.png
Binary files differ
diff --git a/comm/mail/components/extensions/test/xpcshell/images/whitePixel.png b/comm/mail/components/extensions/test/xpcshell/images/whitePixel.png
new file mode 100644
index 0000000000..5514ad40e9
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/images/whitePixel.png
Binary files differ
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/alternative.eml b/comm/mail/components/extensions/test/xpcshell/messages/alternative.eml
new file mode 100644
index 0000000000..11de6a87d6
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/alternative.eml
@@ -0,0 +1,23 @@
+Message-ID: <alternative.eml@mime.sample>
+Date: Fri, 19 May 2000 00:29:55 -0400
+To: Heinz <mueller@example.com>, Karl <friedrich@example.com>
+From: Doug Sauder <dwsauder@example.com>
+Subject: Default content-types
+Mime-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="=====================_714967308==_.ALT"
+
+This message is in MIME format. The first part should be readable text,
+while the remaining parts are likely unreadable without MIME-aware tools.
+
+--=====================_714967308==_.ALT
+Content-Transfer-Encoding: quoted-printable
+
+I am TEXT!
+
+--=====================_714967308==_.ALT
+Content-Type: text/html
+
+<html><body>I <b>am</b> HTML!</body></html>
+
+--=====================_714967308==_.ALT--
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/attachedMessageWithMissingHeaders.eml b/comm/mail/components/extensions/test/xpcshell/messages/attachedMessageWithMissingHeaders.eml
new file mode 100644
index 0000000000..85a54b66c5
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/attachedMessageWithMissingHeaders.eml
@@ -0,0 +1,35 @@
+Message-ID: <sample.eml@mime.sample>
+Date: Fri, 20 May 2000 00:29:55 -0400
+To: Heinz <mueller@example.com>
+From: Batman <bruce@example.com>
+Subject: Attached message without subject
+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 email attachment with missing headers.<br>
+ </p>
+ </body>
+</html>
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: message/rfc822; charset=UTF-8; name="message1.eml"
+Content-Disposition: attachment; filename="message1.eml"
+Content-Transfer-Encoding: 7bit
+
+Message-ID: <sample-attached.eml@mime.sample>
+MIME-Version: 1.0
+
+This is my body
+
+--------------49CVLb1N6p6Spdka4qq7Naeg--
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/nestedMessages.eml b/comm/mail/components/extensions/test/xpcshell/messages/nestedMessages.eml
new file mode 100644
index 0000000000..5ced639ff8
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/nestedMessages.eml
@@ -0,0 +1,127 @@
+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="message1.eml"
+Content-Disposition: attachment; filename="message1.eml"
+Content-Transfer-Encoding: 7bit
+
+Message-ID: <sample-attached.eml@mime.sample>
+From: Superman <clark.kent@dailyplanet.com>
+To: Jimmy <jimmy.olsen@dailyplanet.com>
+Subject: Test message 1
+Date: Wed, 17 May 2000 19:32:47 -0400
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_0002_01BFC036.AE309650"
+
+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: 7bit
+
+Message with multiple attachments.
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png;
+ name="whitePixel.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="whitePixel.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnn
+AAAAAElFTkSuQmCC
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png;
+ name="greenPixel.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACx
+jwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY+C76AoAAhUBJel4xsMAAAAASUVO
+RK5CYII=
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="redPixel.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACx
+jwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY+hgkAYAAbcApOp/9LEAAAAASUVO
+RK5CYII=
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: message/rfc822; charset=UTF-8; name="message2.eml"
+Content-Disposition: attachment; filename="message2.eml"
+Content-Transfer-Encoding: 7bit
+
+Message-ID: <sample-nested-attached.eml@mime.sample>
+From: Jimmy <jimmy.olsen@dailyplanet.com>
+To: Superman <clark.kent@dailyplanet.com>
+Subject: Test message 2
+Date: Wed, 16 May 2000 19:32:47 -0400
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_0003_01BFC036.AE309650"
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_0003_01BFC036.AE309650
+Content-Type: text/plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: 7bit
+
+This message has an attachment
+
+------=_NextPart_000_0003_01BFC036.AE309650
+Content-Type: image/png;
+ name="whitePixel.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="whitePixel.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnn
+AAAAAElFTkSuQmCC
+
+------=_NextPart_000_0003_01BFC036.AE309650--
+
+------=_NextPart_000_0002_01BFC036.AE309650--
+
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: image/png;
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="yellowPixel.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY/j/iQEABOUB8pypNlQA
+AAAASUVORK5CYII=
+
+--------------49CVLb1N6p6Spdka4qq7Naeg--
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample01.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample01.eml
new file mode 100644
index 0000000000..f7ac14a07d
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample01.eml
@@ -0,0 +1,11 @@
+From: Bug Reporter <new@thunderbird.bug>
+Newsgroups: gmane.comp.mozilla.thundebird.user
+Subject: =?UTF-8?B?zrHOu8+GzqzOss63z4TOvw==?=
+Date: Thu, 27 May 2021 21:23:35 +0100
+Message-ID: <01.eml@mime.sample>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8;
+Content-Transfer-Encoding: base64
+Content-Disposition: inline
+
+zobOu8+GzrEK
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample02.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample02.eml
new file mode 100644
index 0000000000..74b60b5665
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample02.eml
@@ -0,0 +1,121 @@
+From: "Doug Sauder" <doug@example.com>
+To: =?iso-8859-1?Q?Heinz_M=FCller?= <mueller@example.com>
+Subject: Test message from Microsoft Outlook 00
+Date: Wed, 17 May 2000 19:32:47 -0400
+Message-ID: <02.eml@mime.sample>
+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--
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample03.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample03.eml
new file mode 100644
index 0000000000..3eb8e06802
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample03.eml
@@ -0,0 +1,43 @@
+From: =?iso-8859-1?Q?Heinz_M=FCller?= <mueller@example.com>
+To: "Joe Blow" <jblow@example.com>
+Subject: Test message from Microsoft Outlook 00
+Date: Wed, 17 May 2000 19:35:05 -0400
+Message-ID: <03.eml@mime.sample>
+MIME-Version: 1.0
+Content-Type: image/png;
+ name="doubelspace ball.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="doubelspace ball.png"
+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
+
+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==
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample04.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample04.eml
new file mode 100644
index 0000000000..6dd2a94b56
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample04.eml
@@ -0,0 +1,10 @@
+Newsgroups: gmane.comp.mozilla.thundebird.user
+From: Bug Reporter <new@thunderbird.bug>
+Subject: =?koi8-r?B?4czGwdfJ1Ao=?=
+Date: Sun, 27 May 2001 21:23:35 +0100
+MIME-Version: 1.0
+Message-ID: <04.eml@mime.sample>
+Content-Type: text/plain; charset=koi8-r;
+Content-Transfer-Encoding: base64
+
+98/Q0s/TCg== \ No newline at end of file
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample05.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample05.eml
new file mode 100644
index 0000000000..6e70eee744
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample05.eml
@@ -0,0 +1,10 @@
+Newsgroups: gmane.comp.mozilla.thundebird.user
+From: Bug Reporter <new@thunderbird.bug>
+Subject: =?windows-1251?B?wOv04OLo8go=?=
+Date: Sun, 27 May 2001 21:23:35 +0100
+MIME-Version: 1.0
+Message-ID: <05.eml@mime.sample>
+Content-Type: text/plain; charset=windows-1251;
+Content-Transfer-Encoding: base64
+
+wu7v8O7xCg== \ No newline at end of file
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample06.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample06.eml
new file mode 100644
index 0000000000..a5b3a40ac5
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample06.eml
@@ -0,0 +1,8 @@
+Newsgroups: gmane.comp.mozilla.thundebird.user
+From: Bug Reporter <new@thunderbird.bug>
+Subject: I have no content type
+Date: Sun, 27 May 2001 21:23:35 +0100
+MIME-Version: 1.0
+Message-ID: <06.eml@mime.sample>
+
+No content type
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample07.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample07.eml
new file mode 100644
index 0000000000..29283b2ce0
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample07.eml
@@ -0,0 +1,24 @@
+Message-ID: <07.eml@mime.sample>
+Date: Fri, 19 May 2000 00:29:55 -0400
+To: Heinz <mueller@example.com>
+From: Doug Sauder <dwsauder@example.com>
+Subject: Default content-types
+Mime-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="=====================_714967308==_.ALT"
+
+This message is in MIME format. The first part should be readable text,
+while the remaining parts are likely unreadable without MIME-aware tools.
+
+--=====================_714967308==_.ALT
+Content-Transfer-Encoding: quoted-printable
+
+Die Hasen
+
+--=====================_714967308==_.ALT
+Content-Type: text/html
+
+<html><body><b>Die Hasen</b></body></html>
+
+--=====================_714967308==_.ALT--
+
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_accounts.js b/comm/mail/components/extensions/test/xpcshell/test_ext_accounts.js
new file mode 100644
index 0000000000..ac6f5482ce
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_accounts.js
@@ -0,0 +1,1089 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(async function test_accounts() {
+ // Here all the accounts are local but the first account will behave as
+ // an actual local account and will be kept last always.
+ let files = {
+ "background.js": async () => {
+ let [account1Id, account1Name] = await window.waitForMessage();
+
+ let defaultAccount = await browser.accounts.getDefault();
+ browser.test.assertEq(
+ null,
+ defaultAccount,
+ "The default account should be null, as none is defined."
+ );
+
+ let result1 = await browser.accounts.list();
+ browser.test.assertEq(1, result1.length);
+ window.assertDeepEqual(
+ {
+ id: account1Id,
+ name: account1Name,
+ type: "none",
+ folders: [
+ {
+ accountId: account1Id,
+ name: "Trash",
+ path: "/Trash",
+ type: "trash",
+ },
+ {
+ accountId: account1Id,
+ name: "Outbox",
+ path: "/Unsent Messages",
+ type: "outbox",
+ },
+ ],
+ },
+ result1[0]
+ );
+
+ // Test that excluding folders works.
+ let result1WithOutFolders = await browser.accounts.list(false);
+ for (let account of result1WithOutFolders) {
+ browser.test.assertEq(null, account.folders, "Folders not included");
+ }
+
+ let [account2Id, account2Name] = await window.sendMessage(
+ "create account 2"
+ );
+ // The new account is defined as default and should be returned first.
+ let result2 = await browser.accounts.list();
+ browser.test.assertEq(2, result2.length);
+ window.assertDeepEqual(
+ [
+ {
+ id: account2Id,
+ name: account2Name,
+ type: "imap",
+ folders: [
+ {
+ accountId: account2Id,
+ name: "Inbox",
+ path: "/INBOX",
+ type: "inbox",
+ },
+ ],
+ },
+ {
+ id: account1Id,
+ name: account1Name,
+ type: "none",
+ folders: [
+ {
+ accountId: account1Id,
+ name: "Trash",
+ path: "/Trash",
+ type: "trash",
+ },
+ {
+ accountId: account1Id,
+ name: "Outbox",
+ path: "/Unsent Messages",
+ type: "outbox",
+ },
+ ],
+ },
+ ],
+ result2
+ );
+
+ let result3 = await browser.accounts.get(account1Id);
+ window.assertDeepEqual(result1[0], result3);
+ let result4 = await browser.accounts.get(account2Id);
+ window.assertDeepEqual(result2[0], result4);
+
+ let result3WithoutFolders = await browser.accounts.get(account1Id, false);
+ browser.test.assertEq(
+ null,
+ result3WithoutFolders.folders,
+ "Folders not included"
+ );
+ let result4WithoutFolders = await browser.accounts.get(account2Id, false);
+ browser.test.assertEq(
+ null,
+ result4WithoutFolders.folders,
+ "Folders not included"
+ );
+
+ await window.sendMessage("create folders");
+ let result5 = await browser.accounts.get(account1Id);
+ let platformInfo = await browser.runtime.getPlatformInfo();
+ window.assertDeepEqual(
+ [
+ {
+ accountId: account1Id,
+ name: "Trash",
+ path: "/Trash",
+ subFolders: [
+ {
+ accountId: account1Id,
+ name: "%foo %test% 'bar'(!)+",
+ path: "/Trash/%foo %test% 'bar'(!)+",
+ },
+ {
+ accountId: account1Id,
+ name: "Ϟ",
+ // This character is not supported on Windows, so it gets hashed,
+ // by NS_MsgHashIfNecessary.
+ path: platformInfo.os == "win" ? "/Trash/b52bc214" : "/Trash/Ϟ",
+ },
+ ],
+ type: "trash",
+ },
+ {
+ accountId: account1Id,
+ name: "Outbox",
+ path: "/Unsent Messages",
+ type: "outbox",
+ },
+ ],
+ result5.folders
+ );
+
+ // Check we can access the folders through folderPathToURI.
+ for (let folder of result5.folders) {
+ await browser.messages.list(folder);
+ }
+
+ let result6 = await browser.accounts.get(account2Id);
+ window.assertDeepEqual(
+ [
+ {
+ accountId: account2Id,
+ name: "Inbox",
+ path: "/INBOX",
+ subFolders: [
+ {
+ accountId: account2Id,
+ name: "%foo %test% 'bar'(!)+",
+ path: "/INBOX/%foo %test% 'bar'(!)+",
+ },
+ {
+ accountId: account2Id,
+ name: "Ϟ",
+ path: "/INBOX/&A94-",
+ },
+ ],
+ type: "inbox",
+ },
+ {
+ // The trash folder magically appears at this point.
+ // It wasn't here before.
+ accountId: "account2",
+ name: "Trash",
+ path: "/Trash",
+ type: "trash",
+ },
+ ],
+ result6.folders
+ );
+
+ // Check we can access the folders through folderPathToURI.
+ for (let folder of result6.folders) {
+ await browser.messages.list(folder);
+ }
+
+ defaultAccount = await browser.accounts.getDefault();
+ browser.test.assertEq(result2[0].id, defaultAccount.id);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsIdentities", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ let account1 = createAccount();
+ extension.sendMessage(account1.key, account1.incomingServer.prettyName);
+
+ await extension.awaitMessage("create account 2");
+ let account2 = createAccount("imap");
+ IMAPServer.open();
+ account2.incomingServer.port = IMAPServer.port;
+ account2.incomingServer.username = "user";
+ account2.incomingServer.password = "password";
+ MailServices.accounts.defaultAccount = account2;
+ extension.sendMessage(account2.key, account2.incomingServer.prettyName);
+
+ await extension.awaitMessage("create folders");
+ let inbox1 = account1.incomingServer.rootFolder.subFolders[0];
+ // Test our code can handle characters that might be escaped.
+ inbox1.createSubfolder("%foo %test% 'bar'(!)+", null);
+ inbox1.createSubfolder("Ϟ", null); // Test our code can handle unicode.
+
+ let inbox2 = account2.incomingServer.rootFolder.subFolders[0];
+ inbox2.QueryInterface(Ci.nsIMsgImapMailFolder).hierarchyDelimiter = "/";
+ // Test our code can handle characters that might be escaped.
+ inbox2.createSubfolder("%foo %test% 'bar'(!)+", null);
+ await PromiseTestUtils.promiseFolderAdded("%foo %test% 'bar'(!)+");
+ inbox2.createSubfolder("Ϟ", null); // Test our code can handle unicode.
+ await PromiseTestUtils.promiseFolderAdded("Ϟ");
+
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(account1);
+ cleanUpAccount(account2);
+});
+
+add_task(async function test_identities() {
+ let account1 = createAccount();
+ let account2 = createAccount("imap");
+ let identity0 = addIdentity(account1, "id0@invalid");
+ let identity1 = addIdentity(account1, "id1@invalid");
+ let identity2 = addIdentity(account1, "id2@invalid");
+ let identity3 = addIdentity(account2, "id3@invalid");
+ addIdentity(account2, "id4@invalid");
+ identity2.label = "A label";
+ identity2.fullName = "Identity 2!";
+ identity2.organization = "Dis Organization";
+ identity2.replyTo = "reply@invalid";
+ identity2.composeHtml = true;
+ identity2.htmlSigText = "This is me. And this is my Dog.";
+ identity2.htmlSigFormat = false;
+
+ equal(account1.defaultIdentity.key, identity0.key);
+ equal(account2.defaultIdentity.key, identity3.key);
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length);
+
+ const localAccount = accounts.find(account => account.type == "none");
+ const imapAccount = accounts.find(account => account.type == "imap");
+
+ // Register event listener.
+ let onCreatedLog = [];
+ browser.identities.onCreated.addListener((id, created) => {
+ onCreatedLog.push({ id, created });
+ });
+ let onUpdatedLog = [];
+ browser.identities.onUpdated.addListener((id, changed) => {
+ onUpdatedLog.push({ id, changed });
+ });
+ let onDeletedLog = [];
+ browser.identities.onDeleted.addListener(id => {
+ onDeletedLog.push(id);
+ });
+
+ const { id: accountId, identities } = localAccount;
+ const identityIds = identities.map(i => i.id);
+ browser.test.assertEq(3, identities.length);
+
+ browser.test.assertEq(accountId, identities[0].accountId);
+ browser.test.assertEq("id0@invalid", identities[0].email);
+ browser.test.assertEq(accountId, identities[1].accountId);
+ browser.test.assertEq("id1@invalid", identities[1].email);
+ browser.test.assertEq(accountId, identities[2].accountId);
+ browser.test.assertEq("id2@invalid", identities[2].email);
+ browser.test.assertEq("A label", identities[2].label);
+ browser.test.assertEq("Identity 2!", identities[2].name);
+ browser.test.assertEq("Dis Organization", identities[2].organization);
+ browser.test.assertEq("reply@invalid", identities[2].replyTo);
+ browser.test.assertEq(true, identities[2].composeHtml);
+ browser.test.assertEq(
+ "This is me. And this is my Dog.",
+ identities[2].signature
+ );
+ browser.test.assertEq(true, identities[2].signatureIsPlainText);
+
+ // Testing browser.identities.list().
+
+ let allIdentities = await browser.identities.list();
+ browser.test.assertEq(5, allIdentities.length);
+
+ let localIdentities = await browser.identities.list(localAccount.id);
+ browser.test.assertEq(
+ 3,
+ localIdentities.length,
+ "number of local identities is correct"
+ );
+ for (let i = 0; i < 2; i++) {
+ browser.test.assertEq(
+ localAccount.identities[i].id,
+ localIdentities[i].id,
+ "returned local identity is correct"
+ );
+ }
+
+ let imapIdentities = await browser.identities.list(imapAccount.id);
+ browser.test.assertEq(
+ 2,
+ imapIdentities.length,
+ "number of imap identities is correct"
+ );
+ for (let i = 0; i < 1; i++) {
+ browser.test.assertEq(
+ imapAccount.identities[i].id,
+ imapIdentities[i].id,
+ "returned imap identity is correct"
+ );
+ }
+
+ // Testing browser.identities.get().
+
+ let badIdentity = await browser.identities.get("funny");
+ browser.test.assertEq(null, badIdentity);
+
+ for (let identity of identities) {
+ let testIdentity = await browser.identities.get(identity.id);
+ for (let prop of Object.keys(identity)) {
+ browser.test.assertEq(
+ identity[prop],
+ testIdentity[prop],
+ `Testing identity.${prop}`
+ );
+ }
+ }
+
+ // Testing browser.identities.delete().
+
+ let imapDefaultIdentity = await browser.identities.getDefault(
+ imapAccount.id
+ );
+ let imapNonDefaultIdentity = imapIdentities.find(
+ identity => identity.id != imapDefaultIdentity.id
+ );
+
+ await browser.identities.delete(imapNonDefaultIdentity.id);
+ imapIdentities = await browser.identities.list(imapAccount.id);
+ browser.test.assertEq(
+ 1,
+ imapIdentities.length,
+ "number of imap identities after delete is correct"
+ );
+ browser.test.assertEq(
+ imapDefaultIdentity.id,
+ imapIdentities[0].id,
+ "leftover identity after delete is correct"
+ );
+
+ await browser.test.assertRejects(
+ browser.identities.delete(imapDefaultIdentity.id),
+ `Identity ${imapDefaultIdentity.id} is the default identity of account ${imapAccount.id} and cannot be deleted`,
+ "browser.identities.delete threw exception"
+ );
+
+ await browser.test.assertRejects(
+ browser.identities.delete("somethingInvalid"),
+ "Identity not found: somethingInvalid",
+ "browser.identities.delete threw exception"
+ );
+
+ // Testing browser.identities.create().
+
+ let createTests = [
+ {
+ // Set all.
+ accountId: imapAccount.id,
+ details: {
+ email: "id0+test@invalid",
+ label: "TestLabel",
+ name: "Mr. Test",
+ organization: "MZLA",
+ replyTo: "id0+test@invalid",
+ signature: "This is Bruce. And this is my Cat.",
+ composeHtml: true,
+ signatureIsPlainText: false,
+ },
+ },
+ {
+ // Set some.
+ accountId: imapAccount.id,
+ details: {
+ email: "id0+work@invalid",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ },
+ },
+ {
+ // Set none.
+ accountId: imapAccount.id,
+ details: {},
+ },
+ {
+ // Set some on an invalid account.
+ accountId: "somethingInvalid",
+ details: {
+ email: "id0+work@invalid",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ },
+ expectedThrow: `Account not found: somethingInvalid`,
+ },
+ {
+ // Try to set a protected property.
+ accountId: imapAccount.id,
+ details: {
+ accountId: "accountId5",
+ },
+ expectedThrow: `Setting the accountId property of a MailIdentity is not supported.`,
+ },
+ {
+ // Try to set a protected property together with others.
+ accountId: imapAccount.id,
+ details: {
+ id: "id8",
+ email: "id0+work@invalid",
+ label: "TestLabel",
+ name: "Mr. Test",
+ organization: "MZLA",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ signatureIsPlainText: false,
+ },
+ expectedThrow: `Setting the id property of a MailIdentity is not supported.`,
+ },
+ ];
+ for (let createTest of createTests) {
+ if (createTest.expectedThrow) {
+ await browser.test.assertRejects(
+ browser.identities.create(createTest.accountId, createTest.details),
+ createTest.expectedThrow,
+ `It rejects as expected: ${createTest.expectedThrow}.`
+ );
+ } else {
+ let createPromise = new Promise(resolve => {
+ const callback = (id, identity) => {
+ browser.identities.onCreated.removeListener(callback);
+ resolve(identity);
+ };
+ browser.identities.onCreated.addListener(callback);
+ });
+ let createdIdentity = await browser.identities.create(
+ createTest.accountId,
+ createTest.details
+ );
+ let createdIdentity2 = await createPromise;
+
+ let expected = createTest.details;
+ for (let prop of Object.keys(expected)) {
+ browser.test.assertEq(
+ expected[prop],
+ createdIdentity[prop],
+ `Testing created identity.${prop}`
+ );
+ browser.test.assertEq(
+ expected[prop],
+ createdIdentity2[prop],
+ `Testing created identity.${prop}`
+ );
+ }
+ await browser.identities.delete(createdIdentity.id);
+ }
+
+ let foundIdentities = await browser.identities.list(imapAccount.id);
+ browser.test.assertEq(
+ 1,
+ foundIdentities.length,
+ "number of imap identities after create/delete is correct"
+ );
+ }
+
+ // Testing browser.identities.update().
+
+ let updateTests = [
+ {
+ // Set all.
+ identityId: identities[2].id,
+ details: {
+ email: "id0+test@invalid",
+ label: "TestLabel",
+ name: "Mr. Test",
+ organization: "MZLA",
+ replyTo: "id0+test@invalid",
+ signature: "This is Bruce. And this is my Cat.",
+ composeHtml: true,
+ signatureIsPlainText: false,
+ },
+ },
+ {
+ // Set some.
+ identityId: identities[2].id,
+ details: {
+ email: "id0+work@invalid",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ },
+ expected: {
+ email: "id0+work@invalid",
+ label: "TestLabel",
+ name: "Mr. Test",
+ organization: "MZLA",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ signatureIsPlainText: false,
+ },
+ },
+ {
+ // Clear.
+ identityId: identities[2].id,
+ details: {
+ email: "",
+ label: "",
+ name: "",
+ organization: "",
+ replyTo: "",
+ signature: "",
+ composeHtml: false,
+ signatureIsPlainText: true,
+ },
+ },
+ {
+ // Try to update an invalid identity.
+ identityId: "somethingInvalid",
+ details: {
+ email: "id0+work@invalid",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ },
+ expectedThrow: "Identity not found: somethingInvalid",
+ },
+ {
+ // Try to update a protected property.
+ identityId: identities[2].id,
+ details: {
+ accountId: "accountId5",
+ },
+ expectedThrow:
+ "Setting the accountId property of a MailIdentity is not supported.",
+ },
+ {
+ // Try to update another protected property together with others.
+ identityId: identities[2].id,
+ details: {
+ id: "id8",
+ email: "id0+work@invalid",
+ label: "TestLabel",
+ name: "Mr. Test",
+ organization: "MZLA",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ signatureIsPlainText: false,
+ },
+ expectedThrow:
+ "Setting the id property of a MailIdentity is not supported.",
+ },
+ ];
+ for (let updateTest of updateTests) {
+ if (updateTest.expectedThrow) {
+ await browser.test.assertRejects(
+ browser.identities.update(
+ updateTest.identityId,
+ updateTest.details
+ ),
+ updateTest.expectedThrow,
+ `It rejects as expected: ${updateTest.expectedThrow}.`
+ );
+ continue;
+ }
+
+ let updatePromise = new Promise(resolve => {
+ const callback = (id, changed) => {
+ browser.identities.onUpdated.removeListener(callback);
+ resolve(changed);
+ };
+ browser.identities.onUpdated.addListener(callback);
+ });
+ let updatedIdentity = await browser.identities.update(
+ updateTest.identityId,
+ updateTest.details
+ );
+ await updatePromise;
+
+ let returnedIdentity = await browser.identities.get(
+ updateTest.identityId
+ );
+
+ let expected = updateTest.expected || updateTest.details;
+ for (let prop of Object.keys(expected)) {
+ browser.test.assertEq(
+ expected[prop],
+ updatedIdentity[prop],
+ `Testing updated identity.${prop}`
+ );
+ browser.test.assertEq(
+ expected[prop],
+ returnedIdentity[prop],
+ `Testing returned identity.${prop}`
+ );
+ }
+ }
+
+ // Testing getDefault().
+
+ let defaultIdentity = await browser.identities.getDefault(accountId);
+ browser.test.assertEq(identities[0].id, defaultIdentity.id);
+
+ await browser.identities.setDefault(accountId, identityIds[2]);
+ defaultIdentity = await browser.identities.getDefault(accountId);
+ browser.test.assertEq(identities[2].id, defaultIdentity.id);
+
+ let { identities: newIdentities } = await browser.accounts.get(accountId);
+ browser.test.assertEq(3, newIdentities.length);
+ browser.test.assertEq(identityIds[2], newIdentities[0].id);
+ browser.test.assertEq(identityIds[0], newIdentities[1].id);
+ browser.test.assertEq(identityIds[1], newIdentities[2].id);
+
+ await browser.identities.setDefault(accountId, identityIds[1]);
+ defaultIdentity = await browser.identities.getDefault(accountId);
+ browser.test.assertEq(identities[1].id, defaultIdentity.id);
+
+ ({ identities: newIdentities } = await browser.accounts.get(accountId));
+ browser.test.assertEq(3, newIdentities.length);
+ browser.test.assertEq(identityIds[1], newIdentities[0].id);
+ browser.test.assertEq(identityIds[2], newIdentities[1].id);
+ browser.test.assertEq(identityIds[0], newIdentities[2].id);
+
+ // Check event listeners.
+ window.assertDeepEqual(
+ onCreatedLog,
+ [
+ {
+ id: "id6",
+ created: {
+ accountId: "account4",
+ id: "id6",
+ label: "TestLabel",
+ name: "Mr. Test",
+ email: "id0+test@invalid",
+ replyTo: "id0+test@invalid",
+ organization: "MZLA",
+ composeHtml: true,
+ signature: "This is Bruce. And this is my Cat.",
+ signatureIsPlainText: false,
+ },
+ },
+ {
+ id: "id7",
+ created: {
+ accountId: "account4",
+ id: "id7",
+ label: "",
+ name: "",
+ email: "id0+work@invalid",
+ replyTo: "",
+ organization: "",
+ composeHtml: false,
+ signature: "I am Batman.",
+ signatureIsPlainText: true,
+ },
+ },
+ {
+ id: "id8",
+ created: {
+ accountId: "account4",
+ id: "id8",
+ label: "",
+ name: "",
+ email: "",
+ replyTo: "",
+ organization: "",
+ composeHtml: true,
+ signature: "",
+ signatureIsPlainText: true,
+ },
+ },
+ ],
+ "captured onCreated events are correct"
+ );
+ window.assertDeepEqual(
+ onUpdatedLog,
+ [
+ {
+ id: "id3",
+ changed: {
+ label: "TestLabel",
+ name: "Mr. Test",
+ email: "id0+test@invalid",
+ replyTo: "id0+test@invalid",
+ organization: "MZLA",
+ signature: "This is Bruce. And this is my Cat.",
+ signatureIsPlainText: false,
+ accountId: "account3",
+ id: "id3",
+ },
+ },
+ {
+ id: "id3",
+ changed: {
+ email: "id0+work@invalid",
+ replyTo: "",
+ composeHtml: false,
+ signature: "I am Batman.",
+ accountId: "account3",
+ id: "id3",
+ },
+ },
+ {
+ id: "id3",
+ changed: {
+ label: "",
+ name: "",
+ email: "",
+ organization: "",
+ signature: "",
+ signatureIsPlainText: true,
+ accountId: "account3",
+ id: "id3",
+ },
+ },
+ ],
+ "captured onUpdated events are correct"
+ );
+ window.assertDeepEqual(
+ onDeletedLog,
+ ["id5", "id6", "id7", "id8"],
+ "captured onDeleted events are correct"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsIdentities"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ equal(account1.defaultIdentity.key, identity1.key);
+
+ cleanUpAccount(account1);
+ cleanUpAccount(account2);
+});
+
+add_task(async function test_identities_without_write_permissions() {
+ let account = createAccount();
+ let identity0 = addIdentity(account, "id0@invalid");
+
+ equal(account.defaultIdentity.key, identity0.key);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+
+ const [{ identities }] = accounts;
+ browser.test.assertEq(1, identities.length);
+
+ // Testing browser.identities.update().
+
+ await browser.test.assertThrows(
+ () => browser.identities.update(identities[0].id, {}),
+ "browser.identities.update is not a function",
+ "It rejects for a missing permission."
+ );
+
+ // Testing browser.identities.delete().
+
+ await browser.test.assertThrows(
+ () => browser.identities.delete(identities[0].id),
+ "browser.identities.delete is not a function",
+ "It rejects for a missing permission."
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ manifest: {
+ permissions: ["accountsRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(account);
+});
+
+add_task(async function test_accounts_events() {
+ let account1 = createAccount();
+ addIdentity(account1, "id1@invalid");
+
+ let files = {
+ "background.js": async () => {
+ // Register event listener.
+ let onCreatedLog = [];
+ let onUpdatedLog = [];
+ let onDeletedLog = [];
+
+ let createListener = (id, created) => {
+ onCreatedLog.push({ id, created });
+ };
+ let updateListener = (id, changed) => {
+ onUpdatedLog.push({ id, changed });
+ };
+ let deleteListener = id => {
+ onDeletedLog.push(id);
+ };
+
+ await browser.accounts.onCreated.addListener(createListener);
+ await browser.accounts.onUpdated.addListener(updateListener);
+ await browser.accounts.onDeleted.addListener(deleteListener);
+
+ // Create accounts.
+ let imapAccountKey = await window.sendMessage("createAccount", {
+ type: "imap",
+ identity: "user@invalidImap",
+ });
+ let localAccountKey = await window.sendMessage("createAccount", {
+ type: "none",
+ identity: "user@invalidLocal",
+ });
+ let popAccountKey = await window.sendMessage("createAccount", {
+ type: "pop3",
+ identity: "user@invalidPop",
+ });
+
+ // Update account identities.
+ let accounts = await browser.accounts.list();
+ let imapAccount = accounts.find(a => a.id == imapAccountKey);
+ let localAccount = accounts.find(a => a.id == localAccountKey);
+ let popAccount = accounts.find(a => a.id == popAccountKey);
+
+ let id1 = await browser.identities.create(imapAccount.id, {
+ composeHtml: true,
+ email: "user1@inter.net",
+ name: "user1",
+ });
+ let id2 = await browser.identities.create(localAccount.id, {
+ composeHtml: false,
+ email: "user2@inter.net",
+ name: "user2",
+ });
+ let id3 = await browser.identities.create(popAccount.id, {
+ composeHtml: false,
+ email: "user3@inter.net",
+ name: "user3",
+ });
+
+ await browser.identities.setDefault(imapAccount.id, id1.id);
+ browser.test.assertEq(
+ id1.id,
+ (await browser.identities.getDefault(imapAccount.id)).id
+ );
+ await browser.identities.setDefault(localAccount.id, id2.id);
+ browser.test.assertEq(
+ id2.id,
+ (await browser.identities.getDefault(localAccount.id)).id
+ );
+ await browser.identities.setDefault(popAccount.id, id3.id);
+ browser.test.assertEq(
+ id3.id,
+ (await browser.identities.getDefault(popAccount.id)).id
+ );
+
+ // Update account names.
+ await window.sendMessage("updateAccountName", {
+ accountKey: imapAccountKey,
+ name: "Test1",
+ });
+ await window.sendMessage("updateAccountName", {
+ accountKey: localAccountKey,
+ name: "Test2",
+ });
+ await window.sendMessage("updateAccountName", {
+ accountKey: popAccountKey,
+ name: "Test3",
+ });
+
+ // Delete accounts.
+ await window.sendMessage("removeAccount", {
+ accountKey: imapAccountKey,
+ });
+ await window.sendMessage("removeAccount", {
+ accountKey: localAccountKey,
+ });
+ await window.sendMessage("removeAccount", {
+ accountKey: popAccountKey,
+ });
+
+ await browser.accounts.onCreated.removeListener(createListener);
+ await browser.accounts.onUpdated.removeListener(updateListener);
+ await browser.accounts.onDeleted.removeListener(deleteListener);
+
+ // Check event listeners.
+ browser.test.assertEq(3, onCreatedLog.length);
+ window.assertDeepEqual(
+ [
+ {
+ id: "account7",
+ created: {
+ id: "account7",
+ type: "imap",
+ identities: [],
+ name: "Mail for account7user@localhost",
+ folders: null,
+ },
+ },
+ {
+ id: "account8",
+ created: {
+ id: "account8",
+ type: "none",
+ identities: [],
+ name: "account8user on localhost",
+ folders: null,
+ },
+ },
+ {
+ id: "account9",
+ created: {
+ id: "account9",
+ type: "pop3",
+ identities: [],
+ name: "account9user on localhost",
+ folders: null,
+ },
+ },
+ ],
+ onCreatedLog,
+ "captured onCreated events are correct"
+ );
+ window.assertDeepEqual(
+ [
+ {
+ id: "account7",
+ changed: { id: "account7", name: "Mail for user@localhost" },
+ },
+ {
+ id: "account7",
+ changed: {
+ id: "account7",
+ defaultIdentity: { id: "id11" },
+ },
+ },
+ {
+ id: "account8",
+ changed: {
+ id: "account8",
+ defaultIdentity: { id: "id12" },
+ },
+ },
+ {
+ id: "account9",
+ changed: {
+ id: "account9",
+ defaultIdentity: { id: "id13" },
+ },
+ },
+ {
+ id: "account7",
+ changed: {
+ id: "account7",
+ defaultIdentity: { id: "id14" },
+ },
+ },
+ {
+ id: "account8",
+ changed: {
+ id: "account8",
+ defaultIdentity: { id: "id15" },
+ },
+ },
+ {
+ id: "account9",
+ changed: {
+ id: "account9",
+ defaultIdentity: { id: "id16" },
+ },
+ },
+ {
+ id: "account7",
+ changed: {
+ id: "account7",
+ name: "Test1",
+ },
+ },
+ {
+ id: "account8",
+ changed: {
+ id: "account8",
+ name: "Test2",
+ },
+ },
+ {
+ id: "account9",
+ changed: {
+ id: "account9",
+ name: "Test3",
+ },
+ },
+ ],
+ onUpdatedLog,
+ "captured onUpdated events are correct"
+ );
+ window.assertDeepEqual(
+ ["account7", "account8", "account9"],
+ onDeletedLog,
+ "captured onDeleted events are correct"
+ );
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => window.setTimeout(r, 250));
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsIdentities"],
+ },
+ });
+
+ extension.onMessage("createAccount", details => {
+ let account = createAccount(details.type);
+ addIdentity(account, details.identity);
+ extension.sendMessage(account.key);
+ });
+ extension.onMessage("updateAccountName", details => {
+ let account = MailServices.accounts.getAccount(details.accountKey);
+ account.incomingServer.prettyName = details.name;
+ extension.sendMessage();
+ });
+ extension.onMessage("removeAccount", details => {
+ let account = MailServices.accounts.getAccount(details.accountKey);
+ cleanUpAccount(account);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(account1);
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_accounts_mv3_event_pages.js b/comm/mail/components/extensions/test/xpcshell/test_ext_accounts_mv3_event_pages.js
new file mode 100644
index 0000000000..0ac4394f40
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_accounts_mv3_event_pages.js
@@ -0,0 +1,220 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+add_task(async function test_accounts_MV3_event_pages() {
+ await AddonTestUtils.promiseStartupManager();
+
+ 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;
+
+ for (let eventName of ["onCreated", "onUpdated", "onDeleted"]) {
+ browser.accounts[eventName].addListener(async (...args) => {
+ browser.test.sendMessage(`${eventName} event received`, {
+ eventCount: ++eventCounter,
+ 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", "accountsIdentities"],
+ },
+ });
+
+ 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 = [
+ "accounts.onCreated",
+ "accounts.onUpdated",
+ "accounts.onDeleted",
+ ];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ let testData = [
+ {
+ type: "imap",
+ identity: "user@invalidImap",
+ expectedUpdate: true,
+ expectedName: accountKey => `Mail for ${accountKey}user@localhost`,
+ expectedType: "imap",
+ updatedName: "Test1",
+ },
+ {
+ type: "pop3",
+ identity: "user@invalidPop",
+ expectedUpdate: false,
+ expectedName: accountKey => `${accountKey}user on localhost`,
+ expectedType: "pop3",
+ updatedName: "Test2",
+ },
+ {
+ type: "none",
+ identity: "user@invalidLocal",
+ expectedUpdate: false,
+ expectedName: accountKey => `${accountKey}user on localhost`,
+ expectedType: "none",
+ updatedName: "Test3",
+ },
+ {
+ type: "local",
+ identity: "user@invalidLocal",
+ expectedUpdate: false,
+ expectedName: accountKey => "Local Folders",
+ expectedType: "none",
+ updatedName: "Test4",
+ },
+ ];
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+
+ // Verify persistent listener, not yet primed.
+ checkPersistentListeners({ primed: false });
+
+ // Create.
+
+ for (let details of testData) {
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ let account = createAccount(details.type);
+ details.account = account;
+
+ {
+ let rv = await extension.awaitMessage("onCreated event received");
+ Assert.deepEqual(
+ {
+ eventCount: 1,
+ args: [
+ details.account.key,
+ {
+ id: details.account.key,
+ name: details.expectedName(account.key),
+ type: details.expectedType,
+ folders: null,
+ identities: [],
+ },
+ ],
+ },
+ rv,
+ "The primed onCreated event should return the correct values"
+ );
+ }
+
+ if (details.expectedUpdate) {
+ let rv = await extension.awaitMessage("onUpdated event received");
+ Assert.deepEqual(
+ {
+ eventCount: 2,
+ args: [
+ details.account.key,
+ { id: details.account.key, name: "Mail for user@localhost" },
+ ],
+ },
+ rv,
+ "The non-primed onUpdated event should return the correct values"
+ );
+ }
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+ }
+
+ // Update.
+
+ for (let details of testData) {
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ let account = MailServices.accounts.getAccount(details.account.key);
+ account.incomingServer.prettyName = details.updatedName;
+ let rv = await extension.awaitMessage("onUpdated event received");
+
+ Assert.deepEqual(
+ {
+ eventCount: 1,
+ args: [
+ details.account.key,
+ {
+ id: details.account.key,
+ name: details.updatedName,
+ },
+ ],
+ },
+ rv,
+ "The primed onUpdated event should return the correct values"
+ );
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+ }
+
+ // Delete.
+
+ for (let details of testData) {
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ cleanUpAccount(details.account);
+ let rv = await extension.awaitMessage("onDeleted event received");
+
+ Assert.deepEqual(
+ {
+ eventCount: 1,
+ args: [details.account.key],
+ },
+ rv,
+ "The primed onDeleted event should return the correct values"
+ );
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+ }
+
+ await extension.unload();
+
+ await AddonTestUtils.promiseShutdownManager();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js
new file mode 100644
index 0000000000..8fcc3ca14f
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js
@@ -0,0 +1,2043 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddrBookCard: "resource:///modules/AddrBookCard.jsm",
+ AddrBookUtils: "resource:///modules/AddrBookUtils.jsm",
+});
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+add_setup(async () => {
+ Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
+
+ registerCleanupFunction(() => {
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+ });
+});
+
+add_task(async function test_addressBooks() {
+ async function background() {
+ let firstBookId, secondBookId, newContactId;
+
+ let events = [];
+ let eventPromise;
+ let eventPromiseResolve;
+ for (let eventNamespace of ["addressBooks", "contacts", "mailingLists"]) {
+ for (let eventName of [
+ "onCreated",
+ "onUpdated",
+ "onDeleted",
+ "onMemberAdded",
+ "onMemberRemoved",
+ ]) {
+ if (eventName in browser[eventNamespace]) {
+ browser[eventNamespace][eventName].addListener((...args) => {
+ events.push({ namespace: eventNamespace, name: eventName, args });
+ if (eventPromiseResolve) {
+ let resolve = eventPromiseResolve;
+ eventPromiseResolve = null;
+ resolve();
+ }
+ });
+ }
+ }
+ }
+
+ let outsideEvent = function (action, ...args) {
+ eventPromise = new Promise(resolve => {
+ eventPromiseResolve = resolve;
+ });
+ return window.sendMessage("outsideEventsTest", action, ...args);
+ };
+ let checkEvents = async function (...expectedEvents) {
+ if (eventPromiseResolve) {
+ await eventPromise;
+ }
+
+ browser.test.assertEq(
+ expectedEvents.length,
+ events.length,
+ "Correct number of events"
+ );
+
+ if (expectedEvents.length != events.length) {
+ for (let event of events) {
+ let args = event.args.join(", ");
+ browser.test.log(`${event.namespace}.${event.name}(${args})`);
+ }
+ throw new Error("Wrong number of events, stopping.");
+ }
+
+ for (let [namespace, name, ...expectedArgs] of expectedEvents) {
+ let event = events.shift();
+ browser.test.assertEq(
+ namespace,
+ event.namespace,
+ "Event namespace is correct"
+ );
+ browser.test.assertEq(name, event.name, "Event type is correct");
+ browser.test.assertEq(
+ expectedArgs.length,
+ event.args.length,
+ "Argument count is correct"
+ );
+ window.assertDeepEqual(expectedArgs, event.args);
+ if (expectedEvents.length == 1) {
+ return event.args;
+ }
+ }
+
+ return null;
+ };
+
+ async function addressBookTest() {
+ browser.test.log("Starting addressBookTest");
+ let list = await browser.addressBooks.list();
+ browser.test.assertEq(2, list.length);
+ for (let b of list) {
+ browser.test.assertEq(5, Object.keys(b).length);
+ browser.test.assertEq(36, b.id.length);
+ browser.test.assertEq("addressBook", b.type);
+ browser.test.assertTrue("name" in b);
+ browser.test.assertFalse(b.readOnly);
+ browser.test.assertFalse(b.remote);
+ }
+
+ let completeList = await browser.addressBooks.list(true);
+ browser.test.assertEq(2, completeList.length);
+ for (let b of completeList) {
+ browser.test.assertEq(7, Object.keys(b).length);
+ }
+
+ firstBookId = list[0].id;
+ secondBookId = list[1].id;
+
+ let firstBook = await browser.addressBooks.get(firstBookId);
+ browser.test.assertEq(5, Object.keys(firstBook).length);
+
+ let secondBook = await browser.addressBooks.get(secondBookId, true);
+ browser.test.assertEq(7, Object.keys(secondBook).length);
+ browser.test.assertTrue(Array.isArray(secondBook.contacts));
+ browser.test.assertEq(0, secondBook.contacts.length);
+ browser.test.assertTrue(Array.isArray(secondBook.mailingLists));
+ browser.test.assertEq(0, secondBook.mailingLists.length);
+ let newBookId = await browser.addressBooks.create({ name: "test name" });
+ browser.test.assertEq(36, newBookId.length);
+ await checkEvents([
+ "addressBooks",
+ "onCreated",
+ { type: "addressBook", id: newBookId },
+ ]);
+
+ list = await browser.addressBooks.list();
+ browser.test.assertEq(3, list.length);
+
+ let newBook = await browser.addressBooks.get(newBookId);
+ browser.test.assertEq(newBookId, newBook.id);
+ browser.test.assertEq("addressBook", newBook.type);
+ browser.test.assertEq("test name", newBook.name);
+
+ await browser.addressBooks.update(newBookId, { name: "new name" });
+ await checkEvents([
+ "addressBooks",
+ "onUpdated",
+ { type: "addressBook", id: newBookId },
+ ]);
+ let updatedBook = await browser.addressBooks.get(newBookId);
+ browser.test.assertEq("new name", updatedBook.name);
+
+ list = await browser.addressBooks.list();
+ browser.test.assertEq(3, list.length);
+
+ await browser.addressBooks.delete(newBookId);
+ await checkEvents(["addressBooks", "onDeleted", newBookId]);
+
+ list = await browser.addressBooks.list();
+ browser.test.assertEq(2, list.length);
+
+ for (let operation of ["get", "update", "delete"]) {
+ let args = [newBookId];
+ if (operation == "update") {
+ args.push({ name: "" });
+ }
+
+ try {
+ await browser.addressBooks[operation].apply(
+ browser.addressBooks,
+ args
+ );
+ browser.test.fail(
+ `Calling ${operation} on a non-existent address book should throw`
+ );
+ } catch (ex) {
+ browser.test.assertEq(
+ `addressBook with id=${newBookId} could not be found.`,
+ ex.message,
+ `browser.addressBooks.${operation} threw exception`
+ );
+ }
+ }
+
+ // Test the prevention of creating new address book with an empty name
+ await browser.test.assertRejects(
+ browser.addressBooks.create({ name: "" }),
+ "An unexpected error occurred",
+ "browser.addressBooks.create threw exception"
+ );
+
+ browser.test.assertEq(0, events.length, "No events left unconsumed");
+ browser.test.log("Completed addressBookTest");
+ }
+
+ async function contactsTest() {
+ browser.test.log("Starting contactsTest");
+ let contacts = await browser.contacts.list(firstBookId);
+ browser.test.assertTrue(Array.isArray(contacts));
+ browser.test.assertEq(0, contacts.length);
+
+ newContactId = await browser.contacts.create(firstBookId, {
+ FirstName: "first",
+ LastName: "last",
+ Notes: "Notes",
+ SomethingCustom: "Custom property",
+ });
+ browser.test.assertEq(36, newContactId.length);
+ await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ ]);
+
+ contacts = await browser.contacts.list(firstBookId);
+ browser.test.assertEq(1, contacts.length, "Contact added to first book.");
+ browser.test.assertEq(contacts[0].id, newContactId);
+
+ contacts = await browser.contacts.list(secondBookId);
+ browser.test.assertEq(
+ 0,
+ contacts.length,
+ "Contact not added to second book."
+ );
+
+ let newContact = await browser.contacts.get(newContactId);
+ browser.test.assertEq(6, Object.keys(newContact).length);
+ browser.test.assertEq(newContactId, newContact.id);
+ browser.test.assertEq(firstBookId, newContact.parentId);
+ browser.test.assertEq("contact", newContact.type);
+ browser.test.assertEq(false, newContact.readOnly);
+ browser.test.assertEq(false, newContact.remote);
+ browser.test.assertEq(5, Object.keys(newContact.properties).length);
+ browser.test.assertEq("first", newContact.properties.FirstName);
+ browser.test.assertEq("last", newContact.properties.LastName);
+ browser.test.assertEq("Notes", newContact.properties.Notes);
+ browser.test.assertEq(
+ "Custom property",
+ newContact.properties.SomethingCustom
+ );
+ browser.test.assertEq(
+ `BEGIN:VCARD\r\nVERSION:4.0\r\nNOTE:Notes\r\nN:last;first;;;\r\nUID:${newContactId}\r\nEND:VCARD\r\n`,
+ newContact.properties.vCard
+ );
+
+ // Changing the UID should throw.
+ try {
+ await browser.contacts.update(newContactId, {
+ vCard: `BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:SomethingNew\r\nEND:VCARD\r\n`,
+ });
+ browser.test.fail(
+ `Updating a contact with a vCard with a differnt UID should throw`
+ );
+ } catch (ex) {
+ browser.test.assertEq(
+ `The card's UID ${newContactId} may not be changed: BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:SomethingNew\r\nEND:VCARD\r\n.`,
+ ex.message,
+ `browser.contacts.update threw exception`
+ );
+ }
+
+ // Test Custom1.
+ {
+ await browser.contacts.update(newContactId, {
+ vCard: `BEGIN:VCARD\r\nVERSION:4.0\r\nNOTE:Notes\r\nN:last;first;;;\r\nX-CUSTOM1;VALUE=TEXT:Original custom value\r\nEND:VCARD`,
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ Custom1: { oldValue: null, newValue: "Original custom value" },
+ },
+ ]);
+ let updContact1 = await browser.contacts.get(newContactId);
+ browser.test.assertEq(
+ "Original custom value",
+ updContact1.properties.Custom1
+ );
+
+ await browser.contacts.update(newContactId, {
+ Custom1: "Updated custom value",
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ Custom1: {
+ oldValue: "Original custom value",
+ newValue: "Updated custom value",
+ },
+ },
+ ]);
+ let updContact2 = await browser.contacts.get(newContactId);
+ browser.test.assertEq(
+ "Updated custom value",
+ updContact2.properties.Custom1
+ );
+ browser.test.assertTrue(
+ updContact2.properties.vCard.includes(
+ "X-CUSTOM1;VALUE=TEXT:Updated custom value"
+ ),
+ "vCard should include the correct x-custom1 entry"
+ );
+ }
+
+ // If a vCard and legacy properties are given, vCard must win.
+ await browser.contacts.update(newContactId, {
+ vCard: `BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:${newContactId}\r\nEND:VCARD\r\n`,
+ FirstName: "Superman",
+ PrimaryEmail: "c.kent@dailyplanet.com",
+ PreferDisplayName: "0",
+ OtherCustom: "Yet another custom property",
+ Notes: "Ignored Notes",
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ PrimaryEmail: { oldValue: null, newValue: "first@last" },
+ LastName: { oldValue: "last", newValue: null },
+ OtherCustom: {
+ oldValue: null,
+ newValue: "Yet another custom property",
+ },
+ PreferDisplayName: { oldValue: null, newValue: "0" },
+ Custom1: { oldValue: "Updated custom value", newValue: null },
+ },
+ ]);
+
+ let updatedContact = await browser.contacts.get(newContactId);
+ browser.test.assertEq(6, Object.keys(updatedContact.properties).length);
+ browser.test.assertEq("first", updatedContact.properties.FirstName);
+ browser.test.assertEq(
+ "first@last",
+ updatedContact.properties.PrimaryEmail
+ );
+ browser.test.assertTrue(!("LastName" in updatedContact.properties));
+ browser.test.assertTrue(
+ !("Notes" in updatedContact.properties),
+ "The vCard is not specifying Notes and the specified Notes property should be ignored."
+ );
+ browser.test.assertEq(
+ "Custom property",
+ updatedContact.properties.SomethingCustom,
+ "Untouched custom properties should not be changed by updating the vCard"
+ );
+ browser.test.assertEq(
+ "Yet another custom property",
+ updatedContact.properties.OtherCustom,
+ "Custom properties should be added even while updating a vCard"
+ );
+ browser.test.assertEq(
+ "0",
+ updatedContact.properties.PreferDisplayName,
+ "Setting non-banished properties parallel to a vCard should update"
+ );
+ browser.test.assertEq(
+ `BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:${newContactId}\r\nEND:VCARD\r\n`,
+ updatedContact.properties.vCard
+ );
+
+ // Manually Remove properties.
+ await browser.contacts.update(newContactId, {
+ LastName: "lastname",
+ PrimaryEmail: null,
+ SecondEmail: "test@invalid.de",
+ SomethingCustom: null,
+ OtherCustom: null,
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ LastName: { oldValue: null, newValue: "lastname" },
+ // It is how it is. Defining a 2nd email with no 1st, will make it the first.
+ PrimaryEmail: { oldValue: "first@last", newValue: "test@invalid.de" },
+ SomethingCustom: { oldValue: "Custom property", newValue: null },
+ OtherCustom: {
+ oldValue: "Yet another custom property",
+ newValue: null,
+ },
+ },
+ ]);
+
+ updatedContact = await browser.contacts.get(newContactId);
+ browser.test.assertEq(5, Object.keys(updatedContact.properties).length);
+ // LastName and FirstName are stored in the same multi field property and changing LastName should not change FirstName.
+ browser.test.assertEq("first", updatedContact.properties.FirstName);
+ browser.test.assertEq("lastname", updatedContact.properties.LastName);
+ browser.test.assertEq(
+ "test@invalid.de",
+ updatedContact.properties.PrimaryEmail
+ );
+ browser.test.assertTrue(
+ !("SomethingCustom" in updatedContact.properties)
+ );
+ browser.test.assertTrue(!("OtherCustom" in updatedContact.properties));
+ browser.test.assertEq(
+ `BEGIN:VCARD\r\nVERSION:4.0\r\nN:lastname;first;;;\r\nEMAIL:test@invalid.de\r\nUID:${newContactId}\r\nEND:VCARD\r\n`,
+ updatedContact.properties.vCard
+ );
+
+ // Add an email address, going from 1 to 2.Also remove FirstName, LastName should stay.
+ await browser.contacts.update(newContactId, {
+ FirstName: null,
+ PrimaryEmail: "new1@invalid.de",
+ SecondEmail: "new2@invalid.de",
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ PrimaryEmail: {
+ oldValue: "test@invalid.de",
+ newValue: "new1@invalid.de",
+ },
+ SecondEmail: { oldValue: null, newValue: "new2@invalid.de" },
+ FirstName: { oldValue: "first", newValue: null },
+ },
+ ]);
+
+ updatedContact = await browser.contacts.get(newContactId);
+ browser.test.assertEq(5, Object.keys(updatedContact.properties).length);
+ browser.test.assertEq("lastname", updatedContact.properties.LastName);
+ browser.test.assertEq(
+ "new1@invalid.de",
+ updatedContact.properties.PrimaryEmail
+ );
+ browser.test.assertEq(
+ "new2@invalid.de",
+ updatedContact.properties.SecondEmail
+ );
+ browser.test.assertEq(
+ `BEGIN:VCARD\r\nVERSION:4.0\r\nN:lastname;;;;\r\nEMAIL;PREF=1:new1@invalid.de\r\nUID:${newContactId}\r\nEMAIL:new2@invalid.de\r\nEND:VCARD\r\n`,
+ updatedContact.properties.vCard
+ );
+
+ // Remove and email address, going from 2 to 1.
+ await browser.contacts.update(newContactId, {
+ SecondEmail: null,
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ SecondEmail: { oldValue: "new2@invalid.de", newValue: null },
+ },
+ ]);
+
+ updatedContact = await browser.contacts.get(newContactId);
+ browser.test.assertEq(4, Object.keys(updatedContact.properties).length);
+ browser.test.assertEq("lastname", updatedContact.properties.LastName);
+ browser.test.assertEq(
+ "new1@invalid.de",
+ updatedContact.properties.PrimaryEmail
+ );
+ browser.test.assertEq(
+ `BEGIN:VCARD\r\nVERSION:4.0\r\nN:lastname;;;;\r\nEMAIL;PREF=1:new1@invalid.de\r\nUID:${newContactId}\r\nEND:VCARD\r\n`,
+ updatedContact.properties.vCard
+ );
+
+ // Set a fixed UID.
+ let fixedContactId = await browser.contacts.create(
+ firstBookId,
+ "this is a test",
+ {
+ FirstName: "a",
+ LastName: "test",
+ }
+ );
+ browser.test.assertEq("this is a test", fixedContactId);
+ await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: firstBookId, id: "this is a test" },
+ ]);
+
+ let fixedContact = await browser.contacts.get("this is a test");
+ browser.test.assertEq("this is a test", fixedContact.id);
+
+ await browser.contacts.delete("this is a test");
+ await checkEvents([
+ "contacts",
+ "onDeleted",
+ firstBookId,
+ "this is a test",
+ ]);
+
+ try {
+ await browser.contacts.create(firstBookId, newContactId, {
+ FirstName: "uh",
+ LastName: "oh",
+ });
+ browser.test.fail(`Adding a contact with a duplicate id should throw`);
+ } catch (ex) {
+ browser.test.assertEq(
+ `Duplicate contact id: ${newContactId}`,
+ ex.message,
+ `browser.contacts.create threw exception`
+ );
+ }
+
+ browser.test.assertEq(0, events.length, "No events left unconsumed");
+ browser.test.log("Completed contactsTest");
+ }
+
+ async function mailingListsTest() {
+ browser.test.log("Starting mailingListsTest");
+ let mailingLists = await browser.mailingLists.list(firstBookId);
+ browser.test.assertTrue(Array.isArray(mailingLists));
+ browser.test.assertEq(0, mailingLists.length);
+
+ let newMailingListId = await browser.mailingLists.create(firstBookId, {
+ name: "name",
+ });
+ browser.test.assertEq(36, newMailingListId.length);
+ await checkEvents([
+ "mailingLists",
+ "onCreated",
+ { type: "mailingList", parentId: firstBookId, id: newMailingListId },
+ ]);
+
+ mailingLists = await browser.mailingLists.list(firstBookId);
+ browser.test.assertEq(
+ 1,
+ mailingLists.length,
+ "List added to first book."
+ );
+
+ mailingLists = await browser.mailingLists.list(secondBookId);
+ browser.test.assertEq(
+ 0,
+ mailingLists.length,
+ "List not added to second book."
+ );
+
+ let newAddressList = await browser.mailingLists.get(newMailingListId);
+ browser.test.assertEq(8, Object.keys(newAddressList).length);
+ browser.test.assertEq(newMailingListId, newAddressList.id);
+ browser.test.assertEq(firstBookId, newAddressList.parentId);
+ browser.test.assertEq("mailingList", newAddressList.type);
+ browser.test.assertEq("name", newAddressList.name);
+ browser.test.assertEq("", newAddressList.nickName);
+ browser.test.assertEq("", newAddressList.description);
+ browser.test.assertEq(false, newAddressList.readOnly);
+ browser.test.assertEq(false, newAddressList.remote);
+
+ // Test that a valid name is ensured for an existing mail list
+ await browser.test.assertRejects(
+ browser.mailingLists.update(newMailingListId, {
+ name: "",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.update(newMailingListId, {
+ name: "Two spaces invalid name",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.update(newMailingListId, {
+ name: "><<<",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ await browser.mailingLists.update(newMailingListId, {
+ name: "name!",
+ nickName: "nickname!",
+ description: "description!",
+ });
+ await checkEvents([
+ "mailingLists",
+ "onUpdated",
+ { type: "mailingList", parentId: firstBookId, id: newMailingListId },
+ ]);
+
+ let updatedMailingList = await browser.mailingLists.get(newMailingListId);
+ browser.test.assertEq("name!", updatedMailingList.name);
+ browser.test.assertEq("nickname!", updatedMailingList.nickName);
+ browser.test.assertEq("description!", updatedMailingList.description);
+
+ await browser.mailingLists.addMember(newMailingListId, newContactId);
+ await checkEvents([
+ "mailingLists",
+ "onMemberAdded",
+ { type: "contact", parentId: newMailingListId, id: newContactId },
+ ]);
+
+ let listMembers = await browser.mailingLists.listMembers(
+ newMailingListId
+ );
+ browser.test.assertTrue(Array.isArray(listMembers));
+ browser.test.assertEq(1, listMembers.length);
+
+ let anotherContactId = await browser.contacts.create(firstBookId, {
+ FirstName: "second",
+ LastName: "last",
+ PrimaryEmail: "em@il",
+ });
+ await checkEvents([
+ "contacts",
+ "onCreated",
+ {
+ type: "contact",
+ parentId: firstBookId,
+ id: anotherContactId,
+ readOnly: false,
+ },
+ ]);
+
+ await browser.mailingLists.addMember(newMailingListId, anotherContactId);
+ await checkEvents([
+ "mailingLists",
+ "onMemberAdded",
+ { type: "contact", parentId: newMailingListId, id: anotherContactId },
+ ]);
+
+ listMembers = await browser.mailingLists.listMembers(newMailingListId);
+ browser.test.assertEq(2, listMembers.length);
+
+ await browser.contacts.delete(anotherContactId);
+ await checkEvents(
+ ["contacts", "onDeleted", firstBookId, anotherContactId],
+ ["mailingLists", "onMemberRemoved", newMailingListId, anotherContactId]
+ );
+ listMembers = await browser.mailingLists.listMembers(newMailingListId);
+ browser.test.assertEq(1, listMembers.length);
+
+ await browser.mailingLists.removeMember(newMailingListId, newContactId);
+ await checkEvents([
+ "mailingLists",
+ "onMemberRemoved",
+ newMailingListId,
+ newContactId,
+ ]);
+ listMembers = await browser.mailingLists.listMembers(newMailingListId);
+ browser.test.assertEq(0, listMembers.length);
+
+ await browser.mailingLists.delete(newMailingListId);
+ await checkEvents([
+ "mailingLists",
+ "onDeleted",
+ firstBookId,
+ newMailingListId,
+ ]);
+
+ mailingLists = await browser.mailingLists.list(firstBookId);
+ browser.test.assertEq(0, mailingLists.length);
+
+ for (let operation of [
+ "get",
+ "update",
+ "delete",
+ "listMembers",
+ "addMember",
+ "removeMember",
+ ]) {
+ let args = [newMailingListId];
+ switch (operation) {
+ case "update":
+ args.push({ name: "" });
+ break;
+ case "addMember":
+ case "removeMember":
+ args.push(newContactId);
+ break;
+ }
+
+ try {
+ await browser.mailingLists[operation].apply(
+ browser.mailingLists,
+ args
+ );
+ browser.test.fail(
+ `Calling ${operation} on a non-existent mailing list should throw`
+ );
+ } catch (ex) {
+ browser.test.assertEq(
+ `mailingList with id=${newMailingListId} could not be found.`,
+ ex.message,
+ `browser.mailingLists.${operation} threw exception`
+ );
+ }
+ }
+
+ // Test that a valid name is ensured for a new mail list
+ await browser.test.assertRejects(
+ browser.mailingLists.create(firstBookId, {
+ name: "",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.create(firstBookId, {
+ name: "Two spaces invalid name",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.create(firstBookId, {
+ name: "><<<",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ browser.test.assertEq(0, events.length, "No events left unconsumed");
+ browser.test.log("Completed mailingListsTest");
+ }
+
+ async function contactRemovalTest() {
+ browser.test.log("Starting contactRemovalTest");
+ await browser.contacts.delete(newContactId);
+ await checkEvents(["contacts", "onDeleted", firstBookId, newContactId]);
+
+ for (let operation of ["get", "update", "delete"]) {
+ let args = [newContactId];
+ if (operation == "update") {
+ args.push({});
+ }
+
+ try {
+ await browser.contacts[operation].apply(browser.contacts, args);
+ browser.test.fail(
+ `Calling ${operation} on a non-existent contact should throw`
+ );
+ } catch (ex) {
+ browser.test.assertEq(
+ `contact with id=${newContactId} could not be found.`,
+ ex.message,
+ `browser.contacts.${operation} threw exception`
+ );
+ }
+ }
+
+ let contacts = await browser.contacts.list(firstBookId);
+ browser.test.assertEq(0, contacts.length);
+
+ browser.test.assertEq(0, events.length, "No events left unconsumed");
+ browser.test.log("Completed contactRemovalTest");
+ }
+
+ async function outsideEventsTest() {
+ browser.test.log("Starting outsideEventsTest");
+ let [bookId, newBookPrefId] = await outsideEvent("createAddressBook");
+ let [newBook] = await checkEvents([
+ "addressBooks",
+ "onCreated",
+ { type: "addressBook", id: bookId },
+ ]);
+ browser.test.assertEq("external add", newBook.name);
+
+ await outsideEvent("updateAddressBook", newBookPrefId);
+ let [updatedBook] = await checkEvents([
+ "addressBooks",
+ "onUpdated",
+ { type: "addressBook", id: bookId },
+ ]);
+ browser.test.assertEq("external edit", updatedBook.name);
+
+ await outsideEvent("deleteAddressBook", newBookPrefId);
+ await checkEvents(["addressBooks", "onDeleted", bookId]);
+
+ let [parentId1, contactId] = await outsideEvent("createContact");
+ let [newContact] = await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: parentId1, id: contactId },
+ ]);
+ browser.test.assertEq("external", newContact.properties.FirstName);
+ browser.test.assertEq("add", newContact.properties.LastName);
+ browser.test.assertTrue(
+ newContact.properties.vCard.includes("VERSION:4.0"),
+ "vCard should be version 4.0"
+ );
+
+ // Update the contact from outside.
+ await outsideEvent("updateContact", contactId);
+ let [updatedContact] = await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: parentId1, id: contactId },
+ { LastName: { oldValue: "add", newValue: "edit" } },
+ ]);
+ browser.test.assertEq("external", updatedContact.properties.FirstName);
+ browser.test.assertEq("edit", updatedContact.properties.LastName);
+
+ let [parentId2, listId] = await outsideEvent("createMailingList");
+ let [newList] = await checkEvents([
+ "mailingLists",
+ "onCreated",
+ { type: "mailingList", parentId: parentId2, id: listId },
+ ]);
+ browser.test.assertEq("external add", newList.name);
+
+ await outsideEvent("updateMailingList", listId);
+ let [updatedList] = await checkEvents([
+ "mailingLists",
+ "onUpdated",
+ { type: "mailingList", parentId: parentId2, id: listId },
+ ]);
+ browser.test.assertEq("external edit", updatedList.name);
+
+ await outsideEvent("addMailingListMember", listId, contactId);
+ await checkEvents([
+ "mailingLists",
+ "onMemberAdded",
+ { type: "contact", parentId: listId, id: contactId },
+ ]);
+ let listMembers = await browser.mailingLists.listMembers(listId);
+ browser.test.assertEq(1, listMembers.length);
+
+ await outsideEvent("removeMailingListMember", listId, contactId);
+ await checkEvents(["mailingLists", "onMemberRemoved", listId, contactId]);
+
+ await outsideEvent("deleteMailingList", listId);
+ await checkEvents(["mailingLists", "onDeleted", parentId2, listId]);
+
+ await outsideEvent("deleteContact", contactId);
+ await checkEvents(["contacts", "onDeleted", parentId1, contactId]);
+
+ browser.test.log("Completed outsideEventsTest");
+ }
+
+ await addressBookTest();
+ await contactsTest();
+ await mailingListsTest();
+ await contactRemovalTest();
+ await outsideEventsTest();
+
+ 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"],
+ },
+ });
+
+ let parent = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+ function findContact(id) {
+ for (let child of parent.childCards) {
+ if (child.UID == id) {
+ return child;
+ }
+ }
+ return null;
+ }
+ function findMailingList(id) {
+ for (let list of parent.childNodes) {
+ if (list.UID == id) {
+ return list;
+ }
+ }
+ return null;
+ }
+
+ extension.onMessage("outsideEventsTest", async (action, ...args) => {
+ switch (action) {
+ case "createAddressBook": {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "external add",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+ extension.sendMessage(book.UID, dirPrefId);
+ return;
+ }
+ case "updateAddressBook": {
+ let book = MailServices.ab.getDirectoryFromId(args[0]);
+ book.dirName = "external edit";
+ extension.sendMessage();
+ return;
+ }
+ case "deleteAddressBook": {
+ let book = MailServices.ab.getDirectoryFromId(args[0]);
+ MailServices.ab.deleteAddressBook(book.URI);
+ extension.sendMessage();
+ return;
+ }
+ case "createContact": {
+ let contact = new AddrBookCard();
+ contact.firstName = "external";
+ contact.lastName = "add";
+ contact.primaryEmail = "test@invalid";
+
+ let newContact = parent.addCard(contact);
+ extension.sendMessage(parent.UID, newContact.UID);
+ return;
+ }
+ case "updateContact": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ contact.firstName = "external";
+ contact.lastName = "edit";
+ parent.modifyCard(contact);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "deleteContact": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ parent.deleteCards([contact]);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "createMailingList": {
+ let list = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+ list.isMailList = true;
+ list.dirName = "external add";
+
+ let newList = parent.addMailList(list);
+ extension.sendMessage(parent.UID, newList.UID);
+ return;
+ }
+ case "updateMailingList": {
+ let list = findMailingList(args[0]);
+ if (list) {
+ list.dirName = "external edit";
+ list.editMailListToDatabase(null);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "deleteMailingList": {
+ let list = findMailingList(args[0]);
+ if (list) {
+ parent.deleteDirectory(list);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "addMailingListMember": {
+ let list = findMailingList(args[0]);
+ let contact = findContact(args[1]);
+
+ if (list && contact) {
+ list.addCard(contact);
+ equal(1, list.childCards.length);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "removeMailingListMember": {
+ let list = findMailingList(args[0]);
+ let contact = findContact(args[1]);
+
+ if (list && contact) {
+ list.deleteCards([contact]);
+ equal(0, list.childCards.length);
+ ok(findContact(args[1]), "Contact was not removed");
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ }
+ throw new Error(
+ `Message "${action}" passed to handler didn't do anything.`
+ );
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
+
+add_task(async function test_addressBooks_MV3_event_pages() {
+ await AddonTestUtils.promiseStartupManager();
+
+ 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;
+
+ // Create and register event listener.
+ for (let event of [
+ "addressBooks.onCreated",
+ "addressBooks.onUpdated",
+ "addressBooks.onDeleted",
+ "contacts.onCreated",
+ "contacts.onUpdated",
+ "contacts.onDeleted",
+ "mailingLists.onCreated",
+ "mailingLists.onUpdated",
+ "mailingLists.onDeleted",
+ "mailingLists.onMemberAdded",
+ "mailingLists.onMemberRemoved",
+ ]) {
+ let [apiName, eventName] = event.split(".");
+ browser[apiName][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(`${apiName}.${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: ["addressBooks"],
+ browser_specific_settings: { gecko: { id: "addressbook@xpcshell.test" } },
+ },
+ });
+
+ let parent = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+ function findContact(id) {
+ for (let child of parent.childCards) {
+ if (child.UID == id) {
+ return child;
+ }
+ }
+ return null;
+ }
+ function findMailingList(id) {
+ for (let list of parent.childNodes) {
+ if (list.UID == id) {
+ return list;
+ }
+ }
+ return null;
+ }
+ function outsideEvent(action, ...args) {
+ switch (action) {
+ case "createAddressBook": {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "external add",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+ return [book, dirPrefId];
+ }
+ case "updateAddressBook": {
+ let book = MailServices.ab.getDirectoryFromId(args[0]);
+ book.dirName = "external edit";
+ return [];
+ }
+ case "deleteAddressBook": {
+ let book = MailServices.ab.getDirectoryFromId(args[0]);
+ MailServices.ab.deleteAddressBook(book.URI);
+ return [];
+ }
+ case "createContact": {
+ let contact = new AddrBookCard();
+ contact.firstName = "external";
+ contact.lastName = "add";
+ contact.primaryEmail = "test@invalid";
+
+ let newContact = parent.addCard(contact);
+ return [parent.UID, newContact.UID];
+ }
+ case "updateContact": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ contact.firstName = "external";
+ contact.lastName = "edit";
+ parent.modifyCard(contact);
+ return [];
+ }
+ break;
+ }
+ case "deleteContact": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ parent.deleteCards([contact]);
+ return [];
+ }
+ break;
+ }
+ case "createMailingList": {
+ let list = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+ list.isMailList = true;
+ list.dirName = "external add";
+
+ let newList = parent.addMailList(list);
+ return [parent.UID, newList.UID];
+ }
+ case "updateMailingList": {
+ let list = findMailingList(args[0]);
+ if (list) {
+ list.dirName = "external edit";
+ list.editMailListToDatabase(null);
+ return [];
+ }
+ break;
+ }
+ case "deleteMailingList": {
+ let list = findMailingList(args[0]);
+ if (list) {
+ parent.deleteDirectory(list);
+ return [];
+ }
+ break;
+ }
+ case "addMailingListMember": {
+ let list = findMailingList(args[0]);
+ let contact = findContact(args[1]);
+
+ if (list && contact) {
+ list.addCard(contact);
+ equal(1, list.childCards.length);
+ return [];
+ }
+ break;
+ }
+ case "removeMailingListMember": {
+ let list = findMailingList(args[0]);
+ let contact = findContact(args[1]);
+
+ if (list && contact) {
+ list.deleteCards([contact]);
+ equal(0, list.childCards.length);
+ ok(findContact(args[1]), "Contact was not removed");
+ return [];
+ }
+ break;
+ }
+ }
+ throw new Error(
+ `Message "${action}" passed to handler didn't do anything.`
+ );
+ }
+
+ 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 = [
+ "addressBook.onAddressBookCreated",
+ "addressBook.onAddressBookUpdated",
+ "addressBook.onAddressBookDeleted",
+ "addressBook.onContactCreated",
+ "addressBook.onContactUpdated",
+ "addressBook.onContactDeleted",
+ "addressBook.onMailingListCreated",
+ "addressBook.onMailingListUpdated",
+ "addressBook.onMailingListDeleted",
+ "addressBook.onMemberAdded",
+ "addressBook.onMemberRemoved",
+ ];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ checkPersistentListeners({ primed: false });
+
+ // addressBooks.onCreated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ let [newBook, dirPrefId] = outsideEvent("createAddressBook");
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [
+ {
+ id: newBook.UID,
+ type: "addressBook",
+ name: "external add",
+ readOnly: false,
+ remote: false,
+ },
+ ],
+ await extension.awaitMessage("addressBooks.onCreated received"),
+ "The primed addressBooks.onCreated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // addressBooks.onUpdated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("updateAddressBook", dirPrefId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [
+ {
+ id: newBook.UID,
+ type: "addressBook",
+ name: "external edit",
+ readOnly: false,
+ remote: false,
+ },
+ ],
+ await extension.awaitMessage("addressBooks.onUpdated received"),
+ "The primed addressBooks.onUpdated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // addressBooks.onDeleted.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("deleteAddressBook", dirPrefId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [newBook.UID],
+ await extension.awaitMessage("addressBooks.onDeleted received"),
+ "The primed addressBooks.onDeleted event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // contacts.onCreated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ let [parentId1, contactId] = outsideEvent("createContact");
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ let [createdNode] = await extension.awaitMessage(
+ "contacts.onCreated received"
+ );
+ Assert.deepEqual(
+ {
+ type: "contact",
+ parentId: parentId1,
+ id: contactId,
+ },
+ {
+ type: createdNode.type,
+ parentId: createdNode.parentId,
+ id: createdNode.id,
+ },
+ "The primed contacts.onCreated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // contacts.onUpdated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("updateContact", contactId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ let [updatedNode, changedProperties] = await extension.awaitMessage(
+ "contacts.onUpdated received"
+ );
+ Assert.deepEqual(
+ [
+ { type: "contact", parentId: parentId1, id: contactId },
+ { LastName: { oldValue: "add", newValue: "edit" } },
+ ],
+ [
+ {
+ type: updatedNode.type,
+ parentId: updatedNode.parentId,
+ id: updatedNode.id,
+ },
+ changedProperties,
+ ],
+ "The primed contacts.onUpdated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // mailingLists.onCreated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ let [parentId2, listId] = outsideEvent("createMailingList");
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [
+ {
+ type: "mailingList",
+ parentId: parentId2,
+ id: listId,
+ name: "external add",
+ nickName: "",
+ description: "",
+ readOnly: false,
+ remote: false,
+ },
+ ],
+ await extension.awaitMessage("mailingLists.onCreated received"),
+ "The primed mailingLists.onCreated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // mailingList.onUpdated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("updateMailingList", listId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [
+ {
+ type: "mailingList",
+ parentId: parentId2,
+ id: listId,
+ name: "external edit",
+ nickName: "",
+ description: "",
+ readOnly: false,
+ remote: false,
+ },
+ ],
+ await extension.awaitMessage("mailingLists.onUpdated received"),
+ "The primed mailingLists.onUpdated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // mailingList.onMemberAdded.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("addMailingListMember", listId, contactId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ let [addedNode] = await extension.awaitMessage(
+ "mailingLists.onMemberAdded received"
+ );
+ Assert.deepEqual(
+ { type: "contact", parentId: listId, id: contactId },
+ { type: addedNode.type, parentId: addedNode.parentId, id: addedNode.id },
+ "The primed mailingLists.onMemberAdded event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // mailingList.onMemberRemoved.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("removeMailingListMember", listId, contactId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [listId, contactId],
+ await extension.awaitMessage("mailingLists.onMemberRemoved received"),
+ "The primed mailingLists.onMemberRemoved event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // mailingList.onDeleted.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("deleteMailingList", listId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [parentId2, listId],
+ await extension.awaitMessage("mailingLists.onDeleted received"),
+ "The primed mailingLists.onDeleted event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // contacts.onDeleted.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("deleteContact", contactId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [parentId1, contactId],
+ await extension.awaitMessage("contacts.onDeleted received"),
+ "The primed contacts.onDeleted event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+
+ await AddonTestUtils.promiseShutdownManager();
+});
+
+add_task(async function test_photos() {
+ async function background() {
+ let events = [];
+ let eventPromise;
+ let eventPromiseResolve;
+ for (let eventNamespace of ["addressBooks", "contacts"]) {
+ for (let eventName of ["onCreated", "onUpdated", "onDeleted"]) {
+ if (eventName in browser[eventNamespace]) {
+ browser[eventNamespace][eventName].addListener((...args) => {
+ events.push({ namespace: eventNamespace, name: eventName, args });
+ if (eventPromiseResolve) {
+ let resolve = eventPromiseResolve;
+ eventPromiseResolve = null;
+ resolve();
+ }
+ });
+ }
+ }
+ }
+
+ let getDataUrl = function (file) {
+ return new Promise((resolve, reject) => {
+ var reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = function () {
+ resolve(reader.result);
+ };
+ reader.onerror = function (error) {
+ reject(new Error(error));
+ };
+ });
+ };
+
+ let updateAndVerifyPhoto = async function (
+ parentId,
+ id,
+ photoFile,
+ photoData
+ ) {
+ eventPromise = new Promise(resolve => {
+ eventPromiseResolve = resolve;
+ });
+ await browser.contacts.setPhoto(id, photoFile);
+
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId, id },
+ {},
+ ]);
+ let updatedPhoto = await browser.contacts.getPhoto(id);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(updatedPhoto instanceof File);
+ browser.test.assertEq("image/png", updatedPhoto.type);
+ browser.test.assertEq(`${id}.png`, updatedPhoto.name);
+ browser.test.assertEq(photoData, await getDataUrl(updatedPhoto));
+ };
+ let normalizeVCard = function (vCard) {
+ return vCard
+ .replaceAll("\r\n", "")
+ .replaceAll("\n", "")
+ .replaceAll(" ", "");
+ };
+ let outsideEvent = function (action, ...args) {
+ eventPromise = new Promise(resolve => {
+ eventPromiseResolve = resolve;
+ });
+ return window.sendMessage("outsideEventsTest", action, ...args);
+ };
+ let checkEvents = async function (...expectedEvents) {
+ if (eventPromiseResolve) {
+ await eventPromise;
+ }
+
+ browser.test.assertEq(
+ expectedEvents.length,
+ events.length,
+ "Correct number of events"
+ );
+
+ if (expectedEvents.length != events.length) {
+ for (let event of events) {
+ let args = event.args.join(", ");
+ browser.test.log(`${event.namespace}.${event.name}(${args})`);
+ }
+ throw new Error("Wrong number of events, stopping.");
+ }
+
+ for (let [namespace, name, ...expectedArgs] of expectedEvents) {
+ let event = events.shift();
+ browser.test.assertEq(
+ namespace,
+ event.namespace,
+ "Event namespace is correct"
+ );
+ browser.test.assertEq(name, event.name, "Event type is correct");
+ browser.test.assertEq(
+ expectedArgs.length,
+ event.args.length,
+ "Argument count is correct"
+ );
+ window.assertDeepEqual(expectedArgs, event.args);
+ if (expectedEvents.length == 1) {
+ return event.args;
+ }
+ }
+
+ return null;
+ };
+
+ let whitePixelData =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC";
+ let bluePixelData =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg==";
+ let greenPixelData =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY5CeYAMAAbEA6ASxSWcAAAAASUVORK5CYII=";
+ let redPixelData =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY3growIAAycBLhVrvukAAAAASUVORK5CYII=";
+ let vCard3WhitePixel =
+ "PHOTO;ENCODING=B;TYPE=PNG:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC";
+ let vCard4WhitePixel =
+ "PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC";
+ let vCard4BluePixel =
+ "PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg==";
+
+ // Create a photo file, which is linked to a local file to simulate a file
+ // opened through a filepicker.
+ let [redPixelRealFile] = await window.sendMessage("getRedPixelFile");
+
+ // Create a photo file, which is a simple data blob.
+ let greenPixelFile = await fetch(greenPixelData)
+ .then(res => res.arrayBuffer())
+ .then(buf => new File([buf], "greenPixel.png", { type: "image/png" }));
+
+ // -------------------------------------------------------------------------
+ // Test vCard v4 with a photoName set.
+ // -------------------------------------------------------------------------
+
+ let [parentId1, contactId1, photoName1] = await outsideEvent(
+ "createV4ContactWithPhotoName"
+ );
+ let [newContact] = await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: parentId1, id: contactId1 },
+ ]);
+ browser.test.assertEq("external", newContact.properties.FirstName);
+ browser.test.assertEq("add", newContact.properties.LastName);
+ browser.test.assertTrue(
+ newContact.properties.vCard.includes("VERSION:4.0"),
+ "vCard should be version 4.0"
+ );
+ browser.test.assertTrue(
+ normalizeVCard(newContact.properties.vCard).includes(vCard4WhitePixel),
+ `vCard should include the correct Photo property [${normalizeVCard(
+ newContact.properties.vCard
+ )}] vs [${vCard4WhitePixel}]`
+ );
+ // Check internal photoUrl is the correct fileUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId1,
+ `^file:.*?${photoName1}$`
+ );
+
+ // Test if we can get the photo through the API.
+
+ let photo = await browser.contacts.getPhoto(contactId1);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(photo instanceof File);
+ browser.test.assertEq("image/png", photo.type);
+ browser.test.assertEq(`${contactId1}.png`, photo.name);
+ browser.test.assertEq(
+ whitePixelData,
+ await getDataUrl(photo),
+ "vCard 4.0 contact with photo from internal fileUrl from photoName should return the correct photo file"
+ );
+ // Re-check internal photoUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId1,
+ `^file:.*?${photoName1}$`
+ );
+
+ // Test if we can update the photo through the API by providing a file which
+ // is linked to a local file. Since this vCard had only a photoName set and
+ // its photo stored as a local file, the updated photo should also be stored
+ // as a local file.
+
+ await updateAndVerifyPhoto(
+ parentId1,
+ contactId1,
+ redPixelRealFile,
+ redPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId1,
+ `^file:.*?${contactId1}\.png$`
+ );
+
+ // Test if we can update the photo through the API, by providing a pure data
+ // blob (decoupled from a local file, without file.mozFullPath set).
+
+ await updateAndVerifyPhoto(
+ parentId1,
+ contactId1,
+ greenPixelFile,
+ greenPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId1,
+ `^file:.*?${contactId1}-1\.png$`
+ );
+
+ // Test if we get the correct photo if it is updated by the user, storing the
+ // photo in its vCard (outside of the API).
+
+ await outsideEvent("updateV4ContactWithBluePixel", contactId1);
+ let [updatedContact1] = await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: parentId1, id: contactId1 },
+ { LastName: { oldValue: "add", newValue: "edit" } },
+ ]);
+ browser.test.assertEq("external", updatedContact1.properties.FirstName);
+ browser.test.assertEq("edit", updatedContact1.properties.LastName);
+ let updatedPhoto1 = await browser.contacts.getPhoto(contactId1);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(updatedPhoto1 instanceof File);
+ browser.test.assertEq("image/png", updatedPhoto1.type);
+ browser.test.assertEq(`${contactId1}.png`, updatedPhoto1.name);
+ browser.test.assertEq(bluePixelData, await getDataUrl(updatedPhoto1));
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId1,
+ bluePixelData
+ );
+
+ // -------------------------------------------------------------------------
+ // Test vCard v4 with a photoName and also a photo in its vCard.
+ // -------------------------------------------------------------------------
+
+ let [parentId2, contactId2] = await outsideEvent(
+ "createV4ContactWithBothPhotoProps"
+ );
+ let [newContact2] = await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: parentId2, id: contactId2 },
+ ]);
+ browser.test.assertEq("external", newContact2.properties.FirstName);
+ browser.test.assertEq("add", newContact2.properties.LastName);
+ browser.test.assertTrue(
+ newContact2.properties.vCard.includes("VERSION:4.0"),
+ "vCard should be version 4.0"
+ );
+ // The card should not include vCard4WhitePixel (which photoName points to),
+ // but the value of vCard4BluePixel stored in the vCard photo property.
+ browser.test.assertTrue(
+ normalizeVCard(newContact2.properties.vCard).includes(vCard4BluePixel),
+ `vCard should include the correct Photo property [${normalizeVCard(
+ newContact2.properties.vCard
+ )}] vs [${vCard4BluePixel}]`
+ );
+ // Check internal photoUrl is the correct dataUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId2,
+ bluePixelData
+ );
+
+ // Test if we can get the correct photo through the API.
+
+ let photo3 = await browser.contacts.getPhoto(contactId2);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(photo3 instanceof File);
+ browser.test.assertEq("image/png", photo3.type);
+ browser.test.assertEq(`${contactId2}.png`, photo3.name);
+ browser.test.assertEq(
+ bluePixelData,
+ await getDataUrl(photo3),
+ "vCard 4.0 contact with photo from internal dataUrl from vCard (vCard wins over photoName) should return the correct photo file"
+ );
+ // Re-check internal photoUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId2,
+ bluePixelData
+ );
+
+ // Test if we can update the photo through the API by providing a file which
+ // is linked to a local file. Since this vCard had its photo stored as dataUrl
+ // in the vCard, the updated photo should be stored as a dataUrl as well.
+
+ await updateAndVerifyPhoto(
+ parentId2,
+ contactId2,
+ redPixelRealFile,
+ redPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId2,
+ redPixelData
+ );
+
+ // Test if we can update the photo through the API, by providing a pure data
+ // blob (decoupled from a local file, without file.mozFullPath set).
+
+ await updateAndVerifyPhoto(
+ parentId2,
+ contactId2,
+ greenPixelFile,
+ greenPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId2,
+ greenPixelData
+ );
+
+ // -------------------------------------------------------------------------
+ // Test vCard v3 with a photoName set.
+ // -------------------------------------------------------------------------
+
+ let [parentId3, contactId3, photoName4] = await outsideEvent(
+ "createV3ContactWithPhotoName"
+ );
+ let [newContact4] = await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: parentId3, id: contactId3 },
+ ]);
+ browser.test.assertEq("external", newContact4.properties.FirstName);
+ browser.test.assertEq("add", newContact4.properties.LastName);
+ browser.test.assertTrue(
+ newContact4.properties.vCard.includes("VERSION:3.0"),
+ "vCard should be version 3.0"
+ );
+ browser.test.assertTrue(
+ normalizeVCard(newContact4.properties.vCard).includes(vCard3WhitePixel),
+ `vCard should include the correct Photo property [${normalizeVCard(
+ newContact4.properties.vCard
+ )}] vs [${vCard3WhitePixel}]`
+ );
+ // Check internal photoUrl is the correct fileUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId3,
+ `^file:.*?${photoName4}$`
+ );
+ let photo4 = await browser.contacts.getPhoto(contactId3);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(photo4 instanceof File);
+ browser.test.assertEq("image/png", photo4.type);
+ browser.test.assertEq(`${contactId3}.png`, photo4.name);
+ browser.test.assertEq(
+ whitePixelData,
+ await getDataUrl(photo4),
+ "vCard 3.0 contact with photo from internal fileUrl from photoName should return the correct photo file"
+ );
+ // Re-check internal photoUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId3,
+ `^file:.*?${photoName4}$`
+ );
+
+ // Test if we can update the photo through the API by providing a file which
+ // is linked to a local file. Since this vCard had only a photoName set and
+ // its photo stored as a local file, the updated photo should also be stored
+ // as a local file.
+
+ await updateAndVerifyPhoto(
+ parentId3,
+ contactId3,
+ redPixelRealFile,
+ redPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId3,
+ `^file:.*?${contactId3}\.png$`
+ );
+
+ // Test if we can update the photo through the API, by providing a pure data
+ // blob (decoupled from a local file, without file.mozFullPath set).
+
+ await updateAndVerifyPhoto(
+ parentId3,
+ contactId3,
+ greenPixelFile,
+ greenPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId3,
+ `^file:.*?${contactId3}-1\.png$`
+ );
+
+ // Test if we get the correct photo if it is updated by the user, storing the
+ // photo in its vCard (outside of the API).
+
+ await outsideEvent("updateV3ContactWithBluePixel", contactId3);
+ let [updatedContact3] = await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: parentId3, id: contactId3 },
+ { LastName: { oldValue: "add", newValue: "edit" } },
+ ]);
+ browser.test.assertEq("external", updatedContact3.properties.FirstName);
+ browser.test.assertEq("edit", updatedContact3.properties.LastName);
+ let updatedPhoto3 = await browser.contacts.getPhoto(contactId3);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(updatedPhoto3 instanceof File);
+ browser.test.assertEq("image/png", updatedPhoto3.type);
+ browser.test.assertEq(`${contactId3}.png`, updatedPhoto3.name);
+ browser.test.assertEq(bluePixelData, await getDataUrl(updatedPhoto3));
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId3,
+ bluePixelData
+ );
+
+ // Cleanup. Delete all created contacts.
+
+ await outsideEvent("deleteContact", contactId1);
+ await checkEvents(["contacts", "onDeleted", parentId1, contactId1]);
+ await outsideEvent("deleteContact", contactId2);
+ await checkEvents(["contacts", "onDeleted", parentId2, contactId2]);
+ await outsideEvent("deleteContact", contactId3);
+ await checkEvents(["contacts", "onDeleted", parentId3, contactId3]);
+ browser.test.notifyPass("addressBooksPhotos");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks"],
+ },
+ });
+
+ let parent = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+ function findContact(id) {
+ for (let child of parent.childCards) {
+ if (child.UID == id) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ async function getUniqueWhitePixelFile() {
+ // Copy photo file into the required Photos subfolder of the profile folder.
+ let photoName = `${AddrBookUtils.newUID()}.png`;
+ await IOUtils.copy(
+ do_get_file("images/whitePixel.png").path,
+ PathUtils.join(PathUtils.profileDir, "Photos", photoName)
+ );
+ return photoName;
+ }
+
+ extension.onMessage("getRedPixelFile", async () => {
+ let redPixelFile = await File.createFromNsIFile(
+ do_get_file("images/redPixel.png")
+ );
+ extension.sendMessage(redPixelFile);
+ });
+
+ extension.onMessage("verifyInternalPhotoUrl", (id, expected) => {
+ let contact = findContact(id);
+ let photoUrl = contact.photoURL;
+ if (expected.startsWith("data:")) {
+ Assert.equal(expected, photoUrl, `photoURL should be correct`);
+ } else {
+ let regExp = new RegExp(expected);
+ Assert.ok(
+ regExp.test(photoUrl),
+ `photoURL <${photoUrl}> should match expected regExp <${expected}>`
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("outsideEventsTest", async (action, ...args) => {
+ switch (action) {
+ case "createV4ContactWithPhotoName": {
+ let photoName = await getUniqueWhitePixelFile();
+ let contact = new AddrBookCard();
+ contact.firstName = "external";
+ contact.lastName = "add";
+ contact.primaryEmail = "test@invalid";
+ contact.setProperty("PhotoName", photoName);
+
+ let newContact = parent.addCard(contact);
+ extension.sendMessage(parent.UID, newContact.UID, photoName);
+ return;
+ }
+ case "createV4ContactWithBothPhotoProps": {
+ // This contact has whitePixel as file but bluePixel in the vCard.
+ let photoName = await getUniqueWhitePixelFile();
+ let contact = new AddrBookCard();
+ contact.setProperty("PhotoName", photoName);
+ contact.setProperty(
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:test@invalid
+ N:add;external;;;
+ UID:fd9aecf9-2453-4ba1-bec6-574a15bb380b
+ PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAA
+ ACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg==
+ END:VCARD
+ `
+ );
+ let newContact = parent.addCard(contact);
+ extension.sendMessage(parent.UID, newContact.UID, photoName);
+ return;
+ }
+ case "updateV4ContactWithBluePixel": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ contact.setProperty(
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:test@invalid
+ N:edit;external;;;
+ PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAA
+ ACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg==
+ END:VCARD
+ `
+ );
+ parent.modifyCard(contact);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "createV3ContactWithPhotoName": {
+ let photoName = await getUniqueWhitePixelFile();
+ let contact = new AddrBookCard();
+ contact.setProperty("PhotoName", photoName);
+ contact.setProperty(
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ EMAIL:test@invalid
+ N:add;external
+ UID:fd9aecf9-2453-4ba1-bec6-574a15bb380c
+ END:VCARD
+ `
+ );
+ let newContact = parent.addCard(contact);
+ extension.sendMessage(parent.UID, newContact.UID, photoName);
+ return;
+ }
+ case "updateV3ContactWithBluePixel": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ contact.setProperty(
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ EMAIL:test@invalid
+ N:edit;external
+ PHOTO;ENCODING=b;TYPE=PNG:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAD
+ ElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg==
+ END:VCARD
+ `
+ );
+ parent.modifyCard(contact);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "deleteContact": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ parent.deleteCards([contact]);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ }
+ throw new Error(
+ `Message "${action}" passed to handler didn't do anything.`
+ );
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("addressBooksPhotos");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_provider.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_provider.js
new file mode 100644
index 0000000000..a09540dcbe
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_provider.js
@@ -0,0 +1,139 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let id = "9b9074ff-8fa4-4c58-9c3b-bc9ea2e17db1";
+ let dummy = async (node, searchString, query) => {
+ await browser.test.assertTrue(
+ false,
+ "Should have removed this address book"
+ );
+ };
+ await browser.addressBooks.provider.onSearchRequest.addListener(dummy, {
+ addressBookName: "dummy",
+ isSecure: false,
+ id,
+ });
+ await browser.addressBooks.provider.onSearchRequest.removeListener(dummy);
+ id = "00e1d9af-a846-4ef5-a6ac-15e8926bf6d3";
+ await browser.addressBooks.provider.onSearchRequest.addListener(
+ async (node, searchString, query) => {
+ await browser.test.assertEq(
+ id,
+ node.id,
+ "Addressbook should have the id we requested"
+ );
+ return {
+ results: [
+ {
+ DisplayName: searchString,
+ PrimaryEmail: searchString + "@example.com",
+ },
+ ],
+ isCompleteResult: true,
+ };
+ },
+ {
+ addressBookName: "xpcshell",
+ isSecure: false,
+ id,
+ }
+ );
+ await browser.addressBooks.provider.onSearchRequest.addListener(
+ async (node, searchString, query) => {
+ await browser.test.assertTrue(
+ false,
+ "Should not have created a duplicate address book"
+ );
+ },
+ {
+ addressBookName: "xpcshell",
+ isSecure: false,
+ id,
+ }
+ );
+ },
+ manifest: { permissions: ["addressBooks"] },
+ });
+
+ await extension.startup();
+
+ const dummyUID = "9b9074ff-8fa4-4c58-9c3b-bc9ea2e17db1";
+ let searchBook = MailServices.ab.getDirectoryFromUID(dummyUID);
+ ok(searchBook == null, "Dummy directory was removed by extension");
+
+ const UID = "00e1d9af-a846-4ef5-a6ac-15e8926bf6d3";
+ searchBook = MailServices.ab.getDirectoryFromUID(UID);
+ ok(searchBook != null, "Extension registered an async directory");
+
+ let foundCards = 0;
+ await new Promise(resolve => {
+ searchBook.search(null, "test", {
+ onSearchFoundCard(card) {
+ ok(card != null, "A card was found.");
+ equal(card.directoryUID, UID, "The card comes from the directory.");
+ equal(
+ card.primaryEmail,
+ "test@example.com",
+ "The card has the correct email address."
+ );
+ equal(
+ card.displayName,
+ "test",
+ "The card has the correct display name."
+ );
+ foundCards++;
+ },
+ onSearchFinished(status, isCompleteResult) {
+ ok(Components.isSuccessCode(status), "Search finished successfully.");
+ equal(foundCards, 1, "One card was found.");
+ ok(isCompleteResult, "A full result set was received.");
+ resolve();
+ },
+ });
+ });
+
+ let autoCompleteSearch = Cc[
+ "@mozilla.org/autocomplete/search;1?name=addrbook"
+ ].createInstance(Ci.nsIAutoCompleteSearch);
+ await new Promise(resolve => {
+ autoCompleteSearch.startSearch("test", null, null, {
+ onSearchResult(aSearch, aResult) {
+ equal(aSearch, autoCompleteSearch, "This is our search.");
+ if (aResult.searchResult == Ci.nsIAutoCompleteResult.RESULT_SUCCESS) {
+ equal(aResult.matchCount, 1, "One match was found.");
+ equal(
+ aResult.getValueAt(0),
+ "test <test@example.com>",
+ "The match had the expected value."
+ );
+ resolve();
+ } else {
+ equal(
+ aResult.searchResult,
+ Ci.nsIAutoCompleteResult.RESULT_NOMATCH_ONGOING,
+ "We should be waiting for the extension's results."
+ );
+ }
+ },
+ });
+ });
+
+ await extension.unload();
+ searchBook = MailServices.ab.getDirectoryFromUID(UID);
+ ok(searchBook == null, "Extension directory removed after unload");
+});
+
+registerCleanupFunction(() => {
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_quickSearch.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_quickSearch.js
new file mode 100644
index 0000000000..9a6bbd8f4e
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_quickSearch.js
@@ -0,0 +1,238 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { LDAPServer } = ChromeUtils.import(
+ "resource://testing-common/LDAPServer.jsm"
+);
+
+add_setup(async () => {
+ Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
+
+ registerCleanupFunction(() => {
+ LDAPServer.close();
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+ });
+});
+
+add_task(async function test_quickSearch() {
+ async function background() {
+ let book1 = await browser.addressBooks.create({ name: "book1" });
+ let book2 = await browser.addressBooks.create({ name: "book2" });
+
+ let book1contacts = {
+ charlie: await browser.contacts.create(book1, { FirstName: "charlie" }),
+ juliet: await browser.contacts.create(book1, { FirstName: "juliet" }),
+ mike: await browser.contacts.create(book1, { FirstName: "mike" }),
+ oscar: await browser.contacts.create(book1, { FirstName: "oscar" }),
+ papa: await browser.contacts.create(book1, { FirstName: "papa" }),
+ romeo: await browser.contacts.create(book1, { FirstName: "romeo" }),
+ victor: await browser.contacts.create(book1, { FirstName: "victor" }),
+ };
+
+ let book2contacts = {
+ bigBird: await browser.contacts.create(book2, {
+ FirstName: "Big",
+ LastName: "Bird",
+ }),
+ cookieMonster: await browser.contacts.create(book2, {
+ FirstName: "Cookie",
+ LastName: "Monster",
+ }),
+ elmo: await browser.contacts.create(book2, { FirstName: "Elmo" }),
+ grover: await browser.contacts.create(book2, { FirstName: "Grover" }),
+ oscarTheGrouch: await browser.contacts.create(book2, {
+ FirstName: "Oscar",
+ LastName: "The Grouch",
+ }),
+ };
+
+ // A search string without a match in either book.
+ let results = await browser.contacts.quickSearch(book1, "snuffleupagus");
+ browser.test.assertEq(0, results.length);
+
+ // A search string with a match in the book we're searching.
+ results = await browser.contacts.quickSearch(book1, "mike");
+ browser.test.assertEq(1, results.length);
+ browser.test.assertEq(book1contacts.mike, results[0].id);
+
+ // A search string passed via queryInfo
+ results = await browser.contacts.quickSearch(book1, {
+ searchString: "mike",
+ });
+ browser.test.assertEq(1, results.length);
+ browser.test.assertEq(book1contacts.mike, results[0].id);
+
+ // A search string with a match in the book we're not searching.
+ results = await browser.contacts.quickSearch(book1, "elmo");
+ browser.test.assertEq(0, results.length);
+
+ // A search string with a match in both books.
+ results = await browser.contacts.quickSearch(book1, "oscar");
+ browser.test.assertEq(1, results.length);
+ browser.test.assertEq(book1contacts.oscar, results[0].id);
+
+ // A search string with a match in both books. Looking in all books.
+ results = await browser.contacts.quickSearch("oscar");
+ browser.test.assertEq(2, results.length);
+ browser.test.assertEq(book1contacts.oscar, results[0].id);
+ browser.test.assertEq(book2contacts.oscarTheGrouch, results[1].id);
+
+ // No valid search strings.
+ results = await browser.contacts.quickSearch(" ");
+ browser.test.assertEq(0, results.length);
+
+ await browser.addressBooks.delete(book1);
+ await browser.addressBooks.delete(book2);
+
+ browser.test.notifyPass("addressBooks");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: { permissions: ["addressBooks"] },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
+
+add_task(async function test_quickSearch_types() {
+ // If nsIAbLDAPDirectory doesn't exist in our build options, someone has
+ // specified --disable-ldap
+ if (!("nsIAbLDAPDirectory" in Ci)) {
+ return;
+ }
+
+ // Add a card to the personal AB.
+ let personaAB = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+
+ let contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact.UID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+ contact.displayName = "personal contact";
+ contact.firstName = "personal";
+ contact.lastName = "contact";
+ contact.primaryEmail = "personal@invalid";
+ contact = personaAB.addCard(contact);
+
+ // Set up the history AB as read-only.
+ let historyAB = MailServices.ab.getDirectory("jsaddrbook://history.sqlite");
+
+ contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact.UID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxy";
+ contact.displayName = "history contact";
+ contact.firstName = "history";
+ contact.lastName = "contact";
+ contact.primaryEmail = "history@invalid";
+ contact = historyAB.addCard(contact);
+
+ historyAB.setBoolValue("readOnly", true);
+
+ Assert.ok(historyAB.readOnly);
+
+ // Set up an LDAP address book.
+ LDAPServer.open();
+
+ // Create an LDAP directory
+ MailServices.ab.newAddressBook(
+ "test",
+ `ldap://localhost:${LDAPServer.port}/people??sub?(objectclass=*)`,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ async function background() {
+ function checkCards(cards, expectedNames) {
+ browser.test.assertEq(expectedNames.length, cards.length);
+ let expected = new Set(expectedNames);
+ for (let card of cards) {
+ expected.delete(card.properties.FirstName);
+ }
+ browser.test.assertEq(
+ 0,
+ expected.size,
+ "Should have seen all expected cards"
+ );
+ }
+ // No arguments should get cards from all address books.
+ let results = await browser.contacts.quickSearch("contact");
+ checkCards(results, ["personal", "history", "LDAP"]);
+
+ // An empty argument should get cards from all address books.
+ results = await browser.contacts.quickSearch({ searchString: "contact" });
+ checkCards(results, ["personal", "history", "LDAP"]);
+
+ // Skip remote address books.
+ results = await browser.contacts.quickSearch({
+ searchString: "contact",
+ includeRemote: false,
+ });
+ checkCards(results, ["personal", "history"]);
+
+ // Skip local address books.
+ results = await browser.contacts.quickSearch({
+ searchString: "contact",
+ includeLocal: false,
+ });
+ checkCards(results, ["LDAP"]);
+
+ // Skip read-only address books.
+ results = await browser.contacts.quickSearch({
+ searchString: "contact",
+ includeReadOnly: false,
+ });
+ checkCards(results, ["personal"]);
+
+ // Skip read-write address books.
+ results = await browser.contacts.quickSearch({
+ searchString: "contact",
+ includeReadWrite: false,
+ });
+ checkCards(results, ["LDAP", "history"]);
+
+ browser.test.notifyPass("addressBooks");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: { permissions: ["addressBooks"] },
+ });
+
+ let startupPromise = extension.startup();
+
+ // This for loop handles returning responses for LDAP. It should run once
+ // for each test that queries the remote address book.
+ for (let i = 0; i < 4; i++) {
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry({
+ dn: "uid=ldap,dc=contact,dc=invalid",
+ attributes: {
+ objectClass: "person",
+ cn: "LDAP contact",
+ givenName: "LDAP",
+ mail: "eurus@contact.invalid",
+ sn: "contact",
+ },
+ });
+ LDAPServer.writeSearchResultDone();
+ }
+
+ await startupPromise;
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_readonly.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_readonly.js
new file mode 100644
index 0000000000..3b40dc67a2
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_readonly.js
@@ -0,0 +1,148 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_setup(async () => {
+ Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
+
+ registerCleanupFunction(() => {
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+ });
+
+ let historyAB = MailServices.ab.getDirectory("jsaddrbook://history.sqlite");
+
+ let contact1 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact1.UID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+ contact1.displayName = "contact number one";
+ contact1.firstName = "contact";
+ contact1.lastName = "one";
+ contact1.primaryEmail = "contact1@invalid";
+ contact1 = historyAB.addCard(contact1);
+
+ let mailList = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+ mailList.isMailList = true;
+ mailList.dirName = "Mailing";
+ mailList.listNickName = "Mailing";
+ mailList.description = "";
+
+ historyAB.addMailList(mailList);
+ historyAB.setBoolValue("readOnly", true);
+
+ Assert.ok(historyAB.readOnly);
+});
+
+add_task(async function test_addressBooks_readonly() {
+ async function background() {
+ let list = await browser.addressBooks.list();
+
+ // The read only AB should be in the list.
+ let readOnlyAB = list.find(ab => ab.name == "Collected Addresses");
+ browser.test.assertTrue(!!readOnlyAB, "Should have found the address book");
+
+ browser.test.assertTrue(
+ readOnlyAB.readOnly,
+ "Should have marked the address book as read-only"
+ );
+
+ let card = await browser.contacts.get(
+ "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ );
+ browser.test.assertTrue(!!card, "Should have found the card");
+
+ browser.test.assertTrue(
+ card.readOnly,
+ "Should have marked the card as read-only"
+ );
+
+ await browser.test.assertRejects(
+ browser.contacts.create(readOnlyAB.id, {
+ email: "test@example.com",
+ }),
+ "Cannot create a contact in a read-only address book",
+ "Should reject creating an address book card"
+ );
+
+ await browser.test.assertRejects(
+ browser.contacts.update(card.id, card.properties),
+ "Cannot modify a contact in a read-only address book",
+ "Should reject modifying an address book card"
+ );
+
+ await browser.test.assertRejects(
+ browser.contacts.delete(card.id),
+ "Cannot delete a contact in a read-only address book",
+ "Should reject deleting an address book card"
+ );
+
+ // Mailing List
+
+ let mailingLists = await browser.mailingLists.list(readOnlyAB.id);
+ let readOnlyML = mailingLists[0];
+ browser.test.assertTrue(!!readOnlyAB, "Should have found the mailing list");
+
+ browser.test.assertTrue(
+ readOnlyML.readOnly,
+ "Should have marked the mailing list as read-only"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.create(readOnlyAB.id, { name: "Test" }),
+ "Cannot create a mailing list in a read-only address book",
+ "Should reject creating a mailing list"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.update(readOnlyML.id, { name: "newTest" }),
+ "Cannot modify a mailing list in a read-only address book",
+ "Should reject modifying a mailing list"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.delete(readOnlyML.id),
+ "Cannot delete a mailing list in a read-only address book",
+ "Should reject deleting a mailing list"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.addMember(readOnlyML.id, card.id),
+ "Cannot add to a mailing list in a read-only address book",
+ "Should reject deleting a mailing list"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.removeMember(readOnlyML.id, card.id),
+ "Cannot remove from a mailing list in a read-only address book",
+ "Should reject deleting a mailing list"
+ );
+
+ 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"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_remote.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_remote.js
new file mode 100644
index 0000000000..7a34c8ce86
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_remote.js
@@ -0,0 +1,101 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { LDAPServer } = ChromeUtils.import(
+ "resource://testing-common/LDAPServer.jsm"
+);
+
+add_setup(async () => {
+ // If nsIAbLDAPDirectory doesn't exist in our build options, someone has
+ // specified --disable-ldap.
+ if (!("nsIAbLDAPDirectory" in Ci)) {
+ return;
+ }
+ Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
+
+ LDAPServer.open();
+
+ // Create an LDAP directory.
+ MailServices.ab.newAddressBook(
+ "test",
+ `ldap://localhost:${LDAPServer.port}/people??sub?(objectclass=*)`,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ registerCleanupFunction(() => {
+ LDAPServer.close();
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+ });
+});
+
+add_task(async function test_addressBooks_remote() {
+ async function background() {
+ let list = await browser.addressBooks.list();
+
+ // The remote AB should be in the list.
+ let remoteAB = list.find(ab => ab.name == "test");
+ browser.test.assertTrue(!!remoteAB, "Should have found the address book");
+
+ browser.test.assertTrue(
+ remoteAB.remote,
+ "Should have marked the address book as remote"
+ );
+
+ let cards = await browser.contacts.quickSearch("eurus");
+ browser.test.assertTrue(
+ cards.length,
+ "Should have found at least one card"
+ );
+
+ browser.test.assertTrue(
+ cards[0].remote,
+ "Should have marked the card as remote"
+ );
+
+ // Mailing lists are not supported for LDAP address books.
+
+ 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"],
+ },
+ });
+
+ let startupPromise = extension.startup();
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry({
+ dn: "uid=eurus,dc=bakerstreet,dc=invalid",
+ attributes: {
+ objectClass: "person",
+ cn: "Eurus Holmes",
+ givenName: "Eurus",
+ mail: "eurus@bakerstreet.invalid",
+ sn: "Holmes",
+ },
+ });
+ LDAPServer.writeSearchResultDone();
+
+ await startupPromise;
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_alias.js b/comm/mail/components/extensions/test/xpcshell/test_ext_alias.js
new file mode 100644
index 0000000000..3fff1e0e08
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_alias.js
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+// ExtensionContent.jsm needs to know when it's running from xpcshell,
+// to use the right timeout for content scripts executed at document_idle.
+ExtensionTestUtils.mockAppInfo();
+
+AddonTestUtils.maybeInit(this);
+const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
+
+server.registerPathHandler("/dummy", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(
+ "<!DOCTYPE html><html><head><meta charset='utf8'></head><body></body></html>"
+ );
+});
+
+add_task(async function test_alias() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let pending = new Set(["contentscript", "webscript"]);
+
+ browser.runtime.onMessage.addListener(message => {
+ if (message == "contentscript") {
+ pending.delete(message);
+ browser.test.succeed("Content script has completed");
+ } else if (message == "webscript") {
+ pending.delete(message);
+ browser.test.succeed("Web accessible script has completed");
+ }
+
+ if (pending.size == 0) {
+ browser.test.notifyPass("ext_alias");
+ }
+ });
+
+ browser.test.assertEq(
+ "object",
+ typeof browser,
+ "Background script has browser object"
+ );
+ browser.test.assertEq(
+ "object",
+ typeof messenger,
+ "Background script has messenger object"
+ );
+ browser.test.assertEq(
+ "alias@xpcshell",
+ messenger.runtime.getManifest().applications.gecko.id, // eslint-disable-line no-undef
+ "Background script can access the manifest"
+ );
+ },
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://example.com/dummy"],
+ js: ["content.js"],
+ },
+ ],
+
+ applications: { gecko: { id: "alias@xpcshell" } },
+ web_accessible_resources: ["web.html", "web.js"],
+ },
+ files: {
+ "content.js": `
+ browser.test.assertEq("object", typeof browser, "Content script has browser object");
+ browser.test.assertEq("object", typeof messenger, "Content script has messenger object");
+ browser.test.assertEq(
+ "alias@xpcshell",
+ messenger.runtime.getManifest().applications.gecko.id,
+ "Content script can access manifest"
+ );
+
+ // Unprivileged content in a frame
+ let frame = document.createElement("iframe");
+ frame.src = browser.runtime.getURL("web.html");
+ document.body.appendChild(frame);
+
+ browser.runtime.sendMessage("contentscript");
+ `,
+ "web.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset='utf8'>
+ <script src="web.js"></script>
+ </head>
+ <body>
+ </body>
+ </html>
+ `,
+ "web.js": `
+ browser.test.assertEq("object", typeof browser, "Web accessible script has browser object");
+ browser.test.assertEq("object", typeof messenger, "Web accessible script has messenger object");
+ browser.test.assertEq(
+ "alias@xpcshell",
+ messenger.runtime.getManifest().applications.gecko.id,
+ "Web accessible script can access manifest"
+ );
+
+ browser.runtime.sendMessage("webscript");
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ const contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://example.com/dummy"
+ );
+ await extension.awaitFinish("ext_alias");
+
+ await contentPage.close();
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_browserAction_unifiedtoolbar_restart.js b/comm/mail/components/extensions/test/xpcshell/test_ext_browserAction_unifiedtoolbar_restart.js
new file mode 100644
index 0000000000..ef2687af68
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_browserAction_unifiedtoolbar_restart.js
@@ -0,0 +1,350 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+var { getCachedAllowedSpaces, setCachedAllowedSpaces } = ChromeUtils.import(
+ "resource:///modules/ExtensionToolbarButtons.jsm"
+);
+var { storeState, getState } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizationState.mjs"
+);
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const {
+ createAppInfo,
+ createHttpServer,
+ createTempXPIFile,
+ promiseRestartManager,
+ promiseShutdownManager,
+ promiseStartupManager,
+ promiseCompleteAllInstalls,
+ promiseFindAddonUpdates,
+} = AddonTestUtils;
+
+// Prepare test environment to be able to load add-on updates.
+const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+let gProfD = do_get_profile();
+let profileDir = gProfD.clone();
+profileDir.append("extensions");
+const stageDir = profileDir.clone();
+stageDir.append("staged");
+
+let server = createHttpServer({
+ hosts: ["example.com"],
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "102");
+
+async function enforceState(state) {
+ const stateChangeObserved = TestUtils.topicObserved(
+ "unified-toolbar-state-change"
+ );
+ storeState(state);
+ await stateChangeObserved;
+}
+
+function check(testType, expectedCache, expectedMail, expectedCalendar) {
+ let extensionId = `browser_action_spaces_${testType}@mochi.test`;
+
+ Assert.equal(
+ getCachedAllowedSpaces().has(extensionId),
+ expectedCache != null,
+ "CachedAllowedSpaces should include the test extension"
+ );
+ if (expectedCache != null) {
+ Assert.deepEqual(
+ getCachedAllowedSpaces().get(extensionId),
+ expectedCache,
+ "CachedAllowedSpaces should be correct"
+ );
+ }
+ Assert.equal(
+ getState().mail.includes(`ext-${extensionId}`),
+ expectedMail,
+ "The mail state should include the action button of the test extension"
+ );
+ Assert.equal(
+ getState().calendar.includes(`ext-${extensionId}`),
+ expectedCalendar,
+ "The calendar state should include the action button of the test extension"
+ );
+}
+
+function addXPI(testType, thisVersion, nextVersion, browser_action) {
+ server.registerFile(
+ `/addons/${testType}_v${thisVersion}.xpi`,
+ createTempXPIFile({
+ "manifest.json": {
+ manifest_version: 2,
+ name: testType,
+ version: `${thisVersion}.0`,
+ background: { scripts: ["background.js"] },
+ applications: {
+ gecko: {
+ id: `browser_action_spaces_${testType}@mochi.test`,
+ update_url: nextVersion
+ ? `http://example.com/${testType}_updates_v${nextVersion}.json`
+ : null,
+ },
+ },
+ browser_action,
+ },
+ "background.js": `
+ if (browser.runtime.getManifest().name == "delayed") {
+ browser.runtime.onUpdateAvailable.addListener(details => {
+ browser.test.sendMessage("update postponed by ${thisVersion}");
+ });
+ }
+ browser.test.log(" ===== ready ${testType} ${thisVersion}");
+ browser.test.sendMessage("ready ${thisVersion}");`,
+ })
+ );
+ if (nextVersion) {
+ addUpdateJSON(testType, nextVersion);
+ }
+}
+
+function addUpdateJSON(testType, nextVersion) {
+ let extensionId = `browser_action_spaces_${testType}@mochi.test`;
+
+ AddonTestUtils.registerJSON(
+ server,
+ `/${testType}_updates_v${nextVersion}.json`,
+ {
+ addons: {
+ [extensionId]: {
+ updates: [
+ {
+ version: `${nextVersion}.0`,
+ update_link: `http://example.com/addons/${testType}_v${nextVersion}.xpi`,
+ applications: {
+ gecko: {
+ strict_min_version: "1",
+ },
+ },
+ },
+ ],
+ },
+ },
+ }
+ );
+}
+
+async function checkForExtensionUpdate(testType, extension) {
+ let update = await promiseFindAddonUpdates(extension.addon);
+ let install = update.updateAvailable;
+ await promiseCompleteAllInstalls([install]);
+
+ if (testType == "normal") {
+ Assert.equal(
+ install.state,
+ AddonManager.STATE_INSTALLED,
+ "Update should have been installed"
+ );
+ } else {
+ Assert.equal(
+ install.state,
+ AddonManager.STATE_POSTPONED,
+ "Update should have been postponed"
+ );
+ }
+}
+
+async function runTest(testType) {
+ // Simulate starting up the app.
+ await promiseStartupManager();
+
+ // 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"],
+ });
+
+ // Check conditions before installing the add-on.
+ check(testType, null, false, false);
+
+ // Add the required update JSON to our test server, to be able to update to v2.
+ addUpdateJSON(testType, 2);
+ // Install addon v1 without a browserAction.
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ files: {
+ "background.js": function () {
+ if (browser.runtime.getManifest().name == "delayed") {
+ function handleUpdateAvailable(details) {
+ browser.test.sendMessage("update postponed by 1");
+ }
+ browser.runtime.onUpdateAvailable.addListener(handleUpdateAvailable);
+ }
+ browser.test.sendMessage("ready 1");
+ },
+ },
+ manifest: {
+ background: { scripts: ["background.js"] },
+ version: "1.0",
+ name: testType,
+ applications: {
+ gecko: {
+ id: `browser_action_spaces_${testType}@mochi.test`,
+ update_url: `http://example.com/${testType}_updates_v2.json`,
+ },
+ },
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("ready 1");
+
+ // State should not have changed.
+ check(testType, null, false, false);
+
+ // v2 will add the mail space and the default space.
+ addXPI(testType, 2, 3, { allowed_spaces: ["mail", "default"] });
+ await checkForExtensionUpdate(testType, extension);
+
+ if (testType == "delayed") {
+ await extension.awaitMessage("update postponed by 1");
+ // Restart to install the update v2.
+ await promiseRestartManager();
+ }
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 2");
+
+ // The button should have been added to the mail space.
+ check(testType, ["mail", "default"], true, false);
+
+ // Remove our extension button from all customized states.
+ await enforceState({
+ mail: ["spacer", "search-bar", "spacer"],
+ calendar: ["spacer", "search-bar", "spacer"],
+ });
+
+ // Simulate restarting the app.
+ await promiseRestartManager();
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 2");
+
+ // The button should not be re-added to any space after the restart.
+ check(testType, ["mail", "default"], false, false);
+
+ // v3 will add the calendar space.
+ addXPI(testType, 3, 4, {
+ allowed_spaces: ["mail", "calendar", "default"],
+ });
+ await checkForExtensionUpdate(testType, extension);
+
+ if (testType == "delayed") {
+ await extension.awaitMessage("update postponed by 2");
+ // Restart to install the update v3.
+ await promiseRestartManager();
+ }
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 3");
+
+ // The button should have been added to the calendar space.
+ check(testType, ["mail", "calendar", "default"], false, true);
+
+ // Simulate restarting the app.
+ await promiseRestartManager();
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 3");
+
+ // Should not have changed.
+ check(testType, ["mail", "calendar", "default"], false, true);
+
+ // v4 will remove the calendar space again.
+ addXPI(testType, 4, 5, { allowed_spaces: ["mail", "default"] });
+ await checkForExtensionUpdate(testType, extension);
+
+ if (testType == "delayed") {
+ await extension.awaitMessage("update postponed by 3");
+ // Restart to install the update v4.
+ await promiseRestartManager();
+ }
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 4");
+
+ // The calendar space should no longer be known and the button should be removed
+ // from the calendar space.
+ check(testType, ["mail", "default"], false, false);
+
+ // Simulate restarting the app.
+ await promiseRestartManager();
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 4");
+
+ // Should not have changed.
+ check(testType, ["mail", "default"], false, false);
+
+ // v5 will remove the entire browser_action. Testing the onUpdate code path in
+ // ext-browserAction.
+ addXPI(testType, 5, 6, null);
+ await checkForExtensionUpdate(testType, extension);
+
+ if (testType == "delayed") {
+ await extension.awaitMessage("update postponed by 4");
+ // Restart to install the update v5.
+ await promiseRestartManager();
+ }
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 5");
+
+ // There should no longer be a cached entry for any known spaces.
+ check(testType, null, false, false);
+
+ // Simulate restarting the app.
+ await promiseRestartManager();
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 5");
+
+ // Should not have changed.
+ check(testType, null, false, false);
+
+ // v6 will add the mail space again.
+ addXPI(testType, 6, null, { allowed_spaces: ["mail", "default"] });
+ await checkForExtensionUpdate(testType, extension);
+
+ if (testType == "delayed") {
+ await extension.awaitMessage("update postponed by 5");
+ // Restart to install the update v6.
+ await promiseRestartManager();
+ }
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 6");
+
+ // The button should have been added to the mail space.
+ check(testType, ["mail", "default"], true, false);
+
+ // Unload the extension. Testing the onUninstall code path in ext-browserAction.
+ await extension.unload();
+
+ // There should no longer be a cached entry for any known spaces.
+ check(testType, null, false, false);
+
+ await promiseShutdownManager();
+}
+
+add_task(async function test_normal_updates() {
+ await runTest("normal");
+});
+
+add_task(async function test_delayed_updates() {
+ await runTest("delayed");
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_experiments.js b/comm/mail/components/extensions/test/xpcshell/test_ext_experiments.js
new file mode 100644
index 0000000000..d8ccd58da6
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_experiments.js
@@ -0,0 +1,279 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(async function test_managers() {
+ let account = createAccount();
+ let folder = await createSubfolder(
+ account.incomingServer.rootFolder,
+ "test1"
+ );
+ await createMessages(folder, 5);
+
+ let files = {
+ "background.js": async () => {
+ let [testAccount] = await browser.accounts.list();
+ let testFolder = testAccount.folders.find(f => f.name == "test1");
+ let {
+ messages: [testMessage],
+ } = await browser.messages.list(testFolder);
+
+ let messageCount = await browser.testapi.testCanGetFolder(testFolder);
+ browser.test.assertEq(5, messageCount);
+
+ let convertedFolder = await browser.testapi.testCanConvertFolder();
+ browser.test.assertEq(testFolder.accountId, convertedFolder.accountId);
+ browser.test.assertEq(testFolder.path, convertedFolder.path);
+
+ let subject = await browser.testapi.testCanGetMessage(testMessage.id);
+ browser.test.assertEq(testMessage.subject, subject);
+
+ let convertedMessage = await browser.testapi.testCanConvertMessage();
+ browser.test.log(JSON.stringify(convertedMessage));
+ browser.test.assertEq(testMessage.id, convertedMessage.id);
+ browser.test.assertEq(testMessage.subject, convertedMessage.subject);
+
+ let messageList = await browser.testapi.testCanStartMessageList();
+ browser.test.assertEq(36, messageList.id.length);
+ browser.test.assertEq(4, messageList.messages.length);
+ browser.test.assertEq(
+ testMessage.subject,
+ messageList.messages[0].subject
+ );
+
+ messageList = await browser.messages.continueList(messageList.id);
+ browser.test.assertEq(null, messageList.id);
+ browser.test.assertEq(1, messageList.messages.length);
+ browser.test.assertTrue(
+ testMessage.subject != messageList.messages[0].subject
+ );
+
+ let [bookUID, contactUID, listUID] = await window.sendMessage("get UIDs");
+ let [foundBook, foundContact, foundList] =
+ await browser.testapi.testCanFindAddressBookItems(
+ bookUID,
+ contactUID,
+ listUID
+ );
+ browser.test.assertEq("new book", foundBook.name);
+ browser.test.assertEq("new contact", foundContact.properties.DisplayName);
+ browser.test.assertEq("new list", foundList.name);
+
+ browser.test.notifyPass("finished");
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ ...files,
+ "schema.json": [
+ {
+ namespace: "testapi",
+ functions: [
+ {
+ name: "testCanGetFolder",
+ type: "function",
+ async: true,
+ parameters: [
+ {
+ name: "folder",
+ $ref: "folders.MailFolder",
+ },
+ ],
+ },
+ {
+ name: "testCanConvertFolder",
+ type: "function",
+ async: true,
+ parameters: [],
+ },
+ {
+ name: "testCanGetMessage",
+ type: "function",
+ async: true,
+ parameters: [
+ {
+ name: "messageId",
+ type: "integer",
+ },
+ ],
+ },
+ {
+ name: "testCanConvertMessage",
+ type: "function",
+ async: true,
+ parameters: [],
+ },
+ {
+ name: "testCanStartMessageList",
+ type: "function",
+ async: true,
+ parameters: [],
+ },
+ {
+ name: "testCanFindAddressBookItems",
+ type: "function",
+ async: true,
+ parameters: [
+ { name: "bookUID", type: "string" },
+ { name: "contactUID", type: "string" },
+ { name: "listUID", type: "string" },
+ ],
+ },
+ ],
+ },
+ ],
+ "implementation.js": () => {
+ var { ExtensionCommon } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionCommon.sys.mjs"
+ );
+ var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+ this.testapi = class extends ExtensionCommon.ExtensionAPI {
+ getAPI(context) {
+ return {
+ testapi: {
+ async testCanGetFolder({ accountId, path }) {
+ let realFolder = context.extension.folderManager.get(
+ accountId,
+ path
+ );
+ return realFolder.getTotalMessages(false);
+ },
+ async testCanConvertFolder() {
+ let realFolder = MailServices.accounts.allFolders.find(
+ f => f.name == "test1"
+ );
+ return context.extension.folderManager.convert(realFolder);
+ },
+ async testCanGetMessage(messageId) {
+ let realMessage =
+ context.extension.messageManager.get(messageId);
+ return realMessage.subject;
+ },
+ async testCanConvertMessage() {
+ let realFolder = MailServices.accounts.allFolders.find(
+ f => f.name == "test1"
+ );
+ let realMessage = [...realFolder.messages][0];
+ return context.extension.messageManager.convert(realMessage);
+ },
+ async testCanStartMessageList() {
+ let realFolder = MailServices.accounts.allFolders.find(
+ f => f.name == "test1"
+ );
+ return context.extension.messageManager.startMessageList(
+ realFolder.messages
+ );
+ },
+ async testCanFindAddressBookItems(
+ bookUID,
+ contactUID,
+ listUID
+ ) {
+ let foundBook =
+ context.extension.addressBookManager.findAddressBookById(
+ bookUID
+ );
+ let foundContact =
+ context.extension.addressBookManager.findContactById(
+ contactUID
+ );
+ let foundList =
+ context.extension.addressBookManager.findMailingListById(
+ listUID
+ );
+
+ return [
+ await context.extension.addressBookManager.convert(
+ foundBook
+ ),
+ await context.extension.addressBookManager.convert(
+ foundContact
+ ),
+ await context.extension.addressBookManager.convert(
+ foundList
+ ),
+ ];
+ },
+ },
+ };
+ }
+ };
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "addressBooks", "messagesRead"],
+ experiment_apis: {
+ testapi: {
+ schema: "schema.json",
+ parent: {
+ scopes: ["addon_parent"],
+ paths: [["testapi"]],
+ script: "implementation.js",
+ },
+ },
+ },
+ },
+ });
+
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "new book",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ let contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact.displayName = "new contact";
+ contact.firstName = "new";
+ contact.lastName = "contact";
+ contact.primaryEmail = "new.contact@invalid";
+ contact = book.addCard(contact);
+
+ let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = "new list";
+ list = book.addMailList(list);
+ list.addCard(contact);
+
+ Services.prefs.setIntPref("extensions.webextensions.messagesPerPage", 4);
+
+ await extension.startup();
+ await extension.awaitMessage("get UIDs");
+ extension.sendMessage(book.UID, contact.UID, list.UID);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ Services.prefs.clearUserPref("extensions.webextensions.messagesPerPage");
+
+ await new Promise(resolve => {
+ let observer = {
+ observe() {
+ Services.obs.removeObserver(observer, "addrbook-directory-deleted");
+ resolve();
+ },
+ };
+ Services.obs.addObserver(observer, "addrbook-directory-deleted");
+ MailServices.ab.deleteAddressBook(book.URI);
+ });
+});
+
+registerCleanupFunction(() => {
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_folders.js b/comm/mail/components/extensions/test/xpcshell/test_ext_folders.js
new file mode 100644
index 0000000000..39a8d63016
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_folders.js
@@ -0,0 +1,560 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_folders() {
+ let files = {
+ "background.js": async () => {
+ let [accountId, IS_IMAP] = await window.waitForMessage();
+
+ let account = await browser.accounts.get(accountId);
+ browser.test.assertEq(3, account.folders.length);
+
+ // Test create.
+
+ let onCreatedPromise = window.waitForEvent("folders.onCreated");
+ let folder1 = await browser.folders.create(account, "folder1");
+ let [createdFolder] = await onCreatedPromise;
+ for (let folder of [folder1, createdFolder]) {
+ browser.test.assertEq(accountId, folder.accountId);
+ browser.test.assertEq("folder1", folder.name);
+ browser.test.assertEq("/folder1", folder.path);
+ }
+
+ account = await browser.accounts.get(accountId);
+ // Check order of the returned folders being correct (new folder not last).
+ browser.test.assertEq(4, account.folders.length);
+ if (IS_IMAP) {
+ browser.test.assertEq("Inbox", account.folders[0].name);
+ browser.test.assertEq("Trash", account.folders[1].name);
+ } else {
+ browser.test.assertEq("Trash", account.folders[0].name);
+ browser.test.assertEq("Outbox", account.folders[1].name);
+ }
+ browser.test.assertEq("folder1", account.folders[2].name);
+ browser.test.assertEq("unused", account.folders[3].name);
+
+ let folder2 = await browser.folders.create(folder1, "folder+2");
+ browser.test.assertEq(accountId, folder2.accountId);
+ browser.test.assertEq("folder+2", folder2.name);
+ browser.test.assertEq("/folder1/folder+2", folder2.path);
+
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(4, account.folders.length);
+ browser.test.assertEq(1, account.folders[2].subFolders.length);
+ browser.test.assertEq(
+ "/folder1/folder+2",
+ account.folders[2].subFolders[0].path
+ );
+
+ // Test reject on creating already existing folder.
+ await browser.test.assertRejects(
+ browser.folders.create(folder1, "folder+2"),
+ `folders.create() failed, because folder+2 already exists in /folder1`,
+ "browser.folders.create threw exception"
+ );
+
+ // Test rename.
+
+ {
+ let onRenamedPromise = window.waitForEvent("folders.onRenamed");
+ let folder3 = await browser.folders.rename(
+ { accountId, path: "/folder1/folder+2" },
+ "folder3"
+ );
+ let [originalFolder, renamedFolder] = await onRenamedPromise;
+ // Test the original folder.
+ browser.test.assertEq(accountId, originalFolder.accountId);
+ browser.test.assertEq("folder+2", originalFolder.name);
+ browser.test.assertEq("/folder1/folder+2", originalFolder.path);
+ // Test the renamed folder.
+ for (let folder of [folder3, renamedFolder]) {
+ browser.test.assertEq(accountId, folder.accountId);
+ browser.test.assertEq("folder3", folder.name);
+ browser.test.assertEq("/folder1/folder3", folder.path);
+ }
+
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(4, account.folders.length);
+ browser.test.assertEq(1, account.folders[2].subFolders.length);
+ browser.test.assertEq(
+ "/folder1/folder3",
+ account.folders[2].subFolders[0].path
+ );
+
+ // Test reject on renaming absolute root.
+ await browser.test.assertRejects(
+ browser.folders.rename({ accountId, path: "/" }, "UhhOh"),
+ `folders.rename() failed, because it cannot rename the root of the account`,
+ "browser.folders.rename threw exception"
+ );
+
+ // Test reject on renaming to existing folder.
+ await browser.test.assertRejects(
+ browser.folders.rename(
+ { accountId, path: "/folder1/folder3" },
+ "folder3"
+ ),
+ `folders.rename() failed, because folder3 already exists in /folder1`,
+ "browser.folders.rename threw exception"
+ );
+ }
+
+ // Test delete (and onMoved).
+
+ {
+ // The delete request will trigger an onDelete event for IMAP and an
+ // onMoved event for local folders.
+ let deletePromise = window.waitForEvent(
+ `folders.${IS_IMAP ? "onDeleted" : "onMoved"}`
+ );
+ await browser.folders.delete({ accountId, path: "/folder1/folder3" });
+ // The onMoved event returns the original/deleted and the new folder.
+ // The onDeleted event returns just the original/deleted folder.
+ let [originalFolder, folderMovedToTrash] = await deletePromise;
+
+ // Test the originalFolder folder.
+ browser.test.assertEq(accountId, originalFolder.accountId);
+ browser.test.assertEq("folder3", originalFolder.name);
+ browser.test.assertEq("/folder1/folder3", originalFolder.path);
+
+ // Check if it really is in trash folder.
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(4, account.folders.length);
+ let trashFolder = account.folders.find(f => f.name == "Trash");
+ browser.test.assertTrue(trashFolder);
+ browser.test.assertEq("/Trash", trashFolder.path);
+ browser.test.assertEq(1, trashFolder.subFolders.length);
+ browser.test.assertEq(
+ "/Trash/folder3",
+ trashFolder.subFolders[0].path
+ );
+ browser.test.assertEq("/folder1", account.folders[2].path);
+
+ if (!IS_IMAP) {
+ // For non IMAP folders, the delete request has triggered an onMoved
+ // event, check if that has reported moving the folder to trash.
+ browser.test.assertEq(accountId, folderMovedToTrash.accountId);
+ browser.test.assertEq("folder3", folderMovedToTrash.name);
+ browser.test.assertEq("/Trash/folder3", folderMovedToTrash.path);
+
+ // Delete the folder from trash.
+ let onDeletedPromise = window.waitForEvent("folders.onDeleted");
+ await browser.folders.delete({ accountId, path: "/Trash/folder3" });
+ let [deletedFolder] = await onDeletedPromise;
+ browser.test.assertEq(accountId, deletedFolder.accountId);
+ browser.test.assertEq("folder3", deletedFolder.name);
+ browser.test.assertEq("/Trash/folder3", deletedFolder.path);
+ // Check if the folder is gone.
+ let trashSubfolders = await browser.folders.getSubFolders(
+ trashFolder,
+ false
+ );
+ browser.test.assertEq(
+ 0,
+ trashSubfolders.length,
+ "Folder has been deleted from trash."
+ );
+ } else {
+ // The IMAP test server signals success for the delete request, but
+ // keeps the folder. Testing for this broken behavior to get notified
+ // via test fails, if this behaviour changes.
+ await browser.folders.delete({ accountId, path: "/Trash/folder3" });
+ let trashSubfolders = await browser.folders.getSubFolders(
+ trashFolder,
+ false
+ );
+ browser.test.assertEq(
+ "/Trash/folder3",
+ trashSubfolders[0].path,
+ "IMAP test server cannot delete from trash, the folder is still there."
+ );
+ }
+
+ // Test reject on deleting non-existing folder.
+ await browser.test.assertRejects(
+ browser.folders.delete({ accountId, path: "/folder1/folder5" }),
+ `Folder not found: /folder1/folder5`,
+ "browser.folders.delete threw exception"
+ );
+
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(4, account.folders.length);
+ browser.test.assertEq("/folder1", account.folders[2].path);
+ }
+
+ // Test move.
+
+ {
+ await browser.folders.create(folder1, "folder4");
+ let onMovedPromise = window.waitForEvent("folders.onMoved");
+ let folder4_moved = await browser.folders.move(
+ { accountId, path: "/folder1/folder4" },
+ { accountId, path: "/" }
+ );
+ let [originalFolder, movedFolder] = await onMovedPromise;
+ // Test the original folder.
+ browser.test.assertEq(accountId, originalFolder.accountId);
+ browser.test.assertEq("folder4", originalFolder.name);
+ browser.test.assertEq("/folder1/folder4", originalFolder.path);
+ // Test the moved folder.
+ for (let folder of [folder4_moved, movedFolder]) {
+ browser.test.assertEq(accountId, folder.accountId);
+ browser.test.assertEq("folder4", folder.name);
+ browser.test.assertEq("/folder4", folder.path);
+ }
+
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(5, account.folders.length);
+ browser.test.assertEq("/folder4", account.folders[3].path);
+
+ // Test reject on moving to already existing folder.
+ await browser.test.assertRejects(
+ browser.folders.move(folder4_moved, account),
+ `folders.move() failed, because folder4 already exists in /`,
+ "browser.folders.move threw exception"
+ );
+ }
+
+ // Test copy.
+
+ {
+ let onCopiedPromise = window.waitForEvent("folders.onCopied");
+ let folder4_copied = await browser.folders.copy(
+ { accountId, path: "/folder4" },
+ { accountId, path: "/folder1" }
+ );
+ let [originalFolder, copiedFolder] = await onCopiedPromise;
+ // Test the original folder.
+ browser.test.assertEq(accountId, originalFolder.accountId);
+ browser.test.assertEq("folder4", originalFolder.name);
+ browser.test.assertEq("/folder4", originalFolder.path);
+ // Test the copied folder.
+ for (let folder of [folder4_copied, copiedFolder]) {
+ browser.test.assertEq(accountId, folder.accountId);
+ browser.test.assertEq("folder4", folder.name);
+ browser.test.assertEq("/folder1/folder4", folder.path);
+ }
+
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(5, account.folders.length);
+ browser.test.assertEq(1, account.folders[2].subFolders.length);
+ browser.test.assertEq("/folder4", account.folders[3].path);
+ browser.test.assertEq(
+ "/folder1/folder4",
+ account.folders[2].subFolders[0].path
+ );
+
+ // Test reject on copy to already existing folder.
+ await browser.test.assertRejects(
+ browser.folders.copy(folder4_copied, folder1),
+ `folders.copy() failed, because folder4 already exists in /folder1`,
+ "browser.folders.copy threw exception"
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsFolders", "messagesDelete"],
+ },
+ });
+
+ let account = createAccount();
+ // Not all folders appear immediately on IMAP. Creating a new one causes them to appear.
+ await createSubfolder(account.incomingServer.rootFolder, "unused");
+
+ // We should now have three folders. For IMAP accounts they are Inbox, Trash,
+ // and unused. Otherwise they are Trash, Unsent Messages and unused.
+
+ await extension.startup();
+ extension.sendMessage(account.key, IS_IMAP);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_without_delete_permission() {
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+
+ // Test reject on delete without messagesDelete permission.
+ await browser.test.assertRejects(
+ browser.folders.delete({ accountId, path: "/unused" }),
+ `Using folders.delete() requires the "accountsFolders" and the "messagesDelete" permission`,
+ "It rejects for a missing permission."
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsFolders"],
+ },
+ });
+
+ let account = createAccount();
+ // Not all folders appear immediately on IMAP. Creating a new one causes them to appear.
+ await createSubfolder(account.incomingServer.rootFolder, "unused");
+
+ // We should now have three folders. For IMAP accounts they are Inbox,
+ // Trash, and unused. Otherwise they are Trash, Unsent Messages and unused.
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(async function test_getParentFolders_getSubFolders() {
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+ let account = await browser.accounts.get(accountId);
+
+ async function createSubFolder(folderOrAccount, name) {
+ let subFolder = await browser.folders.create(folderOrAccount, name);
+ let basePath = folderOrAccount.path || "/";
+ if (!basePath.endsWith("/")) {
+ basePath = basePath + "/";
+ }
+ browser.test.assertEq(accountId, subFolder.accountId);
+ browser.test.assertEq(name, subFolder.name);
+ browser.test.assertEq(`${basePath}${name}`, subFolder.path);
+ return subFolder;
+ }
+
+ // Create a new root folder in the account.
+ let root = await createSubFolder(account, "MyRoot");
+
+ // Build a flat list of newly created nested folders in MyRoot.
+ let flatFolders = [root];
+ for (let i = 0; i < 10; i++) {
+ flatFolders.push(await createSubFolder(flatFolders[i], `level${i}`));
+ }
+
+ // Test getParentFolders().
+
+ // Pop out the last child folder and get its parents.
+ let lastChild = flatFolders.pop();
+ let parentsWithSubDefault = await browser.folders.getParentFolders(
+ lastChild
+ );
+ let parentsWithSubFalse = await browser.folders.getParentFolders(
+ lastChild,
+ false
+ );
+ let parentsWithSubTrue = await browser.folders.getParentFolders(
+ lastChild,
+ true
+ );
+
+ browser.test.assertEq(10, parentsWithSubDefault.length, "Correct depth.");
+ browser.test.assertEq(10, parentsWithSubFalse.length, "Correct depth.");
+ browser.test.assertEq(10, parentsWithSubTrue.length, "Correct depth.");
+
+ // Reverse the flatFolders array, to match the expected return value of
+ // getParentFolders().
+ flatFolders.reverse();
+
+ // Build expected nested subfolder structure.
+ lastChild.subFolders = [];
+ let flatFoldersWithSub = [];
+ for (let i = 0; i < 10; i++) {
+ let f = {};
+ Object.assign(f, flatFolders[i]);
+ if (i == 0) {
+ f.subFolders = [lastChild];
+ } else {
+ f.subFolders = [flatFoldersWithSub[i - 1]];
+ }
+ flatFoldersWithSub.push(f);
+ }
+
+ // Test return values of getParentFolders(). The way the flatFolder array
+ // has been created, its entries do not have subFolder properties.
+ for (let i = 0; i < 10; i++) {
+ window.assertDeepEqual(parentsWithSubFalse[i], flatFolders[i]);
+ window.assertDeepEqual(flatFolders[i], parentsWithSubFalse[i]);
+
+ window.assertDeepEqual(parentsWithSubTrue[i], flatFoldersWithSub[i]);
+ window.assertDeepEqual(flatFoldersWithSub[i], parentsWithSubTrue[i]);
+
+ // Default = false
+ window.assertDeepEqual(parentsWithSubDefault[i], flatFolders[i]);
+ window.assertDeepEqual(flatFolders[i], parentsWithSubDefault[i]);
+ }
+
+ // Test getSubFolders().
+
+ let expectedSubsWithSub = [flatFoldersWithSub[8]];
+ let expectedSubsWithoutSub = [flatFolders[8]];
+
+ // Test excluding subfolders (so only the direct subfolder are reported).
+ let subsWithSubFalse = await browser.folders.getSubFolders(root, false);
+ window.assertDeepEqual(expectedSubsWithoutSub, subsWithSubFalse);
+ window.assertDeepEqual(subsWithSubFalse, expectedSubsWithoutSub);
+
+ // Test including all subfolders.
+ let subsWithSubTrue = await browser.folders.getSubFolders(root, true);
+ window.assertDeepEqual(expectedSubsWithSub, subsWithSubTrue);
+ window.assertDeepEqual(subsWithSubTrue, expectedSubsWithSub);
+
+ // Test default subfolder handling of getSubFolders (= true).
+ let subsWithSubDefault = await browser.folders.getSubFolders(root);
+ window.assertDeepEqual(subsWithSubDefault, subsWithSubTrue);
+ window.assertDeepEqual(subsWithSubTrue, subsWithSubDefault);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsFolders"],
+ },
+ });
+
+ let account = createAccount();
+ // Not all folders appear immediately on IMAP. Creating a new one causes them to appear.
+ await createSubfolder(account.incomingServer.rootFolder, "unused");
+
+ // We should now have three folders. For IMAP accounts they are Inbox,
+ // Trash, and unused. Otherwise they are Trash, Unsent Messages and unused.
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_getFolderInfo() {
+ let files = {
+ "background.js": async () => {
+ let [accountId, IS_NNTP] = await window.waitForMessage();
+
+ let account = await browser.accounts.get(accountId);
+ browser.test.assertEq(IS_NNTP ? 1 : 3, account.folders.length);
+ let folders = await browser.folders.getSubFolders(account, false);
+ let InfoTestFolder = folders.find(f => f.name == "InfoTest");
+
+ // Verify initial state.
+ let info = await browser.folders.getFolderInfo(InfoTestFolder);
+ window.assertDeepEqual(
+ { totalMessageCount: 12, unreadMessageCount: 12, favorite: false },
+ info
+ );
+
+ // Test flipping favorite to true and marking all messages as read.
+ let onFolderInfoChangedPromise = window.waitForEvent(
+ "folders.onFolderInfoChanged"
+ );
+ await window.sendMessage("markAllAsRead");
+ await window.sendMessage("setFavorite", true);
+ let [mailFolder, mailFolderInfo] = await onFolderInfoChangedPromise;
+ window.assertDeepEqual(
+ { unreadMessageCount: 0, favorite: true },
+ mailFolderInfo
+ );
+ browser.test.assertEq(InfoTestFolder.path, mailFolder.path);
+
+ info = await browser.folders.getFolderInfo(InfoTestFolder);
+ window.assertDeepEqual(
+ { totalMessageCount: 12, unreadMessageCount: 0, favorite: true },
+ info
+ );
+
+ // Test flipping favorite back to false.
+ onFolderInfoChangedPromise = window.waitForEvent(
+ "folders.onFolderInfoChanged"
+ );
+ await window.sendMessage("setFavorite", false);
+ [mailFolder, mailFolderInfo] = await onFolderInfoChangedPromise;
+ window.assertDeepEqual({ favorite: false }, mailFolderInfo);
+ browser.test.assertEq(InfoTestFolder.path, mailFolder.path);
+
+ // Test setting some messages back to unread.
+ onFolderInfoChangedPromise = window.waitForEvent(
+ "folders.onFolderInfoChanged"
+ );
+ await window.sendMessage("markSomeAsUnread", 5);
+ [mailFolder, mailFolderInfo] = await onFolderInfoChangedPromise;
+ window.assertDeepEqual({ unreadMessageCount: 5 }, mailFolderInfo);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsFolders", "messagesDelete"],
+ },
+ });
+
+ let account = createAccount();
+ // Not all folders appear immediately on IMAP. Creating a new one causes them to appear.
+ let InfoTestFolder = await createSubfolder(
+ account.incomingServer.rootFolder,
+ "InfoTest"
+ );
+ await createMessages(InfoTestFolder, 12);
+
+ extension.onMessage("markAllAsRead", () => {
+ InfoTestFolder.markAllMessagesRead(null);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("markSomeAsUnread", count => {
+ let messages = InfoTestFolder.messages;
+ while (messages.hasMoreElements() && count > 0) {
+ let msg = messages.getNext();
+ msg.markRead(false);
+ count--;
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("setFavorite", value => {
+ if (value) {
+ InfoTestFolder.setFlag(Ci.nsMsgFolderFlags.Favorite);
+ } else {
+ InfoTestFolder.clearFlag(Ci.nsMsgFolderFlags.Favorite);
+ }
+ extension.sendMessage();
+ });
+
+ // We should now have three folders. For IMAP accounts they are Inbox, Trash,
+ // and InfoTest. Otherwise they are Trash, Unsent Messages and InfoTest.
+
+ await extension.startup();
+ extension.sendMessage(account.key, IS_NNTP);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_folders_mv3_event_pages.js b/comm/mail/components/extensions/test/xpcshell/test_ext_folders_mv3_event_pages.js
new file mode 100644
index 0000000000..eac947cda8
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_folders_mv3_event_pages.js
@@ -0,0 +1,374 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+registerCleanupFunction(async () => {
+ // Remove the temporary MozillaMailnews folder, which is not deleted in time when
+ // the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
+ // files in the temp folder.
+ // Note: PathUtils.tempDir points to the system temp folder, which is different.
+ let path = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "MozillaMailnews"
+ );
+ await IOUtils.remove(path, { recursive: true });
+});
+
+// Test events and persistent events for Manifest V3 for onCreated, onRenamed,
+// onMoved, onCopied and onDeleted.
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_folders_MV3_event_pages() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ addIdentity(account, "id1@invalid");
+
+ let files = {
+ "background.js": () => {
+ for (let eventName of [
+ "onCreated",
+ "onDeleted",
+ "onCopied",
+ "onRenamed",
+ "onMoved",
+ ]) {
+ browser.folders[eventName].addListener(async (...args) => {
+ browser.test.log(`${eventName} received: ${JSON.stringify(args)}`);
+ 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"],
+ },
+ });
+
+ // 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;
+
+ browser.folders[_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"] },
+ permissions: ["accountsRead"],
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "folders", eventName, { primed: false });
+
+ await ext.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listener.
+ assertPersistentListeners(ext, "folders", 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, "folders", eventName, { primed: false });
+
+ await ext.unload();
+ return rv;
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+
+ // Create a test folder before terminating the background script, to make sure
+ // everything is sane.
+
+ rootFolder.createSubfolder("TestFolder", null);
+ await extension.awaitMessage("onCreated received");
+ if (IS_IMAP) {
+ // IMAP creates a default Trash folder on the fly.
+ await extension.awaitMessage("onCreated received");
+ }
+
+ // Create SubFolder1.
+
+ {
+ rootFolder.createSubfolder("SubFolder1", null);
+ let createData = await extension.awaitMessage("onCreated received");
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder1",
+ path: "/SubFolder1",
+ },
+ ],
+ createData,
+ "The onCreated event should return the correct values"
+ );
+ }
+
+ // Create SubFolder2 (used for primed onFolderInfoChanged).
+
+ {
+ let primedChangeData = await event_page_extension(
+ "onFolderInfoChanged",
+ () => {
+ rootFolder.createSubfolder("SubFolder3", null);
+ }
+ );
+ let createData = await extension.awaitMessage("onCreated received");
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder3",
+ },
+ ],
+ createData,
+ "The onCreated event should return the correct values"
+ );
+ // Testing for onFolderInfoChanged is difficult, because it may not be for
+ // the last created folder, but for one of the folders created earlier. We
+ // therefore do not check the folder, but only the value.
+ Assert.deepEqual(
+ { totalMessageCount: 0, unreadMessageCount: 0 },
+ primedChangeData[1],
+ "The primed onFolderInfoChanged event should return the correct values"
+ );
+ }
+
+ // Copy.
+
+ {
+ let primedCopyData = await event_page_extension("onCopied", () => {
+ MailServices.copy.copyFolder(
+ rootFolder.getChildNamed("SubFolder3"),
+ rootFolder.getChildNamed("SubFolder1"),
+ false,
+ null,
+ null
+ );
+ });
+ let copyData = await extension.awaitMessage("onCopied received");
+ Assert.deepEqual(
+ primedCopyData,
+ copyData,
+ "The primed onCopied event should return the correct values"
+ );
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder3",
+ },
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder1/SubFolder3",
+ },
+ ],
+ copyData,
+ "The onCopied event should return the correct values"
+ );
+
+ if (IS_IMAP) {
+ // IMAP fires an additional create event.
+ let createData = await extension.awaitMessage("onCreated received");
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder1/SubFolder3",
+ },
+ ],
+ createData,
+ "The onCreated event should return the correct MailFolder values."
+ );
+ }
+ }
+
+ // Move.
+
+ {
+ let primedMoveData = await event_page_extension("onMoved", () => {
+ MailServices.copy.copyFolder(
+ rootFolder.getChildNamed("SubFolder1").getChildNamed("SubFolder3"),
+ rootFolder.getChildNamed("SubFolder3"),
+ true,
+ null,
+ null
+ );
+ });
+
+ let moveData = await extension.awaitMessage("onMoved received");
+ Assert.deepEqual(
+ primedMoveData,
+ moveData,
+ "The primed onMoved event should return the correct values"
+ );
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder1/SubFolder3",
+ },
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder3/SubFolder3",
+ },
+ ],
+ moveData,
+ "The onMoved event should return the correct values"
+ );
+
+ if (IS_IMAP) {
+ // IMAP fires additional rename and delete events.
+ let renameData = await extension.awaitMessage("onRenamed received");
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder1/SubFolder3",
+ },
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder3/SubFolder3",
+ },
+ ],
+ renameData,
+ "The onRenamed event should return the correct MailFolder values."
+ );
+ let deleteData = await extension.awaitMessage("onDeleted received");
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder1/SubFolder3",
+ },
+ ],
+ deleteData,
+ "The onDeleted event should return the correct MailFolder values."
+ );
+ }
+ }
+
+ // Delete.
+
+ {
+ let primedDeleteData = await event_page_extension("onDeleted", () => {
+ let subFolder1 = rootFolder.getChildNamed("SubFolder3");
+ subFolder1.propagateDelete(
+ subFolder1.getChildNamed("SubFolder3"),
+ true
+ );
+ });
+ let deleteData = await extension.awaitMessage("onDeleted received");
+ Assert.deepEqual(
+ primedDeleteData,
+ deleteData,
+ "The primed onDeleted event should return the correct values"
+ );
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder3/SubFolder3",
+ },
+ ],
+ deleteData,
+ "The onDeleted event should return the correct values"
+ );
+ }
+
+ // Rename.
+
+ {
+ let primedRenameData = await event_page_extension("onRenamed", () => {
+ rootFolder.getChildNamed("TestFolder").rename("TestFolder2", null);
+ });
+ let renameData = await extension.awaitMessage("onRenamed received");
+ Assert.deepEqual(
+ primedRenameData,
+ renameData,
+ "The primed onRenamed event should return the correct values"
+ );
+ if (IS_IMAP) {
+ // IMAP server sends an additional onDeleted and onCreated.
+ await extension.awaitMessage("onDeleted received");
+ await extension.awaitMessage("onCreated received");
+ }
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "TestFolder",
+ path: "/TestFolder",
+ },
+ {
+ accountId: account.key,
+ name: "TestFolder2",
+ path: "/TestFolder2",
+ },
+ ],
+ renameData,
+ "The onRenamed event should return the correct values"
+ );
+ }
+
+ await extension.unload();
+
+ cleanUpAccount(account);
+ await AddonTestUtils.promiseShutdownManager();
+ }
+);
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_identities_mv3_event_pages.js b/comm/mail/components/extensions/test/xpcshell/test_ext_identities_mv3_event_pages.js
new file mode 100644
index 0000000000..0b12f8ca1c
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_identities_mv3_event_pages.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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+add_task(async function test_identities_MV3_event_pages() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let account1 = createAccount();
+ addIdentity(account1, "id1@invalid");
+
+ 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 ["onCreated", "onUpdated", "onDeleted"]) {
+ browser.identities[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", "accountsIdentities"],
+ browser_specific_settings: { gecko: { id: "identities@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 = [
+ "identities.onCreated",
+ "identities.onUpdated",
+ "identities.onDeleted",
+ ];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ await extension.startup();
+
+ await extension.awaitMessage("background started");
+ // Verify persistent listener, not yet primed.
+ checkPersistentListeners({ primed: false });
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Create.
+
+ let id2 = addIdentity(account1, "id2@invalid");
+ let createData = await extension.awaitMessage("onCreated received");
+ Assert.deepEqual(
+ [
+ "id2",
+ {
+ accountId: "account1",
+ id: "id2",
+ label: "",
+ name: "",
+ email: "id2@invalid",
+ replyTo: "",
+ organization: "",
+ composeHtml: true,
+ signature: "",
+ signatureIsPlainText: true,
+ },
+ ],
+ createData,
+ "The primed onCreated event should return the correct values"
+ );
+
+ await extension.awaitMessage("background started");
+ // Verify persistent listener, not yet primed.
+ checkPersistentListeners({ primed: false });
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Update
+
+ id2.fullName = "Updated Name";
+ let updateData = await extension.awaitMessage("onUpdated received");
+ Assert.deepEqual(
+ ["id2", { name: "Updated Name", accountId: "account1", id: "id2" }],
+ updateData,
+ "The primed onUpdated event should return the correct values"
+ );
+ await extension.awaitMessage("background started");
+ // Verify persistent listener, not yet primed.
+ checkPersistentListeners({ primed: false });
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Delete
+
+ account1.removeIdentity(id2);
+ let deleteData = await extension.awaitMessage("onDeleted received");
+ Assert.deepEqual(
+ ["id2"],
+ deleteData,
+ "The primed onDeleted event should return the correct values"
+ );
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+
+ cleanUpAccount(account1);
+ await AddonTestUtils.promiseShutdownManager();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages.js
new file mode 100644
index 0000000000..24b2cb1484
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages.js
@@ -0,0 +1,730 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { ExtensionsUI } = ChromeUtils.import(
+ "resource:///modules/ExtensionsUI.jsm"
+);
+
+let account, rootFolder, subFolders;
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function setup() {
+ account = createAccount();
+ rootFolder = account.incomingServer.rootFolder;
+ subFolders = {
+ test3: await createSubfolder(rootFolder, "test3"),
+ test4: await createSubfolder(rootFolder, "test4"),
+ trash: rootFolder.getChildNamed("Trash"),
+ };
+ await createMessages(subFolders.trash, 99);
+ await createMessages(subFolders.test4, 1);
+ }
+);
+
+add_task(async function non_canonical_permission_description_mapping() {
+ let { msgs } = ExtensionsUI._buildStrings({
+ addon: { name: "FakeExtension" },
+ permissions: {
+ origins: [],
+ permissions: ["accountsRead", "messagesMove"],
+ },
+ });
+ equal(2, msgs.length, "Correct amount of descriptions");
+ equal(
+ "See your mail accounts, their identities and their folders",
+ msgs[0],
+ "Correct description for accountsRead"
+ );
+ equal(
+ "Copy or move your email messages (including moving them to the trash folder)",
+ msgs[1],
+ "Correct description for messagesMove"
+ );
+});
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_pagination() {
+ let files = {
+ "background.js": async () => {
+ // Test a response of 99 messages at 10 messages per page.
+ let [folder] = await window.waitForMessage();
+ let page = await browser.messages.list(folder);
+ browser.test.assertEq(36, page.id.length);
+ browser.test.assertEq(10, page.messages.length);
+
+ let originalPageId = page.id;
+ let numPages = 1;
+ let numMessages = 10;
+ while (page.id) {
+ page = await browser.messages.continueList(page.id);
+ browser.test.assertTrue(page.messages.length > 0);
+ numPages++;
+ numMessages += page.messages.length;
+ if (numMessages < 99) {
+ browser.test.assertEq(originalPageId, page.id);
+ } else {
+ browser.test.assertEq(null, page.id);
+ }
+ }
+ browser.test.assertEq(10, numPages);
+ browser.test.assertEq(99, numMessages);
+
+ browser.test.assertRejects(
+ browser.messages.continueList(originalPageId),
+ /No message list for id .*\. Have you reached the end of a list\?/
+ );
+
+ await window.sendMessage("setPref");
+
+ // Do the same test, but with the default 100 messages per page.
+ page = await browser.messages.list(folder);
+ browser.test.assertEq(null, page.id);
+ browser.test.assertEq(99, page.messages.length);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ Services.prefs.setIntPref("extensions.webextensions.messagesPerPage", 10);
+
+ await extension.startup();
+ extension.sendMessage({ accountId: account.key, path: "/Trash" });
+
+ await extension.awaitMessage("setPref");
+ Services.prefs.clearUserPref("extensions.webextensions.messagesPerPage");
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_delete_without_permission() {
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+ let { folders } = await browser.accounts.get(accountId);
+ let testFolder4 = folders.find(f => f.name == "test4");
+
+ let { messages: folder4Messages } = await browser.messages.list(
+ testFolder4
+ );
+
+ // Try to delete a message.
+ await browser.test.assertThrows(
+ () => browser.messages.delete([folder4Messages[0].id], true),
+ `browser.messages.delete is not a function`,
+ "Should reject deleting without proper permission"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ browser_specific_settings: {
+ gecko: { id: "messages.delete@mochi.test" },
+ },
+ permissions: ["accountsRead", "messagesMove", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_move_and_copy_without_permission() {
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+ let { folders } = await browser.accounts.get(accountId);
+ let testFolder4 = folders.find(f => f.name == "test4");
+ let testFolder3 = folders.find(f => f.name == "test3");
+
+ let { messages: folder4Messages } = await browser.messages.list(
+ testFolder4
+ );
+
+ // Try to move a message.
+ await browser.test.assertRejects(
+ browser.messages.move([folder4Messages[0].id], testFolder3),
+ `Using messages.move() requires the "accountsRead" and the "messagesMove" permission`,
+ "Should reject move without proper permission"
+ );
+
+ // Try to copy a message.
+ await browser.test.assertRejects(
+ browser.messages.copy([folder4Messages[0].id], testFolder3),
+ `Using messages.copy() requires the "accountsRead" and the "messagesMove" permission`,
+ "Should reject copy without proper permission"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ browser_specific_settings: {
+ gecko: { id: "messages.move@mochi.test" },
+ },
+ permissions: ["messagesRead", "accountsRead"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_tags() {
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+ let { folders } = await browser.accounts.get(accountId);
+ let testFolder4 = folders.find(f => f.name == "test4");
+ let { messages: folder4Messages } = await browser.messages.list(
+ testFolder4
+ );
+
+ let tags1 = await browser.messages.listTags();
+ window.assertDeepEqual(
+ [
+ {
+ key: "$label1",
+ tag: "Important",
+ color: "#FF0000",
+ ordinal: "",
+ },
+ {
+ key: "$label2",
+ tag: "Work",
+ color: "#FF9900",
+ ordinal: "",
+ },
+ {
+ key: "$label3",
+ tag: "Personal",
+ color: "#009900",
+ ordinal: "",
+ },
+ {
+ key: "$label4",
+ tag: "To Do",
+ color: "#3333FF",
+ ordinal: "",
+ },
+ {
+ key: "$label5",
+ tag: "Later",
+ color: "#993399",
+ ordinal: "",
+ },
+ ],
+ tags1
+ );
+
+ // Test some allowed special chars and that the key is created as lower
+ // case.
+ let goodKeys = [
+ "TestKey",
+ "Test_Key",
+ "Test\\Key",
+ "Test}Key",
+ "Test&Key",
+ "Test!Key",
+ "Test§Key",
+ "Test$Key",
+ "Test=Key",
+ "Test?Key",
+ ];
+ for (let key of goodKeys) {
+ await browser.messages.createTag(key, "Test Tag", "#123456");
+ let goodTags = await browser.messages.listTags();
+ window.assertDeepEqual(
+ [
+ {
+ key: "$label1",
+ tag: "Important",
+ color: "#FF0000",
+ ordinal: "",
+ },
+ {
+ key: "$label2",
+ tag: "Work",
+ color: "#FF9900",
+ ordinal: "",
+ },
+ {
+ key: "$label3",
+ tag: "Personal",
+ color: "#009900",
+ ordinal: "",
+ },
+ {
+ key: "$label4",
+ tag: "To Do",
+ color: "#3333FF",
+ ordinal: "",
+ },
+ {
+ key: "$label5",
+ tag: "Later",
+ color: "#993399",
+ ordinal: "",
+ },
+ {
+ key: key.toLowerCase(),
+ tag: "Test Tag",
+ color: "#123456",
+ ordinal: "",
+ },
+ ],
+ goodTags
+ );
+ await browser.messages.deleteTag(key.toLowerCase());
+ }
+
+ await browser.messages.createTag("custom_tag", "Custom Tag", "#123456");
+ let tags2 = await browser.messages.listTags();
+ window.assertDeepEqual(
+ [
+ {
+ key: "$label1",
+ tag: "Important",
+ color: "#FF0000",
+ ordinal: "",
+ },
+ {
+ key: "$label2",
+ tag: "Work",
+ color: "#FF9900",
+ ordinal: "",
+ },
+ {
+ key: "$label3",
+ tag: "Personal",
+ color: "#009900",
+ ordinal: "",
+ },
+ {
+ key: "$label4",
+ tag: "To Do",
+ color: "#3333FF",
+ ordinal: "",
+ },
+ {
+ key: "$label5",
+ tag: "Later",
+ color: "#993399",
+ ordinal: "",
+ },
+ {
+ key: "custom_tag",
+ tag: "Custom Tag",
+ color: "#123456",
+ ordinal: "",
+ },
+ ],
+ tags2
+ );
+
+ await browser.messages.updateTag("$label5", {
+ tag: "Much Later",
+ color: "#225599",
+ });
+ let tags3 = await browser.messages.listTags();
+ window.assertDeepEqual(
+ [
+ {
+ key: "$label1",
+ tag: "Important",
+ color: "#FF0000",
+ ordinal: "",
+ },
+ {
+ key: "$label2",
+ tag: "Work",
+ color: "#FF9900",
+ ordinal: "",
+ },
+ {
+ key: "$label3",
+ tag: "Personal",
+ color: "#009900",
+ ordinal: "",
+ },
+ {
+ key: "$label4",
+ tag: "To Do",
+ color: "#3333FF",
+ ordinal: "",
+ },
+ {
+ key: "$label5",
+ tag: "Much Later",
+ color: "#225599",
+ ordinal: "",
+ },
+ {
+ key: "custom_tag",
+ tag: "Custom Tag",
+ color: "#123456",
+ ordinal: "",
+ },
+ ],
+ tags3
+ );
+
+ // Test rejects for createTag().
+ let badKeys = [
+ "Bad Key",
+ "Bad%Key",
+ "Bad/Key",
+ "Bad*Key",
+ 'Bad"Key',
+ "Bad{Key}",
+ "Bad(Key)",
+ "Bad<Key>",
+ ];
+ for (let badKey of badKeys) {
+ await browser.test.assertThrows(
+ () =>
+ browser.messages.createTag(badKey, "Important Stuff", "#223344"),
+ /Type error for parameter key/,
+ `Should reject creating an invalid key: ${badKey}`
+ );
+ }
+
+ await browser.test.assertThrows(
+ () =>
+ browser.messages.createTag(
+ "GoodKeyBadColor",
+ "Important Stuff",
+ "#223"
+ ),
+ /Type error for parameter color /,
+ "Should reject creating a key using an invalid short color"
+ );
+
+ await browser.test.assertThrows(
+ () =>
+ browser.messages.createTag(
+ "GoodKeyBadColor",
+ "Important Stuff",
+ "123223"
+ ),
+ /Type error for parameter color /,
+ "Should reject creating a key using an invalid color without leading #"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.createTag("$label5", "Important Stuff", "#223344"),
+ `Specified key already exists: $label5`,
+ "Should reject creating a key which exists already"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.createTag(
+ "Custom_Tag",
+ "Important Stuff",
+ "#223344"
+ ),
+ `Specified key already exists: custom_tag`,
+ "Should reject creating a key which exists already"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.createTag("GoodKey", "Important", "#223344"),
+ `Specified tag already exists: Important`,
+ "Should reject creating a key using a tag which exists already"
+ );
+
+ // Test rejects for updateTag();
+ await browser.test.assertThrows(
+ () => browser.messages.updateTag("Bad Key", { tag: "Much Later" }),
+ /Type error for parameter key/,
+ "Should reject updating an invalid key"
+ );
+
+ await browser.test.assertThrows(
+ () =>
+ browser.messages.updateTag("GoodKeyBadColor", { color: "123223" }),
+ /Error processing color/,
+ "Should reject updating a key using an invalid color"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.updateTag("$label50", { tag: "Much Later" }),
+ `Specified key does not exist: $label50`,
+ "Should reject updating an unknown key"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.updateTag("$label5", { tag: "Important" }),
+ `Specified tag already exists: Important`,
+ "Should reject updating a key using a tag which exists already"
+ );
+
+ // Test rejects for deleteTag();
+ await browser.test.assertThrows(
+ () => browser.messages.deleteTag("Bad Key"),
+ /Type error for parameter key/,
+ "Should reject deleting an invalid key"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.deleteTag("$label50"),
+ `Specified key does not exist: $label50`,
+ "Should reject deleting an unknown key"
+ );
+
+ // Test tagging messages, deleting tag and re-creating tag.
+ await browser.messages.update(folder4Messages[0].id, {
+ tags: ["custom_tag"],
+ });
+ let message1 = await browser.messages.get(folder4Messages[0].id);
+ window.assertDeepEqual(["custom_tag"], message1.tags);
+
+ await browser.messages.deleteTag("custom_tag");
+ let message2 = await browser.messages.get(folder4Messages[0].id);
+ window.assertDeepEqual([], message2.tags);
+
+ await browser.messages.createTag("custom_tag", "Custom Tag", "#123456");
+ let message3 = await browser.messages.get(folder4Messages[0].id);
+ window.assertDeepEqual(["custom_tag"], message3.tags);
+
+ // Test deleting built-in tag.
+ await browser.messages.deleteTag("$label5");
+ let tags4 = await browser.messages.listTags();
+ window.assertDeepEqual(
+ [
+ {
+ key: "$label1",
+ tag: "Important",
+ color: "#FF0000",
+ ordinal: "",
+ },
+ {
+ key: "$label2",
+ tag: "Work",
+ color: "#FF9900",
+ ordinal: "",
+ },
+ {
+ key: "$label3",
+ tag: "Personal",
+ color: "#009900",
+ ordinal: "",
+ },
+ {
+ key: "$label4",
+ tag: "To Do",
+ color: "#3333FF",
+ ordinal: "",
+ },
+ {
+ key: "custom_tag",
+ tag: "Custom Tag",
+ color: "#123456",
+ ordinal: "",
+ },
+ ],
+ tags4
+ );
+
+ // Clean up.
+ await browser.messages.update(folder4Messages[0].id, { tags: [] });
+ await browser.messages.deleteTag("custom_tag");
+ await browser.messages.createTag("$label5", "Later", "#993399");
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesRead", "accountsRead", "messagesTags"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_tags_no_permission() {
+ let files = {
+ "background.js": async () => {
+ await browser.test.assertThrows(
+ () =>
+ browser.messages.createTag(
+ "custom_tag",
+ "Important Stuff",
+ "#223344"
+ ),
+ /browser.messages.createTag is not a function/,
+ "Should reject creating tags without messagesTags permission"
+ );
+
+ await browser.test.assertThrows(
+ () => browser.messages.updateTag("$label5", { tag: "Much Later" }),
+ /browser.messages.updateTag is not a function/,
+ "Should reject updating tags without messagesTags permission"
+ );
+
+ await browser.test.assertThrows(
+ () => browser.messages.deleteTag("$label5"),
+ /browser.messages.deleteTag is not a function/,
+ "Should reject deleting tags without messagesTags permission"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesRead", "accountsRead"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+// The IMAP fakeserver just can't handle this.
+add_task({ skip_if: () => IS_IMAP || IS_NNTP }, async function test_archive() {
+ let account2 = createAccount();
+ account2.addIdentity(MailServices.accounts.createIdentity());
+ let inbox2 = await createSubfolder(
+ account2.incomingServer.rootFolder,
+ "test"
+ );
+ await createMessages(inbox2, 15);
+
+ let month = 10;
+ for (let message of inbox2.messages) {
+ message.date = new Date(2018, month++, 15) * 1000;
+ }
+
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+
+ let accountBefore = await browser.accounts.get(accountId);
+ browser.test.assertEq(3, accountBefore.folders.length);
+ browser.test.assertEq("/test", accountBefore.folders[2].path);
+
+ let messagesBefore = await browser.messages.list(
+ accountBefore.folders[2]
+ );
+ browser.test.assertEq(15, messagesBefore.messages.length);
+ await browser.messages.archive(messagesBefore.messages.map(m => m.id));
+
+ let accountAfter = await browser.accounts.get(accountId);
+ browser.test.assertEq(4, accountAfter.folders.length);
+ browser.test.assertEq("/test", accountAfter.folders[3].path);
+ browser.test.assertEq("/Archives", accountAfter.folders[0].path);
+ browser.test.assertEq(3, accountAfter.folders[0].subFolders.length);
+ browser.test.assertEq(
+ "/Archives/2018",
+ accountAfter.folders[0].subFolders[0].path
+ );
+ browser.test.assertEq(
+ "/Archives/2019",
+ accountAfter.folders[0].subFolders[1].path
+ );
+ browser.test.assertEq(
+ "/Archives/2020",
+ accountAfter.folders[0].subFolders[2].path
+ );
+
+ let messagesAfter = await browser.messages.list(accountAfter.folders[3]);
+ browser.test.assertEq(0, messagesAfter.messages.length);
+
+ let messages2018 = await browser.messages.list(
+ accountAfter.folders[0].subFolders[0]
+ );
+ browser.test.assertEq(2, messages2018.messages.length);
+
+ let messages2019 = await browser.messages.list(
+ accountAfter.folders[0].subFolders[1]
+ );
+ browser.test.assertEq(12, messages2019.messages.length);
+
+ let messages2020 = await browser.messages.list(
+ accountAfter.folders[0].subFolders[2]
+ );
+ browser.test.assertEq(1, messages2020.messages.length);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesMove", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account2.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_attachments.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_attachments.js
new file mode 100644
index 0000000000..e46e35afe7
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_attachments.js
@@ -0,0 +1,499 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+add_task(
+ {
+ skip_if: () => IS_IMAP,
+ },
+ async function test_setup() {
+ let _account = createAccount();
+ let _testFolder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+
+ let textAttachment = {
+ body: "textAttachment",
+ filename: "test.txt",
+ contentType: "text/plain",
+ };
+ let binaryAttachment = {
+ body: btoa("binaryAttachment"),
+ filename: "test",
+ contentType: "application/octet-stream",
+ encoding: "base64",
+ };
+
+ await createMessages(_testFolder, {
+ count: 1,
+ subject: "0 attachments",
+ });
+ await createMessages(_testFolder, {
+ count: 1,
+ subject: "1 text attachment",
+ attachments: [textAttachment],
+ });
+ await createMessages(_testFolder, {
+ count: 1,
+ subject: "1 binary attachment",
+ attachments: [binaryAttachment],
+ });
+ await createMessages(_testFolder, {
+ count: 1,
+ subject: "2 attachments",
+ attachments: [binaryAttachment, textAttachment],
+ });
+ await createMessageFromFile(
+ _testFolder,
+ do_get_file("messages/nestedMessages.eml").path
+ );
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_IMAP,
+ },
+ async function test_attachments() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [account] = await browser.accounts.list();
+ let testFolder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(testFolder);
+ browser.test.assertEq(5, messages.length);
+
+ let attachments, attachment, file;
+
+ // "0 attachments" message.
+
+ attachments = await browser.messages.listAttachments(messages[0].id);
+ browser.test.assertEq("0 attachments", messages[0].subject);
+ browser.test.assertEq(0, attachments.length);
+
+ // "1 text attachment" message.
+
+ attachments = await browser.messages.listAttachments(messages[1].id);
+ browser.test.assertEq("1 text attachment", messages[1].subject);
+ browser.test.assertEq(1, attachments.length);
+
+ attachment = attachments[0];
+ browser.test.assertEq("text/plain", attachment.contentType);
+ browser.test.assertEq("test.txt", attachment.name);
+ browser.test.assertEq("1.2", attachment.partName);
+ browser.test.assertEq(14, attachment.size);
+
+ file = await browser.messages.getAttachmentFile(
+ messages[1].id,
+ attachment.partName
+ );
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(file instanceof File);
+ browser.test.assertEq("test.txt", file.name);
+ browser.test.assertEq(14, file.size);
+
+ browser.test.assertEq("textAttachment", await file.text());
+
+ let reader = new FileReader();
+ let data = await new Promise(resolve => {
+ reader.onload = e => resolve(e.target.result);
+ reader.readAsDataURL(file);
+ });
+
+ browser.test.assertEq(
+ "data:text/plain;base64,dGV4dEF0dGFjaG1lbnQ=",
+ data
+ );
+
+ // "1 binary attachment" message.
+
+ attachments = await browser.messages.listAttachments(messages[2].id);
+ browser.test.assertEq("1 binary attachment", messages[2].subject);
+ browser.test.assertEq(1, attachments.length);
+
+ attachment = attachments[0];
+ browser.test.assertEq(
+ attachment.contentType,
+ "application/octet-stream"
+ );
+ browser.test.assertEq("test", attachment.name);
+ browser.test.assertEq("1.2", attachment.partName);
+ browser.test.assertEq(16, attachment.size);
+
+ file = await browser.messages.getAttachmentFile(
+ messages[2].id,
+ attachment.partName
+ );
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(file instanceof File);
+ browser.test.assertEq("test", file.name);
+ browser.test.assertEq(16, file.size);
+
+ browser.test.assertEq("binaryAttachment", await file.text());
+
+ reader = new FileReader();
+ data = await new Promise(resolve => {
+ reader.onload = e => resolve(e.target.result);
+ reader.readAsDataURL(file);
+ });
+
+ browser.test.assertEq(
+ "data:application/octet-stream;base64,YmluYXJ5QXR0YWNobWVudA==",
+ data
+ );
+
+ // "2 attachments" message.
+
+ attachments = await browser.messages.listAttachments(messages[3].id);
+ browser.test.assertEq("2 attachments", messages[3].subject);
+ browser.test.assertEq(2, attachments.length);
+
+ attachment = attachments[0];
+ browser.test.assertEq(
+ attachment.contentType,
+ "application/octet-stream"
+ );
+ browser.test.assertEq("test", attachment.name);
+ browser.test.assertEq("1.2", attachment.partName);
+ browser.test.assertEq(16, attachment.size);
+
+ file = await browser.messages.getAttachmentFile(
+ messages[3].id,
+ attachment.partName
+ );
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(file instanceof File);
+ browser.test.assertEq("test", file.name);
+ browser.test.assertEq(16, file.size);
+
+ browser.test.assertEq("binaryAttachment", await file.text());
+
+ attachment = attachments[1];
+ browser.test.assertEq("text/plain", attachment.contentType);
+ browser.test.assertEq("test.txt", attachment.name);
+ browser.test.assertEq("1.3", attachment.partName);
+ browser.test.assertEq(14, attachment.size);
+
+ file = await browser.messages.getAttachmentFile(
+ messages[3].id,
+ attachment.partName
+ );
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(file instanceof File);
+ browser.test.assertEq("test.txt", file.name);
+ browser.test.assertEq(14, file.size);
+
+ browser.test.assertEq("textAttachment", await file.text());
+
+ await browser.test.assertRejects(
+ browser.messages.listAttachments(0),
+ /^Message not found: \d+\.$/,
+ "Bad message ID should throw"
+ );
+ await browser.test.assertRejects(
+ browser.messages.getAttachmentFile(0, "1.2"),
+ /^Message not found: \d+\.$/,
+ "Bad message ID should throw"
+ );
+ browser.test.assertThrows(
+ () => browser.messages.getAttachmentFile(messages[3].id, "silly"),
+ /^Type error for parameter partName .* for messages\.getAttachmentFile\.$/,
+ "Bad part name should throw"
+ );
+ await browser.test.assertRejects(
+ browser.messages.getAttachmentFile(messages[3].id, "1.42"),
+ /Part 1.42 not found in message \d+\./,
+ "Non-existent part should throw"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_IMAP,
+ },
+ async function test_messages_as_attachments() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [account] = await browser.accounts.list();
+ let testFolder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(testFolder);
+ browser.test.assertEq(5, messages.length);
+ let message = messages[4];
+
+ function validateMessage(msg, expectedValues) {
+ for (let expectedValueName in expectedValues) {
+ let value = msg[expectedValueName];
+ let expected = expectedValues[expectedValueName];
+ if (Array.isArray(expected)) {
+ browser.test.assertTrue(
+ Array.isArray(value),
+ `Value for ${expectedValueName} should be an Array.`
+ );
+ browser.test.assertEq(
+ expected.length,
+ value.length,
+ `Value for ${expectedValueName} should have the correct Array size.`
+ );
+ for (let i = 0; i < expected.length; i++) {
+ browser.test.assertEq(
+ expected[i],
+ value[i],
+ `Value for ${expectedValueName}[${i}] should be correct.`
+ );
+ }
+ } else if (expected instanceof Date) {
+ browser.test.assertTrue(
+ value instanceof Date,
+ `Value for ${expectedValueName} should be a Date.`
+ );
+ browser.test.assertEq(
+ expected.getTime(),
+ value.getTime(),
+ `Date value for ${expectedValueName} should be correct.`
+ );
+ } else {
+ browser.test.assertEq(
+ expected,
+ value,
+ `Value for ${expectedValueName} should be correct.`
+ );
+ }
+ }
+ }
+
+ // Request attachments.
+ let attachments = await browser.messages.listAttachments(message.id);
+ browser.test.assertEq(2, attachments.length);
+ browser.test.assertEq("1.2", attachments[0].partName);
+ browser.test.assertEq("1.3", attachments[1].partName);
+
+ browser.test.assertEq("message1.eml", attachments[0].name);
+ browser.test.assertEq("yellowPixel.png", attachments[1].name);
+
+ // Validate the returned MessageHeader for attached message1.eml.
+ let subMessage = attachments[0].message;
+ browser.test.assertTrue(
+ subMessage.id != message.id,
+ `Id of attached SubMessage (${subMessage.id}) should be different from the id of the outer message (${message.id})`
+ );
+ validateMessage(subMessage, {
+ date: new Date(958606367000),
+ author: "Superman <clark.kent@dailyplanet.com>",
+ recipients: ["Jimmy <jimmy.olsen@dailyplanet.com>"],
+ ccList: [],
+ bccList: [],
+ subject: "Test message 1",
+ new: false,
+ headersOnly: false,
+ flagged: false,
+ junk: false,
+ junkScore: 0,
+ headerMessageId: "sample-attached.eml@mime.sample",
+ size: 0,
+ tags: [],
+ external: true,
+ });
+
+ // Get attachments of sub-message messag1.eml.
+ let subAttachments = await browser.messages.listAttachments(
+ subMessage.id
+ );
+ browser.test.assertEq(4, subAttachments.length);
+ browser.test.assertEq("1.2.1.2", subAttachments[0].partName);
+ browser.test.assertEq("1.2.1.3", subAttachments[1].partName);
+ browser.test.assertEq("1.2.1.4", subAttachments[2].partName);
+ browser.test.assertEq("1.2.1.5", subAttachments[3].partName);
+
+ browser.test.assertEq("whitePixel.png", subAttachments[0].name);
+ browser.test.assertEq("greenPixel.png", subAttachments[1].name);
+ browser.test.assertEq("redPixel.png", subAttachments[2].name);
+ browser.test.assertEq("message2.eml", subAttachments[3].name);
+
+ // Validate the returned MessageHeader for sub-message message2.eml
+ // attached to sub-message message1.eml.
+ let subSubMessage = subAttachments[3].message;
+ browser.test.assertTrue(
+ ![message.id, subMessage.id].includes(subSubMessage.id),
+ `Id of attached SubSubMessage (${subSubMessage.id}) should be different from the id of the outer message (${message.id}) and from the SubMessage (${subMessage.id})`
+ );
+ validateMessage(subSubMessage, {
+ date: new Date(958519967000),
+ author: "Jimmy <jimmy.olsen@dailyplanet.com>",
+ recipients: ["Superman <clark.kent@dailyplanet.com>"],
+ ccList: [],
+ bccList: [],
+ subject: "Test message 2",
+ new: false,
+ headersOnly: false,
+ flagged: false,
+ junk: false,
+ junkScore: 0,
+ headerMessageId: "sample-nested-attached.eml@mime.sample",
+ size: 0,
+ tags: [],
+ external: true,
+ });
+
+ // Test getAttachmentFile().
+ // Note: This function has x-ray vision into sub-messages and can get
+ // any part inside the message, even if - technically - the attachments
+ // belong to subMessages. There is no difference between requesting
+ // part 1.2.1.2 from the main message or from message1.eml (part 1.2).
+ // X-ray vision from a sub-message back into a parent is not allowed.
+ let platform = await browser.runtime.getPlatformInfo();
+ let fileTests = [
+ {
+ partName: "1.2",
+ name: "message1.eml",
+ size:
+ platform.os != "win" &&
+ (account.type == "none" || account.type == "nntp")
+ ? 2517
+ : 2601,
+ text: "Message-ID: <sample-attached.eml@mime.sample>",
+ },
+ {
+ partName: "1.2.1.2",
+ name: "whitePixel.png",
+ size: 69,
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC",
+ },
+ {
+ partName: "1.2.1.3",
+ name: "greenPixel.png",
+ size: 119,
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY+C76AoAAhUBJel4xsMAAAAASUVORK5CYII=",
+ },
+ {
+ partName: "1.2.1.4",
+ name: "redPixel.png",
+ size: 119,
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY+hgkAYAAbcApOp/9LEAAAAASUVORK5CYII=",
+ },
+ {
+ partName: "1.2.1.5",
+ name: "message2.eml",
+ size:
+ platform.os != "win" &&
+ (account.type == "none" || account.type == "nntp")
+ ? 838
+ : 867,
+ text: "Message-ID: <sample-nested-attached.eml@mime.sample>",
+ },
+ {
+ partName: "1.2.1.5.1.2",
+ name: "whitePixel.png",
+ size: 69,
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC",
+ },
+ {
+ partName: "1.3",
+ name: "yellowPixel.png",
+ size: 119,
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY/j/iQEABOUB8pypNlQAAAAASUVORK5CYII=",
+ },
+ ];
+ let testMessages = [
+ {
+ id: message.id,
+ expectedFileCounts: 7,
+ },
+ {
+ id: subMessage.id,
+ subPart: "1.2.",
+ expectedFileCounts: 5,
+ },
+ {
+ id: subSubMessage.id,
+ subPart: "1.2.1.5.",
+ expectedFileCounts: 1,
+ },
+ ];
+ for (let msg of testMessages) {
+ let fileCounts = 0;
+ for (let test of fileTests) {
+ if (msg.subPart && !test.partName.startsWith(msg.subPart)) {
+ await browser.test.assertRejects(
+ browser.messages.getAttachmentFile(msg.id, test.partName),
+ `Part ${test.partName} not found in message ${msg.id}.`,
+ "Sub-message should not be able to get parts from parent message"
+ );
+ continue;
+ }
+ fileCounts++;
+
+ let file = await browser.messages.getAttachmentFile(
+ msg.id,
+ test.partName
+ );
+
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(file instanceof File);
+ browser.test.assertEq(test.name, file.name);
+ browser.test.assertEq(test.size, file.size);
+
+ if (test.text) {
+ browser.test.assertTrue(
+ (await file.text()).startsWith(test.text)
+ );
+ }
+
+ if (test.data) {
+ let reader = new FileReader();
+ let data = await new Promise(resolve => {
+ reader.onload = e => resolve(e.target.result);
+ reader.readAsDataURL(file);
+ });
+ browser.test.assertEq(
+ test.data,
+ data.replaceAll("\r\n", "\n").trim()
+ );
+ }
+ }
+ browser.test.assertEq(
+ msg.expectedFileCounts,
+ fileCounts,
+ "Should have requested to correct amount of attachment files."
+ );
+ }
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_get.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_get.js
new file mode 100644
index 0000000000..2872a2141f
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_get.js
@@ -0,0 +1,1073 @@
+/* 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 { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+
+const OPENPGP_TEST_DIR = do_get_file("../../../../test/browser/openpgp");
+const OPENPGP_KEY_PATH = PathUtils.join(
+ OPENPGP_TEST_DIR.path,
+ "data",
+ "keys",
+ "alice@openpgp.example-0xf231550c4f47e38e-secret.asc"
+);
+
+/**
+ * Test the messages.getRaw and messages.getFull functions. Since each message
+ * is unique and there are minor differences between the account
+ * implementations, we don't compare exactly with a reference message.
+ */
+add_task(async function test_plain_mv2() {
+ let _account = createAccount();
+ let _folder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+ await createMessages(_folder, 1);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+
+ for (let account of accounts) {
+ let folder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(1, messages.length);
+
+ let [message] = messages;
+
+ // Expected message content:
+ // -------------------------
+ // From andy@anway.invalid
+ // Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+ // Subject: Big Meeting Today
+ // From: "Andy Anway" <andy@anway.invalid>
+ // To: "Bob Bell" <bob@bell.invalid>
+ // Message-Id: <0@made.up.invalid>
+ // Date: Wed, 06 Nov 2019 22:37:40 +1300
+ //
+ // Hello Bob Bell!
+ //
+
+ browser.test.assertEq("Big Meeting Today", message.subject);
+ browser.test.assertEq(
+ '"Andy Anway" <andy@anway.invalid>',
+ message.author
+ );
+
+ // The msgHdr of NNTP messages have no recipients.
+ if (account.type != "nntp") {
+ browser.test.assertEq(
+ "Bob Bell <bob@bell.invalid>",
+ message.recipients[0]
+ );
+ }
+
+ let strMessage_1 = await browser.messages.getRaw(message.id);
+ browser.test.assertEq("string", typeof strMessage_1);
+ let strMessage_2 = await browser.messages.getRaw(message.id, {
+ data_format: "BinaryString",
+ });
+ browser.test.assertEq("string", typeof strMessage_2);
+ let fileMessage_3 = await browser.messages.getRaw(message.id, {
+ data_format: "File",
+ });
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(fileMessage_3 instanceof File);
+ // Since we do not have utf-8 chars in the test message, the returned BinaryString is
+ // identical to the return value of File.text().
+ let strMessage_3 = await fileMessage_3.text();
+
+ for (let strMessage of [strMessage_1, strMessage_2, strMessage_3]) {
+ // Fold Windows line-endings \r\n to \n.
+ strMessage = strMessage.replace(/\r/g, "");
+ browser.test.assertTrue(
+ strMessage.includes("Subject: Big Meeting Today\n")
+ );
+ browser.test.assertTrue(
+ strMessage.includes('From: "Andy Anway" <andy@anway.invalid>\n')
+ );
+ browser.test.assertTrue(
+ strMessage.includes('To: "Bob Bell" <bob@bell.invalid>\n')
+ );
+ browser.test.assertTrue(strMessage.includes("Hello Bob Bell!"));
+ }
+
+ // {
+ // "contentType": "message/rfc822",
+ // "headers": {
+ // "content-type": ["text/plain; charset=ISO-8859-1; format=flowed"],
+ // "subject": ["Big Meeting Today"],
+ // "from": ["\"Andy Anway\" <andy@anway.invalid>"],
+ // "to": ["\"Bob Bell\" <bob@bell.invalid>"],
+ // "message-id": ["<0@made.up.invalid>"],
+ // "date": ["Wed, 06 Nov 2019 22:37:40 +1300"]
+ // },
+ // "partName": "",
+ // "size": 17,
+ // "parts": [
+ // {
+ // "body": "Hello Bob Bell!\n\n",
+ // "contentType": "text/plain",
+ // "headers": {
+ // "content-type": ["text/plain; charset=ISO-8859-1; format=flowed"]
+ // },
+ // "partName": "1",
+ // "size": 17
+ // }
+ // ]
+ // }
+
+ let fullMessage = await browser.messages.getFull(message.id);
+ browser.test.log(JSON.stringify(fullMessage));
+ browser.test.assertEq("object", typeof fullMessage);
+ browser.test.assertEq("message/rfc822", fullMessage.contentType);
+
+ browser.test.assertEq("object", typeof fullMessage.headers);
+ for (let header of [
+ "content-type",
+ "date",
+ "from",
+ "message-id",
+ "subject",
+ "to",
+ ]) {
+ browser.test.assertTrue(Array.isArray(fullMessage.headers[header]));
+ browser.test.assertEq(1, fullMessage.headers[header].length);
+ }
+ browser.test.assertEq(
+ "Big Meeting Today",
+ fullMessage.headers.subject[0]
+ );
+ browser.test.assertEq(
+ '"Andy Anway" <andy@anway.invalid>',
+ fullMessage.headers.from[0]
+ );
+ browser.test.assertEq(
+ '"Bob Bell" <bob@bell.invalid>',
+ fullMessage.headers.to[0]
+ );
+
+ browser.test.assertTrue(Array.isArray(fullMessage.parts));
+ browser.test.assertEq(1, fullMessage.parts.length);
+ browser.test.assertEq("object", typeof fullMessage.parts[0]);
+ browser.test.assertEq(
+ "Hello Bob Bell!",
+ fullMessage.parts[0].body.trimRight()
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ manifest: { permissions: ["accountsRead", "messagesRead"] },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+});
+
+add_task(async function test_plain_mv3() {
+ let _account = createAccount();
+ let _folder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+ await createMessages(_folder, 1);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+
+ for (let account of accounts) {
+ let folder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(1, messages.length);
+
+ let [message] = messages;
+
+ // Expected message content:
+ // -------------------------
+ // From chris@clarke.invalid
+ // Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+ // Subject: Small Party Tomorrow
+ // From: "Chris Clarke" <chris@clarke.invalid>
+ // To: "David Davol" <david@davol.invalid>
+ // Message-Id: <1@made.up.invalid>
+ // Date: Tue, 01 Feb 2000 01:00:00 +0100
+ //
+ // Hello David Davol!
+ //
+
+ browser.test.assertEq("Small Party Tomorrow", message.subject);
+ browser.test.assertEq(
+ '"Chris Clarke" <chris@clarke.invalid>',
+ message.author
+ );
+
+ // The msgHdr of NNTP messages have no recipients.
+ if (account.type != "nntp") {
+ browser.test.assertEq(
+ "David Davol <david@davol.invalid>",
+ message.recipients[0]
+ );
+ }
+
+ let fileMessage_1 = await browser.messages.getRaw(message.id);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(fileMessage_1 instanceof File);
+ // Since we do not have utf-8 chars in the test message, the returned
+ // BinaryString is identical to the return value of File.text().
+ let strMessage_1 = await fileMessage_1.text();
+
+ let strMessage_2 = await browser.messages.getRaw(message.id, {
+ data_format: "BinaryString",
+ });
+ browser.test.assertEq("string", typeof strMessage_2);
+
+ let fileMessage_3 = await browser.messages.getRaw(message.id, {
+ data_format: "File",
+ });
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(fileMessage_3 instanceof File);
+ let strMessage_3 = await fileMessage_3.text();
+
+ for (let strMessage of [strMessage_1, strMessage_2, strMessage_3]) {
+ // Fold Windows line-endings \r\n to \n.
+ strMessage = strMessage.replace(/\r/g, "");
+ browser.test.assertTrue(
+ strMessage.includes("Subject: Small Party Tomorrow\n")
+ );
+ browser.test.assertTrue(
+ strMessage.includes('From: "Chris Clarke" <chris@clarke.invalid>\n')
+ );
+ browser.test.assertTrue(
+ strMessage.includes('To: "David Davol" <david@davol.invalid>\n')
+ );
+ browser.test.assertTrue(strMessage.includes("Hello David Davol!"));
+ }
+
+ // {
+ // "contentType": "message/rfc822",
+ // "headers": {
+ // "content-type": ["text/plain; charset=ISO-8859-1; format=flowed"],
+ // "subject": ["Small Party Tomorrow"],
+ // "from": ["\"Chris Clarke\" <chris@clarke.invalid>"],
+ // "to": ["\"David Davol\" <David Davol>"],
+ // "message-id": ["<1@made.up.invalid>"],
+ // "date": ["Tue, 01 Feb 2000 01:00:00 +0100"]
+ // },
+ // "partName": "",
+ // "size": 20,
+ // "parts": [
+ // {
+ // "body": "David Davol!\n\n",
+ // "contentType": "text/plain",
+ // "headers": {
+ // "content-type": ["text/plain; charset=ISO-8859-1; format=flowed"]
+ // },
+ // "partName": "1",
+ // "size": 20
+ // }
+ // ]
+ // }
+
+ let fullMessage = await browser.messages.getFull(message.id);
+ browser.test.log(JSON.stringify(fullMessage));
+ browser.test.assertEq("object", typeof fullMessage);
+ browser.test.assertEq("message/rfc822", fullMessage.contentType);
+
+ browser.test.assertEq("object", typeof fullMessage.headers);
+ for (let header of [
+ "content-type",
+ "date",
+ "from",
+ "message-id",
+ "subject",
+ "to",
+ ]) {
+ browser.test.assertTrue(Array.isArray(fullMessage.headers[header]));
+ browser.test.assertEq(1, fullMessage.headers[header].length);
+ }
+ browser.test.assertEq(
+ "Small Party Tomorrow",
+ fullMessage.headers.subject[0]
+ );
+ browser.test.assertEq(
+ '"Chris Clarke" <chris@clarke.invalid>',
+ fullMessage.headers.from[0]
+ );
+ browser.test.assertEq(
+ '"David Davol" <david@davol.invalid>',
+ fullMessage.headers.to[0]
+ );
+
+ browser.test.assertTrue(Array.isArray(fullMessage.parts));
+ browser.test.assertEq(1, fullMessage.parts.length);
+ browser.test.assertEq("object", typeof fullMessage.parts[0]);
+ browser.test.assertEq(
+ "Hello David Davol!",
+ fullMessage.parts[0].body.trimRight()
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ manifest: {
+ manifest_version: 3,
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+});
+
+/**
+ * Test that mime parsers for all message types retrieve the correctly decoded
+ * headers and bodies. Bodies should no not be returned, if it is an attachment.
+ * Sizes are not checked for.
+ */
+add_task(async function test_encoding() {
+ let _account = createAccount();
+ let _folder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+
+ // Main body with disposition inline, base64 encoded,
+ // subject is UTF-8 encoded word.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample01.eml").path
+ );
+ // A multipart/mixed mime message, to header is iso-8859-1 encoded word,
+ // body is quoted printable with iso-8859-1, attachments with different names
+ // and filenames.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample02.eml").path
+ );
+ // Message with attachment only, From header is iso-8859-1 encoded word.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample03.eml").path
+ );
+ // Message with koi8-r + base64 encoded body, subject is koi8-r encoded word.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample04.eml").path
+ );
+ // Message with windows-1251 + base64 encoded body, subject is windows-1251
+ // encoded word.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample05.eml").path
+ );
+ // Message without plain/text content-type.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample06.eml").path
+ );
+ // A multipart/alternative message without plain/text content-type.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample07.eml").path
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+
+ let expectedData = {
+ "01.eml@mime.sample": {
+ msgHeaders: {
+ subject: "αλφάβητο",
+ author: "Bug Reporter <new@thunderbird.bug>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Bug Reporter <new@thunderbird.bug>"],
+ newsgroups: ["gmane.comp.mozilla.thundebird.user"],
+ subject: ["αλφάβητο"],
+ date: ["Thu, 27 May 2021 21:23:35 +0100"],
+ "message-id": ["<01.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": ["text/plain; charset=utf-8;"],
+ "content-transfer-encoding": ["base64"],
+ "content-disposition": ["inline"],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1",
+ size: 0,
+ body: "Άλφα\n",
+ headers: {
+ "content-type": ["text/plain; charset=utf-8;"],
+ },
+ },
+ ],
+ },
+ },
+ "02.eml@mime.sample": {
+ msgHeaders: {
+ subject: "Test message from Microsoft Outlook 00",
+ author: '"Doug Sauder" <doug@example.com>',
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ['"Doug Sauder" <doug@example.com>'],
+ to: ["Heinz Müller <mueller@example.com>"],
+ subject: ["Test message from Microsoft Outlook 00"],
+ date: ["Wed, 17 May 2000 19:32:47 -0400"],
+ "message-id": ["<02.eml@mime.sample>"],
+ "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"],
+ },
+ parts: [
+ {
+ contentType: "multipart/mixed",
+ partName: "1",
+ size: 0,
+ headers: {
+ "content-type": [
+ 'multipart/mixed; boundary="----=_NextPart_000_0002_01BFC036.AE309650"',
+ ],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1.1",
+ size: 0,
+ body: `\nDie Hasen und die Frösche \n \n`,
+ headers: {
+ "content-type": ['text/plain; charset="iso-8859-1"'],
+ },
+ },
+ {
+ contentType: "image/png",
+ partName: "1.2",
+ size: 0,
+ name: "blueball2.png",
+ headers: {
+ "content-type": ['image/png; name="blueball1.png"'],
+ },
+ },
+ {
+ contentType: "image/png",
+ partName: "1.3",
+ size: 0,
+ name: "greenball.png",
+ headers: {
+ "content-type": ['image/png; name="greenball.png"'],
+ },
+ },
+ {
+ contentType: "image/png",
+ partName: "1.4",
+ size: 0,
+ name: "redball.png",
+ headers: {
+ "content-type": ["image/png"],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ "03.eml@mime.sample": {
+ msgHeaders: {
+ subject: "Test message from Microsoft Outlook 00",
+ author: "Heinz Müller <mueller@example.com>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Heinz Müller <mueller@example.com>"],
+ to: ['"Joe Blow" <jblow@example.com>'],
+ subject: ["Test message from Microsoft Outlook 00"],
+ date: ["Wed, 17 May 2000 19:35:05 -0400"],
+ "message-id": ["<03.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": ['image/png; name="doubelspace ball.png"'],
+ "content-transfer-encoding": ["base64"],
+ "content-disposition": [
+ 'attachment; filename="doubelspace ball.png"',
+ ],
+ "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"],
+ },
+ parts: [
+ {
+ contentType: "image/png",
+ name: "doubelspace ball.png",
+ partName: "1",
+ size: 0,
+ headers: {
+ "content-type": ['image/png; name="doubelspace ball.png"'],
+ },
+ },
+ ],
+ },
+ },
+ "04.eml@mime.sample": {
+ msgHeaders: {
+ subject: "Алфавит",
+ author: "Bug Reporter <new@thunderbird.bug>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Bug Reporter <new@thunderbird.bug>"],
+ newsgroups: ["gmane.comp.mozilla.thundebird.user"],
+ subject: ["Алфавит"],
+ date: ["Sun, 27 May 2001 21:23:35 +0100"],
+ "message-id": ["<04.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": ["text/plain; charset=koi8-r;"],
+ "content-transfer-encoding": ["base64"],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1",
+ size: 0,
+ body: "Вопрос\n",
+ headers: {
+ "content-type": ["text/plain; charset=koi8-r;"],
+ },
+ },
+ ],
+ },
+ },
+ "05.eml@mime.sample": {
+ msgHeaders: {
+ subject: "Алфавит",
+ author: "Bug Reporter <new@thunderbird.bug>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Bug Reporter <new@thunderbird.bug>"],
+ newsgroups: ["gmane.comp.mozilla.thundebird.user"],
+ subject: ["Алфавит"],
+ date: ["Sun, 27 May 2001 21:23:35 +0100"],
+ "message-id": ["<05.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": ["text/plain; charset=windows-1251;"],
+ "content-transfer-encoding": ["base64"],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1",
+ size: 0,
+ body: "Вопрос\n",
+ headers: {
+ "content-type": ["text/plain; charset=windows-1251;"],
+ },
+ },
+ ],
+ },
+ },
+ "06.eml@mime.sample": {
+ msgHeaders: {
+ subject: "I have no content type",
+ author: "Bug Reporter <new@thunderbird.bug>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Bug Reporter <new@thunderbird.bug>"],
+ newsgroups: ["gmane.comp.mozilla.thundebird.user"],
+ subject: ["I have no content type"],
+ date: ["Sun, 27 May 2001 21:23:35 +0100"],
+ "message-id": ["<06.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1",
+ size: 0,
+ body: "No content type\n",
+ headers: {
+ "content-type": ["text/plain"],
+ },
+ },
+ ],
+ },
+ },
+ "07.eml@mime.sample": {
+ msgHeaders: {
+ subject: "Default content-types",
+ author: "Doug Sauder <dwsauder@example.com>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Doug Sauder <dwsauder@example.com>"],
+ to: ["Heinz <mueller@example.com>"],
+ subject: ["Default content-types"],
+ date: ["Fri, 19 May 2000 00:29:55 -0400"],
+ "message-id": ["<07.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": [
+ 'multipart/alternative; boundary="=====================_714967308==_.ALT"',
+ ],
+ },
+ parts: [
+ {
+ contentType: "multipart/alternative",
+ partName: "1",
+ size: 0,
+ headers: {
+ "content-type": [
+ 'multipart/alternative; boundary="=====================_714967308==_.ALT"',
+ ],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1.1",
+ size: 0,
+ body: "Die Hasen\n",
+ headers: {
+ "content-type": ["text/plain"],
+ },
+ },
+ {
+ contentType: "text/html",
+ partName: "1.2",
+ size: 0,
+ body: "<html><body><b>Die Hasen</b></body></html>\n",
+ headers: {
+ "content-type": ["text/html"],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ };
+
+ function checkMsgHeaders(expected, actual) {
+ // Check if all expected properties are there.
+ for (let property of Object.keys(expected)) {
+ browser.test.assertEq(
+ expected.hasOwnProperty(property),
+ actual.hasOwnProperty(property),
+ `expected property ${property} is present`
+ );
+ // Check property content.
+ browser.test.assertEq(
+ expected[property],
+ actual[property],
+ `property ${property} is correct`
+ );
+ }
+ }
+
+ function checkMsgParts(expected, actual) {
+ // Check if all expected properties are there.
+ for (let property of Object.keys(expected)) {
+ browser.test.assertEq(
+ expected.hasOwnProperty(property),
+ actual.hasOwnProperty(property),
+ `expected property ${property} is present`
+ );
+ if (
+ ["parts", "headers", "size"].includes(property) ||
+ (["body"].includes(property) && expected[property] == "")
+ ) {
+ continue;
+ }
+ // Check property content.
+ browser.test.assertEq(
+ JSON.stringify(expected[property].replaceAll("\r\n", "\n")),
+ JSON.stringify(actual[property].replaceAll("\r\n", "\n")),
+ `property ${property} is correct`
+ );
+ }
+
+ // Check for unexpected properties.
+ for (let property of Object.keys(actual)) {
+ browser.test.assertEq(
+ expected.hasOwnProperty(property),
+ actual.hasOwnProperty(property),
+ `property ${property} is expected`
+ );
+ }
+
+ // Check if all expected headers are there.
+ if (expected.headers) {
+ for (let header of Object.keys(expected.headers)) {
+ browser.test.assertEq(
+ expected.headers.hasOwnProperty(header),
+ actual.headers.hasOwnProperty(header),
+ `expected header ${header} is present`
+ );
+ // Check header content.
+ // Note: jsmime does not eat TABs after a CLRF.
+ browser.test.assertEq(
+ expected.headers[header].toString().replaceAll("\t", " "),
+ actual.headers[header].toString().replaceAll("\t", " "),
+ `header ${header} is correct`
+ );
+ }
+ // Check for unexpected headers.
+ for (let header of Object.keys(actual.headers)) {
+ browser.test.assertEq(
+ expected.headers.hasOwnProperty(header),
+ actual.headers.hasOwnProperty(header),
+ `header ${header} is expected`
+ );
+ }
+ }
+
+ // Check sub-parts.
+ browser.test.assertEq(
+ Array.isArray(expected.parts),
+ Array.isArray(actual.parts),
+ `has sub-parts`
+ );
+ if (Array.isArray(expected.parts)) {
+ browser.test.assertEq(
+ expected.parts.length,
+ actual.parts.length,
+ "number of parts"
+ );
+ for (let i in expected.parts) {
+ checkMsgParts(expected.parts[i], actual.parts[i]);
+ }
+ }
+ }
+
+ for (let account of accounts) {
+ let folder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(7, messages.length);
+
+ for (let message of messages) {
+ let fullMessage = await browser.messages.getFull(message.id);
+ browser.test.assertEq("object", typeof fullMessage);
+
+ let expected = expectedData[message.headerMessageId];
+ checkMsgHeaders(expected.msgHeaders, message);
+ checkMsgParts(expected.msgParts, fullMessage);
+ }
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ manifest: { permissions: ["accountsRead", "messagesRead"] },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+});
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_openpgp() {
+ let _account = createAccount();
+ let _identity = addIdentity(_account);
+ let _folder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+
+ // Load an encrypted message.
+
+ let messagePath = PathUtils.join(
+ OPENPGP_TEST_DIR.path,
+ "data",
+ "eml",
+ "unsigned-encrypted-to-0xf231550c4f47e38e-from-0xfbfcc82a015e7330.eml"
+ );
+ await createMessageFromFile(_folder, messagePath);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [account] = await browser.accounts.list();
+ let folder = account.folders.find(f => f.name == "test1");
+
+ // Read the message, without the key set up. The headers should be
+ // readable, but not the message itself.
+
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(1, messages.length);
+
+ let [message] = messages;
+ browser.test.assertEq("...", message.subject);
+ browser.test.assertEq(
+ "Bob Babbage <bob@openpgp.example>",
+ message.author
+ );
+ browser.test.assertEq("alice@openpgp.example", message.recipients[0]);
+
+ let fullMessage = await browser.messages.getFull(message.id);
+ browser.test.log(JSON.stringify(fullMessage));
+ browser.test.assertEq("object", typeof fullMessage);
+ browser.test.assertEq("message/rfc822", fullMessage.contentType);
+
+ browser.test.assertEq("object", typeof fullMessage.headers);
+ for (let header of [
+ "content-type",
+ "date",
+ "from",
+ "message-id",
+ "subject",
+ "to",
+ ]) {
+ browser.test.assertTrue(Array.isArray(fullMessage.headers[header]));
+ browser.test.assertEq(1, fullMessage.headers[header].length);
+ }
+ browser.test.assertEq("...", fullMessage.headers.subject[0]);
+ browser.test.assertEq(
+ "Bob Babbage <bob@openpgp.example>",
+ fullMessage.headers.from[0]
+ );
+ browser.test.assertEq(
+ "alice@openpgp.example",
+ fullMessage.headers.to[0]
+ );
+
+ browser.test.assertTrue(Array.isArray(fullMessage.parts));
+ browser.test.assertEq(1, fullMessage.parts.length);
+
+ let part = fullMessage.parts[0];
+ browser.test.assertEq("object", typeof part);
+ browser.test.assertEq("multipart/encrypted", part.contentType);
+ browser.test.assertEq(undefined, part.parts);
+
+ // Now set up the key and read the message again. It should all be
+ // there this time.
+
+ await window.sendMessage("load key");
+
+ ({ messages } = await browser.messages.list(folder));
+ browser.test.assertEq(1, messages.length);
+ [message] = messages;
+ browser.test.assertEq("...", message.subject);
+ browser.test.assertEq(
+ "Bob Babbage <bob@openpgp.example>",
+ message.author
+ );
+ browser.test.assertEq("alice@openpgp.example", message.recipients[0]);
+
+ fullMessage = await browser.messages.getFull(message.id);
+ browser.test.log(JSON.stringify(fullMessage));
+ browser.test.assertEq("object", typeof fullMessage);
+ browser.test.assertEq("message/rfc822", fullMessage.contentType);
+
+ browser.test.assertEq("object", typeof fullMessage.headers);
+ for (let header of [
+ "content-type",
+ "date",
+ "from",
+ "message-id",
+ "subject",
+ "to",
+ ]) {
+ browser.test.assertTrue(Array.isArray(fullMessage.headers[header]));
+ browser.test.assertEq(1, fullMessage.headers[header].length);
+ }
+ browser.test.assertEq("...", fullMessage.headers.subject[0]);
+ browser.test.assertEq(
+ "Bob Babbage <bob@openpgp.example>",
+ fullMessage.headers.from[0]
+ );
+ browser.test.assertEq(
+ "alice@openpgp.example",
+ fullMessage.headers.to[0]
+ );
+
+ browser.test.assertTrue(Array.isArray(fullMessage.parts));
+ browser.test.assertEq(1, fullMessage.parts.length);
+
+ part = fullMessage.parts[0];
+ browser.test.assertEq("object", typeof part);
+ browser.test.assertEq("multipart/encrypted", part.contentType);
+ browser.test.assertTrue(Array.isArray(part.parts));
+ browser.test.assertEq(1, part.parts.length);
+
+ part = part.parts[0];
+ browser.test.assertEq("object", typeof part);
+ browser.test.assertEq("multipart/fake-container", part.contentType);
+ browser.test.assertTrue(Array.isArray(part.parts));
+ browser.test.assertEq(1, part.parts.length);
+
+ part = part.parts[0];
+ browser.test.assertEq("object", typeof part);
+ browser.test.assertEq("text/plain", part.contentType);
+ browser.test.assertEq(
+ "Sundays are nothing without callaloo.",
+ part.body.trimRight()
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("load key");
+ info(`Adding key from ${OPENPGP_KEY_PATH}`);
+ await OpenPGPTestUtils.initOpenPGP();
+ let [id] = await OpenPGPTestUtils.importPrivateKey(
+ null,
+ new FileUtils.File(OPENPGP_KEY_PATH)
+ );
+ _identity.setUnicharAttribute("openpgp_key_id", id);
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+ }
+);
+
+add_task(async function test_attached_message_with_missing_headers() {
+ let _account = createAccount();
+ let _folder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/attachedMessageWithMissingHeaders.eml").path
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+
+ for (let account of accounts) {
+ let folder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(1, messages.length);
+
+ let msg = messages[0];
+ let attachments = await browser.messages.listAttachments(msg.id);
+ browser.test.assertEq(
+ attachments.length,
+ 1,
+ "Should have found the correct number of attachments"
+ );
+
+ let attachedMessage = attachments[0].message;
+ browser.test.assertTrue(
+ !!attachedMessage,
+ "Should have found an attached message"
+ );
+ browser.test.assertEq(
+ attachedMessage.date.getTime(),
+ 0,
+ "The date should be correct"
+ );
+ browser.test.assertEq(
+ attachedMessage.subject,
+ "",
+ "The subject should be empty"
+ );
+ browser.test.assertEq(
+ attachedMessage.author,
+ "",
+ "The author should be empty"
+ );
+ browser.test.assertEq(
+ attachedMessage.headerMessageId,
+ "sample-attached.eml@mime.sample",
+ "The headerMessageId should be correct"
+ );
+ window.assertDeepEqual(
+ attachedMessage.recipients,
+ [],
+ "The recipients should be correct"
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_id.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_id.js
new file mode 100644
index 0000000000..dac01fa514
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_id.js
@@ -0,0 +1,256 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var subFolders;
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function setup() {
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ subFolders = {
+ test1: await createSubfolder(rootFolder, "test1"),
+ test2: await createSubfolder(rootFolder, "test2"),
+ test3: await createSubfolder(rootFolder, "test3"),
+ attachment: await createSubfolder(rootFolder, "attachment"),
+ };
+ await createMessages(subFolders.test1, 5);
+ let textAttachment = {
+ body: "textAttachment",
+ filename: "test.txt",
+ contentType: "text/plain",
+ };
+ await createMessages(subFolders.attachment, {
+ count: 1,
+ subject: "Msg with text attachment",
+ attachments: [textAttachment],
+ });
+ }
+);
+
+// In this test we'll move and copy some messages around between
+// folders. Every operation should result in the message's id property
+// changing to a never-seen-before value.
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_identifiers() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [{ folders }] = await browser.accounts.list();
+ let testFolder1 = folders.find(f => f.name == "test1");
+ let testFolder2 = folders.find(f => f.name == "test2");
+ let testFolder3 = folders.find(f => f.name == "test3");
+
+ let { messages } = await browser.messages.list(testFolder1);
+ browser.test.assertEq(
+ 5,
+ messages.length,
+ "message count in testFolder1"
+ );
+ browser.test.assertEq(1, messages[0].id);
+ browser.test.assertEq(2, messages[1].id);
+ browser.test.assertEq(3, messages[2].id);
+ browser.test.assertEq(4, messages[3].id);
+ browser.test.assertEq(5, messages[4].id);
+
+ let subjects = messages.map(m => m.subject);
+
+ // Move two messages. We could do this in one operation, but to be
+ // sure of the order, do it in separate operations.
+
+ await browser.messages.move([1], testFolder2);
+ await browser.messages.move([3], testFolder2);
+
+ ({ messages } = await browser.messages.list(testFolder1));
+ browser.test.assertEq(
+ 3,
+ messages.length,
+ "message count in testFolder1"
+ );
+ browser.test.assertEq(2, messages[0].id);
+ browser.test.assertEq(4, messages[1].id);
+ browser.test.assertEq(5, messages[2].id);
+ browser.test.assertEq(subjects[1], messages[0].subject);
+ browser.test.assertEq(subjects[3], messages[1].subject);
+ browser.test.assertEq(subjects[4], messages[2].subject);
+
+ ({ messages } = await browser.messages.list(testFolder2));
+ browser.test.assertEq(
+ 2,
+ messages.length,
+ "message count in testFolder2"
+ );
+ browser.test.assertEq(6, messages[0].id, "new id created");
+ browser.test.assertEq(7, messages[1].id, "new id created");
+ browser.test.assertEq(subjects[0], messages[0].subject);
+ browser.test.assertEq(subjects[2], messages[1].subject);
+
+ // Copy one message.
+
+ await browser.messages.copy([6], testFolder3);
+
+ ({ messages } = await browser.messages.list(testFolder2));
+ browser.test.assertEq(
+ 2,
+ messages.length,
+ "message count in testFolder2"
+ );
+ browser.test.assertEq(6, messages[0].id);
+ browser.test.assertEq(7, messages[1].id);
+ browser.test.assertEq(subjects[0], messages[0].subject);
+ browser.test.assertEq(subjects[2], messages[1].subject);
+
+ ({ messages } = await browser.messages.list(testFolder3));
+ browser.test.assertEq(
+ 1,
+ messages.length,
+ "message count in testFolder3"
+ );
+ browser.test.assertEq(8, messages[0].id, "new id created");
+ browser.test.assertEq(subjects[0], messages[0].subject);
+
+ // Move the copied message back to the previous folder. There should
+ // now be two copies there, each with their own ID.
+
+ await browser.messages.move([8], testFolder2);
+
+ ({ messages } = await browser.messages.list(testFolder2));
+ browser.test.assertEq(
+ 3,
+ messages.length,
+ "message count in testFolder2"
+ );
+ browser.test.assertEq(6, messages[0].id);
+ browser.test.assertEq(7, messages[1].id);
+ browser.test.assertEq(
+ 9,
+ messages[2].id,
+ "new id created, not a duplicate"
+ );
+ browser.test.assertEq(subjects[0], messages[0].subject);
+ browser.test.assertEq(subjects[2], messages[1].subject);
+ browser.test.assertEq(
+ subjects[0],
+ messages[2].subject,
+ "same message as another in this folder"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesMove", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+// In this test we'll remove an attachment from a message and its id property
+// should not change. (Bug 1645595). Test does not work with IMAP test server,
+// which has issues with attachments.
+add_task(
+ {
+ skip_if: () => IS_NNTP || IS_IMAP,
+ },
+ async function test_attachments() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let id;
+
+ browser.test.onMessage.addListener(async () => {
+ // This listener gets called once the attachment has been removed.
+ // Make sure we still get the message and it no longer has the
+ // attachment.
+ let modifiedMessage = await browser.messages.getFull(id);
+ browser.test.assertEq(
+ "Msg with text attachment",
+ modifiedMessage.headers.subject[0]
+ );
+ browser.test.assertEq(
+ "text/x-moz-deleted",
+ modifiedMessage.parts[0].parts[1].contentType
+ );
+ browser.test.assertEq(
+ "Deleted: test.txt",
+ modifiedMessage.parts[0].parts[1].name
+ );
+ browser.test.notifyPass("finished");
+ });
+
+ let [{ folders }] = await browser.accounts.list();
+ let testFolder = folders.find(f => f.name == "attachment");
+ let { messages } = await browser.messages.list(testFolder);
+ browser.test.assertEq(1, messages.length);
+ id = messages[0].id;
+
+ let originalMessage = await browser.messages.getFull(id);
+ browser.test.assertEq(
+ "Msg with text attachment",
+ originalMessage.headers.subject[0]
+ );
+ browser.test.assertEq(
+ "text/plain",
+ originalMessage.parts[0].parts[1].contentType
+ );
+ browser.test.assertEq(
+ "test.txt",
+ originalMessage.parts[0].parts[1].name
+ );
+ browser.test.sendMessage("removeAttachment", id);
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ let observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "attachment-delete-msgkey-changed") {
+ extension.sendMessage();
+ }
+ },
+ };
+ Services.obs.addObserver(observer, "attachment-delete-msgkey-changed");
+
+ extension.onMessage("removeAttachment", () => {
+ let msgHdr = subFolders.attachment.messages.getNext();
+ let msgUri = msgHdr.folder.getUriForMsg(msgHdr);
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+ messenger.detachAttachment(
+ "text/plain",
+ `${msgUri}?part=1.2&filename=test.txt`,
+ "test.txt",
+ msgUri,
+ false /* do not save */,
+ true /* do not ask */
+ );
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_import.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_import.js
new file mode 100644
index 0000000000..c3bef58835
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_import.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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+add_task(async function test_import() {
+ let _account = createAccount();
+ await createSubfolder(_account.incomingServer.rootFolder, "test1");
+ await createSubfolder(_account.incomingServer.rootFolder, "test2");
+ await createSubfolder(_account.incomingServer.rootFolder, "test3");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ async function do_import(expected, file, folder, options) {
+ let msg = await browser.messages.import(file, folder, options);
+ browser.test.assertEq(
+ "alternative.eml@mime.sample",
+ msg.headerMessageId,
+ "should find the correct message after import"
+ );
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(
+ 1,
+ messages.length,
+ "should find the imported message in the destination folder"
+ );
+ for (let [propName, value] of Object.entries(expected)) {
+ window.assertDeepEqual(
+ value,
+ messages[0][propName],
+ `Property ${propName} should be correct`
+ );
+ }
+ }
+
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+ let [account] = accounts;
+ let folder1 = account.folders.find(f => f.name == "test1");
+ let folder2 = account.folders.find(f => f.name == "test2");
+ let folder3 = account.folders.find(f => f.name == "test3");
+ browser.test.assertTrue(folder1, "Test folder should exist");
+ browser.test.assertTrue(folder2, "Test folder should exist");
+ browser.test.assertTrue(folder3, "Test folder should exist");
+
+ let [emlFileContent] = await window.sendMessage(
+ "getFileContent",
+ "messages/alternative.eml"
+ );
+ let file = new File([emlFileContent], "test.eml");
+
+ if (account.type == "nntp" || account.type == "imap") {
+ // nsIMsgCopyService.copyFileMessage() not implemented for NNTP.
+ // offline/online behavior of IMAP nsIMsgCopyService.copyFileMessage()
+ // is too erratic to be supported ATM.
+ await browser.test.assertRejects(
+ browser.messages.import(file, folder1),
+ `browser.messenger.import() is not supported for ${account.type} accounts`,
+ "Should throw for unsupported accounts"
+ );
+ } else {
+ await do_import(
+ {
+ new: false,
+ read: false,
+ flagged: false,
+ },
+ file,
+ folder1
+ );
+ await do_import(
+ {
+ new: true,
+ read: true,
+ flagged: true,
+ tags: ["$label1"],
+ },
+ file,
+ folder2,
+ {
+ new: true,
+ read: true,
+ flagged: true,
+ tags: ["$label1"],
+ }
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "messagesImport"],
+ },
+ });
+
+ extension.onMessage("getFileContent", async path => {
+ let raw = await IOUtils.read(do_get_file(path).path);
+ extension.sendMessage(MailStringUtils.uint8ArrayToByteString(raw));
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_move_copy_delete.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_move_copy_delete.js
new file mode 100644
index 0000000000..81011374e3
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_move_copy_delete.js
@@ -0,0 +1,656 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { ExtensionsUI } = ChromeUtils.import(
+ "resource:///modules/ExtensionsUI.jsm"
+);
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+);
+
+registerCleanupFunction(async () => {
+ // Remove the temporary MozillaMailnews folder, which is not deleted in time when
+ // the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
+ // files in the temp folder.
+ // Note: PathUtils.tempDir points to the system temp folder, which is different.
+ let path = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "MozillaMailnews"
+ );
+ await IOUtils.remove(path, { recursive: true });
+});
+
+// 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;
+
+ browser.messages[_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: "event_page_extension@mochi.test" },
+ },
+ permissions: ["accountsRead", "messagesRead", "messagesMove"],
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "messages", eventName, { primed: false });
+
+ await ext.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listener.
+ assertPersistentListeners(ext, "messages", 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, "messages", eventName, { primed: false });
+
+ await ext.unload();
+ return rv;
+}
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_move_copy_delete() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = {
+ test1: await createSubfolder(rootFolder, "test1"),
+ test2: await createSubfolder(rootFolder, "test2"),
+ test3: await createSubfolder(rootFolder, "test3"),
+ trash: rootFolder.getChildNamed("Trash"),
+ };
+ await createMessages(subFolders.trash, 4);
+ // 4 messages must be created before this line or test_move_copy_delete will break.
+ await createMessages(subFolders.test1, 5);
+
+ let files = {
+ "background.js": async () => {
+ async function capturePrimedEvent(eventName, callback) {
+ let eventPageExtensionReadyPromise = window.waitForMessage();
+ browser.test.sendMessage("capturePrimedEvent", eventName);
+ await eventPageExtensionReadyPromise;
+ let eventPageExtensionFinishedPromise = window.waitForMessage();
+ callback();
+ return eventPageExtensionFinishedPromise;
+ }
+
+ async function checkMessagesInFolder(expectedKeys, folder) {
+ let expectedSubjects = expectedKeys.map(k => messages[k].subject);
+
+ let { messages: actualMessages } = await browser.messages.list(
+ folder
+ );
+ browser.test.log("expect: " + expectedSubjects.sort());
+ browser.test.log(
+ "actual: " + actualMessages.map(m => m.subject).sort()
+ );
+
+ browser.test.assertEq(
+ expectedSubjects.sort().toString(),
+ actualMessages
+ .map(m => m.subject)
+ .sort()
+ .toString(),
+ "Messages on server should be correct"
+ );
+ for (let m of actualMessages) {
+ browser.test.assertTrue(
+ expectedSubjects.includes(m.subject),
+ `${m.subject} at ${m.id}`
+ );
+ messages[m.subject.split(" ")[0]].id = m.id;
+ }
+
+ // Return the messages for convenience.
+ return actualMessages;
+ }
+
+ function newMovePromise(numberOfEventsToCollapse = 1) {
+ return new Promise(resolve => {
+ let seenEvents = 0;
+ let seenSrcMsgs = [];
+ let seenDstMsgs = [];
+ const listener = (srcMsgs, dstMsgs) => {
+ seenEvents++;
+ seenSrcMsgs.push(...srcMsgs.messages);
+ seenDstMsgs.push(...dstMsgs.messages);
+ if (seenEvents == numberOfEventsToCollapse) {
+ browser.messages.onMoved.removeListener(listener);
+ resolve({ srcMsgs: seenSrcMsgs, dstMsgs: seenDstMsgs });
+ }
+ };
+ browser.messages.onMoved.addListener(listener);
+ });
+ }
+
+ function newCopyPromise(numberOfEventsToCollapse = 1) {
+ return new Promise(resolve => {
+ let seenEvents = 0;
+ let seenSrcMsgs = [];
+ let seenDstMsgs = [];
+ const listener = (srcMsgs, dstMsgs) => {
+ seenEvents++;
+ seenSrcMsgs.push(...srcMsgs.messages);
+ seenDstMsgs.push(...dstMsgs.messages);
+ if (seenEvents == numberOfEventsToCollapse) {
+ browser.messages.onCopied.removeListener(listener);
+ resolve({ srcMsgs: seenSrcMsgs, dstMsgs: seenDstMsgs });
+ }
+ };
+ browser.messages.onCopied.addListener(listener);
+ });
+ }
+
+ function newDeletePromise(numberOfEventsToCollapse = 1) {
+ return new Promise(resolve => {
+ let seenEvents = 0;
+ let seenMsgs = [];
+ const listener = msgs => {
+ seenEvents++;
+ seenMsgs.push(...msgs.messages);
+ if (seenEvents == numberOfEventsToCollapse) {
+ browser.messages.onDeleted.removeListener(listener);
+ resolve(seenMsgs);
+ }
+ };
+ browser.messages.onDeleted.addListener(listener);
+ });
+ }
+
+ async function checkEventInformation(
+ infoPromise,
+ expected,
+ messages,
+ dstFolder
+ ) {
+ let eventInfo = await infoPromise;
+ browser.test.assertEq(eventInfo.srcMsgs.length, expected.length);
+ browser.test.assertEq(eventInfo.dstMsgs.length, expected.length);
+ for (let msg of expected) {
+ let idx = eventInfo.srcMsgs.findIndex(
+ e => e.id == messages[msg].id
+ );
+ browser.test.assertEq(
+ eventInfo.srcMsgs[idx].subject,
+ messages[msg].subject
+ );
+ browser.test.assertEq(
+ eventInfo.dstMsgs[idx].subject,
+ messages[msg].subject
+ );
+ browser.test.assertEq(
+ eventInfo.dstMsgs[idx].folder.path,
+ dstFolder.path
+ );
+ }
+ }
+
+ let [accountId] = await window.sendMessage("getAccount");
+ let { folders } = await browser.accounts.get(accountId);
+ let testFolder1 = folders.find(f => f.name == "test1");
+ let testFolder2 = folders.find(f => f.name == "test2");
+ let testFolder3 = folders.find(f => f.name == "test3");
+ let trashFolder = folders.find(f => f.name == "Trash");
+
+ let { messages: folder1Messages } = await browser.messages.list(
+ testFolder1
+ );
+
+ // Since the ID of a message changes when it is moved, track by subject.
+ let messages = {};
+ for (let m of folder1Messages) {
+ messages[m.subject.split(" ")[0]] = { id: m.id, subject: m.subject };
+ }
+
+ // To help with debugging, output the IDs of our five messages.
+ browser.test.log(JSON.stringify(messages)); // Red:1, Green:2, Blue:3, My:4, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Move one message to another folder.");
+ let movePromise = newMovePromise();
+ let primedMoveInfo = await capturePrimedEvent("onMoved", () =>
+ browser.messages.move([messages.Red.id], testFolder2)
+ );
+ window.assertDeepEqual(
+ await movePromise,
+ {
+ srcMsgs: primedMoveInfo[0].messages,
+ dstMsgs: primedMoveInfo[1].messages,
+ },
+ "The primed and non-primed onMoved events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Red"],
+ messages,
+ testFolder2
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+
+ await checkMessagesInFolder(
+ ["Green", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder(["Red"], testFolder2);
+ browser.test.log(JSON.stringify(messages)); // Red:6, Green:2, Blue:3, My:4, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> And back again.");
+ movePromise = newMovePromise();
+ primedMoveInfo = await capturePrimedEvent("onMoved", () =>
+ browser.messages.move([messages.Red.id], testFolder1)
+ );
+ window.assertDeepEqual(
+ await movePromise,
+ {
+ srcMsgs: primedMoveInfo[0].messages,
+ dstMsgs: primedMoveInfo[1].messages,
+ },
+ "The primed and non-primed onMoved events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Red"],
+ messages,
+ testFolder1
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+
+ await checkMessagesInFolder(
+ ["Red", "Green", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder([], testFolder2);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:2, Blue:3, My:4, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Move two messages to another folder.");
+ movePromise = newMovePromise();
+ primedMoveInfo = await capturePrimedEvent("onMoved", () =>
+ browser.messages.move(
+ [messages.Green.id, messages.My.id],
+ testFolder2
+ )
+ );
+ window.assertDeepEqual(
+ await movePromise,
+ {
+ srcMsgs: primedMoveInfo[0].messages,
+ dstMsgs: primedMoveInfo[1].messages,
+ },
+ "The primed and non-primed onMoved events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Green", "My"],
+ messages,
+ testFolder2
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+
+ await checkMessagesInFolder(["Red", "Blue", "Happy"], testFolder1);
+ await checkMessagesInFolder(["Green", "My"], testFolder2);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:8, Blue:3, My:9, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Move one back again: " + messages.My.id);
+ movePromise = newMovePromise();
+ primedMoveInfo = await capturePrimedEvent("onMoved", () =>
+ browser.messages.move([messages.My.id], testFolder1)
+ );
+ window.assertDeepEqual(
+ await movePromise,
+ {
+ srcMsgs: primedMoveInfo[0].messages,
+ dstMsgs: primedMoveInfo[1].messages,
+ },
+ "The primed and non-primed onMoved events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(movePromise, ["My"], messages, testFolder1);
+
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+
+ await checkMessagesInFolder(
+ ["Red", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder(["Green"], testFolder2);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:8, Blue:3, My:10, Happy:5
+
+ browser.test.log("");
+ browser.test.log(
+ " --> Move messages from different folders to a third folder."
+ );
+ // We collapse the two events (one for each source folder).
+ movePromise = newMovePromise(2);
+ await browser.messages.move(
+ [messages.Green.id, messages.My.id],
+ testFolder3
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Green", "My"],
+ messages,
+ testFolder3
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder3.name);
+
+ await checkMessagesInFolder(["Red", "Blue", "Happy"], testFolder1);
+ await checkMessagesInFolder([], testFolder2);
+ await checkMessagesInFolder(["Green", "My"], testFolder3);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:11, Blue:3, My:12, Happy:5
+
+ browser.test.log("");
+ browser.test.log(
+ " --> The following tests should not trigger move events."
+ );
+ let listenerCalls = 0;
+ const listenerFunc = () => {
+ listenerCalls++;
+ };
+ browser.messages.onMoved.addListener(listenerFunc);
+
+ // Move a message to the folder it's already in.
+ await browser.messages.move([messages.Green.id], testFolder3);
+ await checkMessagesInFolder(["Green", "My"], testFolder3);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:11, Blue:3, My:12, Happy:5
+
+ // Move no messages.
+ await browser.messages.move([], testFolder3);
+ await checkMessagesInFolder(["Red", "Blue", "Happy"], testFolder1);
+ await checkMessagesInFolder([], testFolder2);
+ await checkMessagesInFolder(["Green", "My"], testFolder3);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:11, Blue:3, My:12, Happy:5
+
+ // Move a non-existent message.
+ await browser.test.assertRejects(
+ browser.messages.move([9999], testFolder1),
+ /Error moving message/,
+ "something should happen"
+ );
+
+ // Move to a non-existent folder.
+ await browser.test.assertRejects(
+ browser.messages.move([messages.Red.id], {
+ accountId,
+ path: "/missing",
+ }),
+ /Error moving message/,
+ "something should happen"
+ );
+
+ // Check that no move event was triggered.
+ browser.messages.onMoved.removeListener(listenerFunc);
+ browser.test.assertEq(0, listenerCalls);
+
+ browser.test.log("");
+ browser.test.log(
+ " --> Put everything back where it was at the start of the test."
+ );
+ movePromise = newMovePromise();
+ await browser.messages.move(
+ [messages.My.id, messages.Green.id],
+ testFolder1
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Green", "My"],
+ messages,
+ testFolder1
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder3.name);
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+
+ await checkMessagesInFolder(
+ ["Red", "Green", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder([], testFolder2);
+ await checkMessagesInFolder([], testFolder3);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:13, Blue:3, My:14, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Copy one message to another folder.");
+ let copyPromise = newCopyPromise();
+ let primedCopyInfo = await capturePrimedEvent("onCopied", () =>
+ browser.messages.copy([messages.Happy.id], testFolder2)
+ );
+ window.assertDeepEqual(
+ await copyPromise,
+ {
+ srcMsgs: primedCopyInfo[0].messages,
+ dstMsgs: primedCopyInfo[1].messages,
+ },
+ "The primed and non-primed onCopied events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(
+ copyPromise,
+ ["Happy"],
+ messages,
+ testFolder2
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder3.name);
+
+ await checkMessagesInFolder(
+ ["Red", "Green", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ let { messages: folder2Messages } = await browser.messages.list(
+ testFolder2
+ );
+ browser.test.assertEq(1, folder2Messages.length);
+ browser.test.assertEq(
+ messages.Happy.subject,
+ folder2Messages[0].subject
+ );
+ browser.test.assertTrue(folder2Messages[0].id != messages.Happy.id);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:13, Blue:3, My:14, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Delete the copied message.");
+ let deletePromise = newDeletePromise();
+ let primedDeleteLog = await capturePrimedEvent("onDeleted", () =>
+ browser.messages.delete([folder2Messages[0].id], true)
+ );
+ // Check if the delete information is correct.
+ let deleteLog = await deletePromise;
+ window.assertDeepEqual(
+ [
+ {
+ id: null,
+ messages: deleteLog,
+ },
+ ],
+ primedDeleteLog,
+ "The primed and non-primed onDeleted events should return the same values",
+ { strict: true }
+ );
+ browser.test.assertEq(1, deleteLog.length);
+ browser.test.assertEq(folder2Messages[0].id, deleteLog[0].id);
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder3.name);
+
+ // Check if the message was deleted.
+ await checkMessagesInFolder(
+ ["Red", "Green", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder([], testFolder2);
+ await checkMessagesInFolder([], testFolder3);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:13, Blue:3, My:14, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Move a message to the trash.");
+ movePromise = newMovePromise();
+ primedMoveInfo = await capturePrimedEvent("onMoved", () =>
+ browser.messages.move([messages.Green.id], trashFolder)
+ );
+ window.assertDeepEqual(
+ await movePromise,
+ {
+ srcMsgs: primedMoveInfo[0].messages,
+ dstMsgs: primedMoveInfo[1].messages,
+ },
+ "The primed and non-primed onMoved events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Green"],
+ messages,
+ trashFolder
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder3.name);
+
+ await checkMessagesInFolder(
+ ["Red", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder([], testFolder2);
+ await checkMessagesInFolder([], testFolder3);
+
+ let { messages: trashFolderMessages } = await browser.messages.list(
+ trashFolder
+ );
+ browser.test.assertTrue(
+ trashFolderMessages.find(m => m.subject == messages.Green.subject)
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [
+ "accountsRead",
+ "messagesMove",
+ "messagesRead",
+ "messagesDelete",
+ ],
+ browser_specific_settings: {
+ gecko: { id: "messages.move@mochi.test" },
+ },
+ },
+ });
+
+ extension.onMessage("forceServerUpdate", async foldername => {
+ if (IS_IMAP) {
+ let folder = rootFolder
+ .getChildNamed(foldername)
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ folder.updateFolderWithListener(null, listener);
+ await listener.promise;
+
+ // ...and download for offline use.
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ folder.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ }
+ extension.sendMessage();
+ });
+
+ 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);
+ });
+
+ extension.onMessage("getAccount", () => {
+ extension.sendMessage(account.key);
+ });
+
+ // The sync between the IMAP Service and the fake IMAP Server is partially
+ // broken: It is not possible to re-move messages cleanly. The move commands
+ // are send to the server about 500ms after the local operation and the server
+ // will update the local state wrongly.
+ // In this test we enforce a server update after each operation. If this is
+ // still causing intermittent fails, enable the offline mode for this test.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1797764#c24
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(account);
+ await AddonTestUtils.promiseShutdownManager();
+ }
+);
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_onNewMailReceived.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_onNewMailReceived.js
new file mode 100644
index 0000000000..5c8e62872d
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_onNewMailReceived.js
@@ -0,0 +1,153 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+registerCleanupFunction(async () => {
+ // Remove the temporary MozillaMailnews folder, which is not deleted in time when
+ // the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
+ // files in the temp folder.
+ // Note: PathUtils.tempDir points to the system temp folder, which is different.
+ let path = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "MozillaMailnews"
+ );
+ await IOUtils.remove(path, { recursive: true });
+});
+
+// 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;
+
+ browser.messages[_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: "event_page_extension@mochi.test" },
+ },
+ permissions: ["accountsRead", "messagesRead", "messagesMove"],
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "messages", eventName, { primed: false });
+
+ await ext.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listener.
+ assertPersistentListeners(ext, "messages", 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, "messages", eventName, { primed: false });
+
+ await ext.unload();
+ return rv;
+}
+
+add_task(async function () {
+ await AddonTestUtils.promiseStartupManager();
+
+ let account = createAccount();
+ let inbox = await createSubfolder(account.incomingServer.rootFolder, "test1");
+
+ let files = {
+ "background.js": async () => {
+ browser.messages.onNewMailReceived.addListener((folder, messageList) => {
+ window.assertDeepEqual(
+ { accountId: "account1", name: "test1", path: "/test1" },
+ folder
+ );
+ browser.test.sendMessage("onNewMailReceived event received", [
+ folder,
+ messageList,
+ ]);
+ });
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+
+ // Create a new message.
+
+ await createMessages(inbox, 1);
+ inbox.hasNewMessages = true;
+ inbox.setNumNewMessages(1);
+ inbox.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
+
+ let inboxMessages = [...inbox.messages];
+ let newMessages = await extension.awaitMessage(
+ "onNewMailReceived event received"
+ );
+ equal(newMessages[1].messages.length, 1);
+ equal(newMessages[1].messages[0].subject, inboxMessages[0].subject);
+
+ // Create 2 more new messages.
+
+ let primedOnNewMailReceivedEventData = await event_page_extension(
+ "onNewMailReceived",
+ async () => {
+ await createMessages(inbox, 2);
+ inbox.hasNewMessages = true;
+ inbox.setNumNewMessages(2);
+ inbox.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
+ }
+ );
+
+ inboxMessages = [...inbox.messages];
+ newMessages = await extension.awaitMessage(
+ "onNewMailReceived event received"
+ );
+ Assert.deepEqual(
+ primedOnNewMailReceivedEventData,
+ newMessages,
+ "The primed and non-primed onNewMailReceived events should return the same values"
+ );
+ equal(newMessages[1].messages.length, 2);
+ equal(newMessages[1].messages[0].subject, inboxMessages[1].subject);
+ equal(newMessages[1].messages[1].subject, inboxMessages[2].subject);
+
+ await extension.unload();
+
+ cleanUpAccount(account);
+ await AddonTestUtils.promiseShutdownManager();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_query.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_query.js
new file mode 100644
index 0000000000..9d9e5d8595
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_query.js
@@ -0,0 +1,333 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(async function test_query() {
+ let account = createAccount();
+
+ let textAttachment = {
+ body: "textAttachment",
+ filename: "test.txt",
+ contentType: "text/plain",
+ };
+
+ let subFolders = {
+ test1: await createSubfolder(account.incomingServer.rootFolder, "test1"),
+ test2: await createSubfolder(account.incomingServer.rootFolder, "test2"),
+ };
+ await createMessages(subFolders.test1, { count: 9, age_incr: { days: 2 } });
+
+ let messages = [...subFolders.test1.messages];
+ // NB: Here, the messages are zero-indexed. In the test they're one-indexed.
+ subFolders.test1.markMessagesRead([messages[0]], true);
+ subFolders.test1.markMessagesFlagged([messages[1]], true);
+ subFolders.test1.markMessagesFlagged([messages[6]], true);
+
+ subFolders.test1.addKeywordsToMessages(messages.slice(0, 1), "notATag");
+ subFolders.test1.addKeywordsToMessages(messages.slice(2, 4), "$label2");
+ subFolders.test1.addKeywordsToMessages(messages.slice(3, 6), "$label3");
+
+ addIdentity(account, messages[5].author.replace(/.*<(.*)>/, "$1"));
+ // No recipient support for NNTP.
+ if (account.incomingServer.type != "nntp") {
+ addIdentity(account, messages[2].recipients.replace(/.*<(.*)>/, "$1"));
+ }
+
+ await createMessages(subFolders.test2, { count: 7, age_incr: { days: 2 } });
+ // Email with multipart/alternative.
+ await createMessageFromFile(
+ subFolders.test2,
+ do_get_file("messages/alternative.eml").path
+ );
+
+ await createMessages(subFolders.test2, {
+ count: 1,
+ subject: "1 text attachment",
+ attachments: [textAttachment],
+ });
+
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+ let _account = await browser.accounts.get(accountId);
+ let accountType = _account.type;
+
+ let messages1 = await browser.messages.list({
+ accountId,
+ path: "/test1",
+ });
+ browser.test.assertEq(9, messages1.messages.length);
+ let messages2 = await browser.messages.list({
+ accountId,
+ path: "/test2",
+ });
+ browser.test.assertEq(9, messages2.messages.length);
+
+ // Check all messages are returned.
+ let { messages: allMessages } = await browser.messages.query({});
+ browser.test.assertEq(18, allMessages.length);
+
+ let folder1 = { accountId, path: "/test1" };
+ let folder2 = { accountId, path: "/test2" };
+ let rootFolder = { accountId, path: "/" };
+
+ // Query messages from test1. No messages from test2 should be returned.
+ // We'll use these messages as a reference for further tests.
+ let { messages: referenceMessages } = await browser.messages.query({
+ folder: folder1,
+ });
+ browser.test.assertEq(9, referenceMessages.length);
+ browser.test.assertTrue(
+ referenceMessages.every(m => m.folder.path == "/test1")
+ );
+
+ // Test includeSubFolders: Default (False).
+ let { messages: searchRecursiveDefault } = await browser.messages.query({
+ folder: rootFolder,
+ });
+ browser.test.assertEq(
+ 0,
+ searchRecursiveDefault.length,
+ "includeSubFolders: Default"
+ );
+
+ // Test includeSubFolders: True.
+ let { messages: searchRecursiveTrue } = await browser.messages.query({
+ folder: rootFolder,
+ includeSubFolders: true,
+ });
+ browser.test.assertEq(
+ 18,
+ searchRecursiveTrue.length,
+ "includeSubFolders: True"
+ );
+
+ // Test includeSubFolders: False.
+ let { messages: searchRecursiveFalse } = await browser.messages.query({
+ folder: rootFolder,
+ includeSubFolders: false,
+ });
+ browser.test.assertEq(
+ 0,
+ searchRecursiveFalse.length,
+ "includeSubFolders: False"
+ );
+
+ // Test attachment query: False.
+ let { messages: searchAttachmentFalse } = await browser.messages.query({
+ attachment: false,
+ includeSubFolders: true,
+ });
+ browser.test.assertEq(
+ 17,
+ searchAttachmentFalse.length,
+ "attachment: False"
+ );
+
+ // Test attachment query: True.
+ let { messages: searchAttachmentTrue } = await browser.messages.query({
+ attachment: true,
+ includeSubFolders: true,
+ });
+ browser.test.assertEq(1, searchAttachmentTrue.length, "attachment: True");
+
+ // Dump the reference messages to the console for easier debugging.
+ browser.test.log("Reference messages:");
+ for (let m of referenceMessages) {
+ let date = m.date.toISOString().substring(0, 10);
+ let author = m.author.replace(/"(.*)".*/, "$1").padEnd(16, " ");
+ // No recipient support for NNTP.
+ let recipients =
+ accountType == "nntp"
+ ? ""
+ : m.recipients[0].replace(/(.*) <.*>/, "$1").padEnd(16, " ");
+ browser.test.log(
+ `[${m.id}] ${date} From: ${author} To: ${recipients} Subject: ${m.subject}`
+ );
+ }
+
+ let subtest = async function (queryInfo, ...expectedMessageIndices) {
+ if (!queryInfo.folder) {
+ queryInfo.folder = folder1;
+ }
+ browser.test.log("Testing " + JSON.stringify(queryInfo));
+ let { messages: actualMessages } = await browser.messages.query(
+ queryInfo
+ );
+
+ browser.test.assertEq(
+ expectedMessageIndices.length,
+ actualMessages.length,
+ "Correct number of messages"
+ );
+ for (let index of expectedMessageIndices) {
+ // browser.test.log(`Looking for message ${index}`);
+ if (!actualMessages.some(am => am.id == index)) {
+ browser.test.fail(`Message ${index} was not returned`);
+ browser.test.log(
+ "These messages were returned: " + actualMessages.map(am => am.id)
+ );
+ }
+ }
+ };
+
+ // Date range query. The messages are 0 days old, 2 days old, 4 days old, etc..
+ let today = new Date();
+ let date1 = new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate() - 5
+ );
+ let date2 = new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate() - 11
+ );
+ await subtest({ fromDate: today });
+ await subtest({ fromDate: date1 }, 1, 2, 3);
+ await subtest({ fromDate: date2 }, 1, 2, 3, 4, 5, 6);
+ await subtest({ toDate: date1 }, 4, 5, 6, 7, 8, 9);
+ await subtest({ toDate: date2 }, 7, 8, 9);
+ await subtest({ fromDate: date1, toDate: date2 });
+ await subtest({ fromDate: date2, toDate: date1 }, 4, 5, 6);
+
+ // Unread query. Only message 1 has been read.
+ await subtest({ unread: false }, 1);
+ await subtest({ unread: true }, 2, 3, 4, 5, 6, 7, 8, 9);
+
+ // Flagged query. Messages 2 and 7 are flagged.
+ await subtest({ flagged: true }, 2, 7);
+ await subtest({ flagged: false }, 1, 3, 4, 5, 6, 8, 9);
+
+ // Subject query.
+ let keyword = referenceMessages[1].subject.split(" ")[1];
+ await subtest({ subject: keyword }, 2);
+ await subtest({ fullText: keyword }, 2);
+
+ // Author query.
+ keyword = referenceMessages[2].author.replace('"', "").split(" ")[0];
+ await subtest({ author: keyword }, 3);
+ await subtest({ fullText: keyword }, 3);
+
+ // Recipients query.
+ // No recipient support for NNTP.
+ if (accountType != "nntp") {
+ keyword = referenceMessages[7].recipients[0].split(" ")[0];
+ await subtest({ recipients: keyword }, 8);
+ await subtest({ fullText: keyword }, 8);
+ await subtest({ body: keyword }, 8);
+ }
+
+ // From Me and To Me. These use the identities added to account.
+ await subtest({ fromMe: true }, 6);
+ // No recipient support for NNTP.
+ if (accountType != "nntp") {
+ await subtest({ toMe: true }, 3);
+ }
+
+ // Tags query.
+ await subtest({ tags: { mode: "any", tags: { notATag: true } } });
+ await subtest({ tags: { mode: "any", tags: { $label2: true } } }, 3, 4);
+ await subtest(
+ { tags: { mode: "any", tags: { $label3: true } } },
+ 4,
+ 5,
+ 6
+ );
+ await subtest(
+ { tags: { mode: "any", tags: { $label2: true, $label3: true } } },
+ 3,
+ 4,
+ 5,
+ 6
+ );
+ await subtest({
+ tags: { mode: "all", tags: { $label1: true, $label2: true } },
+ });
+ await subtest(
+ { tags: { mode: "all", tags: { $label2: true, $label3: true } } },
+ 4
+ );
+ await subtest(
+ { tags: { mode: "any", tags: { $label2: false, $label3: false } } },
+ 1,
+ 2,
+ 7,
+ 8,
+ 9
+ );
+ await subtest(
+ { tags: { mode: "all", tags: { $label2: false, $label3: false } } },
+ 1,
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ );
+
+ // headerMessageId query
+ await subtest({ headerMessageId: "0@made.up.invalid" }, 1);
+ await subtest({ headerMessageId: "7@made.up.invalid" }, 8);
+ await subtest({ headerMessageId: "8@made.up.invalid" }, 9);
+ await subtest({ headerMessageId: "unknown@made.up.invalid" });
+
+ // attachment query
+ await subtest({ folder: folder2, attachment: true }, 18);
+
+ // text in nested html part of multipart/alternative
+ await subtest({ folder: folder2, body: "I am HTML!" }, 17);
+
+ // No recipient support for NNTP.
+ if (accountType != "nntp") {
+ // advanced search on recipients
+ await subtest({ folder: folder2, recipients: "karl; heinz" }, 17);
+ await subtest(
+ { folder: folder2, recipients: "<friedrich@example.COM>; HEINZ" },
+ 17
+ );
+ await subtest(
+ {
+ folder: folder2,
+ recipients: "karl <friedrich@example.COM>; HEINZ",
+ },
+ 17
+ );
+ await subtest({
+ folder: folder2,
+ recipients: "Heinz <friedrich@example.COM>; Karl",
+ });
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+registerCleanupFunction(() => {
+ // Make sure any open address book database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_update.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_update.js
new file mode 100644
index 0000000000..4771f3ee17
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_update.js
@@ -0,0 +1,415 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { ExtensionsUI } = ChromeUtils.import(
+ "resource:///modules/ExtensionsUI.jsm"
+);
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+registerCleanupFunction(async () => {
+ // Remove the temporary MozillaMailnews folder, which is not deleted in time when
+ // the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
+ // files in the temp folder.
+ // Note: PathUtils.tempDir points to the system temp folder, which is different.
+ let path = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "MozillaMailnews"
+ );
+ await IOUtils.remove(path, { recursive: true });
+});
+
+// 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;
+
+ browser.messages[_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: "event_page_extension@mochi.test" },
+ },
+ permissions: ["accountsRead", "messagesRead", "messagesMove"],
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "messages", eventName, { primed: false });
+
+ await ext.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listener.
+ assertPersistentListeners(ext, "messages", 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, "messages", eventName, { primed: false });
+
+ await ext.unload();
+ return rv;
+}
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_update() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let testFolder0 = await createSubfolder(rootFolder, "test0");
+ await createMessages(testFolder0, 1);
+ testFolder0.addKeywordsToMessages(
+ [[...testFolder0.messages][0]],
+ "testkeyword"
+ );
+
+ let files = {
+ "background.js": async () => {
+ async function capturePrimedEvent(eventName, callback) {
+ let eventPageExtensionReadyPromise = window.waitForMessage();
+ browser.test.sendMessage("capturePrimedEvent", eventName);
+ await eventPageExtensionReadyPromise;
+ let eventPageExtensionFinishedPromise = window.waitForMessage();
+ callback();
+ return eventPageExtensionFinishedPromise;
+ }
+
+ function newUpdatePromise(numberOfEventsToCollapse = 1) {
+ return new Promise(resolve => {
+ let seenEvents = {};
+ const listener = (msg, props) => {
+ if (!seenEvents.hasOwnProperty(msg.id)) {
+ seenEvents[msg.id] = {
+ counts: 0,
+ props: {},
+ };
+ }
+
+ seenEvents[msg.id].counts++;
+ for (let prop of Object.keys(props)) {
+ seenEvents[msg.id].props[prop] = props[prop];
+ }
+
+ if (seenEvents[msg.id].counts == numberOfEventsToCollapse) {
+ browser.messages.onUpdated.removeListener(listener);
+ resolve({ msg, props: seenEvents[msg.id].props });
+ }
+ };
+ browser.messages.onUpdated.addListener(listener);
+ });
+ }
+ let tags = await browser.messages.listTags();
+ let [data] = await window.sendMessage("getFolder");
+ let messageList = await browser.messages.list(data.folder);
+ browser.test.assertEq(1, messageList.messages.length);
+ let message = messageList.messages[0];
+ browser.test.assertFalse(message.flagged);
+ browser.test.assertFalse(message.read);
+ browser.test.assertFalse(message.junk);
+ browser.test.assertEq(0, message.junkScore);
+ browser.test.assertEq(0, message.tags.length);
+ browser.test.assertEq(data.size, message.size);
+ browser.test.assertEq("0@made.up.invalid", message.headerMessageId);
+
+ // Test that setting flagged works.
+ let updatePromise = newUpdatePromise();
+ let primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
+ browser.messages.update(message.id, { flagged: true })
+ );
+ let updateInfo = await updatePromise;
+ window.assertDeepEqual(
+ [updateInfo.msg, updateInfo.props],
+ primedUpdatedInfo,
+ "The primed and non-primed onUpdated events should return the same values",
+ { strict: true }
+ );
+ browser.test.assertEq(message.id, updateInfo.msg.id);
+ window.assertDeepEqual({ flagged: true }, updateInfo.props);
+ await window.sendMessage("flagged");
+
+ // Test that setting read works.
+ updatePromise = newUpdatePromise();
+ primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
+ browser.messages.update(message.id, { read: true })
+ );
+ updateInfo = await updatePromise;
+ window.assertDeepEqual(
+ [updateInfo.msg, updateInfo.props],
+ primedUpdatedInfo,
+ "The primed and non-primed onUpdated events should return the same values",
+ { strict: true }
+ );
+ browser.test.assertEq(message.id, updateInfo.msg.id);
+ window.assertDeepEqual({ read: true }, updateInfo.props);
+ await window.sendMessage("read");
+
+ // Test that setting junk works.
+ updatePromise = newUpdatePromise();
+ primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
+ browser.messages.update(message.id, { junk: true })
+ );
+ updateInfo = await updatePromise;
+ window.assertDeepEqual(
+ [updateInfo.msg, updateInfo.props],
+ primedUpdatedInfo,
+ "The primed and non-primed onUpdated events should return the same values",
+ { strict: true }
+ );
+ browser.test.assertEq(message.id, updateInfo.msg.id);
+ window.assertDeepEqual({ junk: true }, updateInfo.props);
+ await window.sendMessage("junk");
+
+ // Test that setting one tag works.
+ updatePromise = newUpdatePromise();
+ primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
+ browser.messages.update(message.id, { tags: [tags[0].key] })
+ );
+ updateInfo = await updatePromise;
+ window.assertDeepEqual(
+ [updateInfo.msg, updateInfo.props],
+ primedUpdatedInfo,
+ "The primed and non-primed onUpdated events should return the same values",
+ { strict: true }
+ );
+ browser.test.assertEq(message.id, updateInfo.msg.id);
+ window.assertDeepEqual({ tags: [tags[0].key] }, updateInfo.props);
+ await window.sendMessage("tags1");
+
+ // Test that setting two tags works. We get 3 events: one removing tags0,
+ // one adding tags1 and one adding tags2. updatePromise is waiting for
+ // the third one before resolving.
+ updatePromise = newUpdatePromise(3);
+ await browser.messages.update(message.id, {
+ tags: [tags[1].key, tags[2].key],
+ });
+ updateInfo = await updatePromise;
+ browser.test.assertEq(message.id, updateInfo.msg.id);
+ window.assertDeepEqual(
+ { tags: [tags[1].key, tags[2].key] },
+ updateInfo.props
+ );
+ await window.sendMessage("tags2");
+
+ // Test that unspecified properties aren't changed.
+ let listenerCalls = 0;
+ const listenerFunc = (msg, props) => {
+ listenerCalls++;
+ };
+ browser.messages.onUpdated.addListener(listenerFunc);
+ await browser.messages.update(message.id, {});
+ await window.sendMessage("empty");
+ // Check if the no-op update call triggered a listener.
+ await new Promise(resolve => setTimeout(resolve));
+ browser.messages.onUpdated.removeListener(listenerFunc);
+ browser.test.assertEq(
+ 0,
+ listenerCalls,
+ "Not expecting listener callbacks on no-op updates."
+ );
+
+ message = await browser.messages.get(message.id);
+ browser.test.assertTrue(message.flagged);
+ browser.test.assertTrue(message.read);
+ browser.test.assertTrue(message.junk);
+ browser.test.assertEq(100, message.junkScore);
+ browser.test.assertEq(2, message.tags.length);
+ browser.test.assertEq(tags[1].key, message.tags[0]);
+ browser.test.assertEq(tags[2].key, message.tags[1]);
+ browser.test.assertEq("0@made.up.invalid", message.headerMessageId);
+
+ // Test that clearing properties works.
+ updatePromise = newUpdatePromise(5);
+ await browser.messages.update(message.id, {
+ flagged: false,
+ read: false,
+ junk: false,
+ tags: [],
+ });
+ updateInfo = await updatePromise;
+ window.assertDeepEqual(
+ {
+ flagged: false,
+ read: false,
+ junk: false,
+ tags: [],
+ },
+ updateInfo.props
+ );
+ await window.sendMessage("clear");
+
+ message = await browser.messages.get(message.id);
+ browser.test.assertFalse(message.flagged);
+ browser.test.assertFalse(message.read);
+ browser.test.assertFalse(message.external);
+ browser.test.assertFalse(message.junk);
+ browser.test.assertEq(0, message.junkScore);
+ browser.test.assertEq(0, message.tags.length);
+ browser.test.assertEq("0@made.up.invalid", message.headerMessageId);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ browser_specific_settings: {
+ gecko: { id: "messages.update@mochi.test" },
+ },
+ },
+ });
+
+ let message = [...testFolder0.messages][0];
+ ok(!message.isFlagged);
+ ok(!message.isRead);
+ equal(message.getStringProperty("keywords"), "testkeyword");
+
+ 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);
+ });
+
+ extension.onMessage("flagged", async () => {
+ await TestUtils.waitForCondition(() => message.isFlagged);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("read", async () => {
+ await TestUtils.waitForCondition(() => message.isRead);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("junk", async () => {
+ await TestUtils.waitForCondition(
+ () => message.getStringProperty("junkscore") == 100
+ );
+ extension.sendMessage();
+ });
+
+ extension.onMessage("tags1", async () => {
+ if (IS_IMAP) {
+ // Only IMAP sets the junk/nonjunk keyword.
+ await TestUtils.waitForCondition(
+ () =>
+ message.getStringProperty("keywords") == "testkeyword junk $label1"
+ );
+ } else {
+ await TestUtils.waitForCondition(
+ () => message.getStringProperty("keywords") == "testkeyword $label1"
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("tags2", async () => {
+ if (IS_IMAP) {
+ await TestUtils.waitForCondition(
+ () =>
+ message.getStringProperty("keywords") ==
+ "testkeyword junk $label2 $label3"
+ );
+ } else {
+ await TestUtils.waitForCondition(
+ () =>
+ message.getStringProperty("keywords") ==
+ "testkeyword $label2 $label3"
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("empty", async () => {
+ await TestUtils.waitForCondition(() => message.isFlagged);
+ await TestUtils.waitForCondition(() => message.isRead);
+ if (IS_IMAP) {
+ await TestUtils.waitForCondition(
+ () =>
+ message.getStringProperty("keywords") ==
+ "testkeyword junk $label2 $label3"
+ );
+ } else {
+ await TestUtils.waitForCondition(
+ () =>
+ message.getStringProperty("keywords") ==
+ "testkeyword $label2 $label3"
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("clear", async () => {
+ await TestUtils.waitForCondition(() => !message.isFlagged);
+ await TestUtils.waitForCondition(() => !message.isRead);
+ await TestUtils.waitForCondition(
+ () => message.getStringProperty("junkscore") == 0
+ );
+ if (IS_IMAP) {
+ await TestUtils.waitForCondition(
+ () => message.getStringProperty("keywords") == "testkeyword nonjunk"
+ );
+ } else {
+ await TestUtils.waitForCondition(
+ () => message.getStringProperty("keywords") == "testkeyword"
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("getFolder", async () => {
+ extension.sendMessage({
+ folder: { accountId: account.key, path: "/test0" },
+ size: message.messageSize,
+ });
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(account);
+ await AddonTestUtils.promiseShutdownManager();
+ }
+);
diff --git a/comm/mail/components/extensions/test/xpcshell/xpcshell-imap.ini b/comm/mail/components/extensions/test/xpcshell/xpcshell-imap.ini
new file mode 100644
index 0000000000..88659a20ad
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/xpcshell-imap.ini
@@ -0,0 +1,7 @@
+[default]
+dupe-manifest = true
+head = head.js head-imap.js
+support-files = data/utils.js
+tags = imap webextensions
+
+[include:xpcshell.ini]
diff --git a/comm/mail/components/extensions/test/xpcshell/xpcshell-local.ini b/comm/mail/components/extensions/test/xpcshell/xpcshell-local.ini
new file mode 100644
index 0000000000..19d50044cd
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/xpcshell-local.ini
@@ -0,0 +1,23 @@
+[default]
+dupe-manifest = true
+head = head.js
+support-files = data/utils.js
+tags = local webextensions
+
+[include:xpcshell.ini]
+[test_ext_accounts.js]
+[test_ext_accounts_mv3_event_pages.js]
+[test_ext_identities_mv3_event_pages.js]
+[test_ext_addressBook.js]
+support-files = images/**
+tags = addrbook
+[test_ext_addressBook_readonly.js]
+tags = addrbook
+[test_ext_addressBook_remote.js]
+tags = addrbook
+[test_ext_addressBook_provider.js]
+tags = addrbook
+[test_ext_addressBook_quickSearch.js]
+tags = addrbook
+[test_ext_alias.js]
+[test_ext_browserAction_unifiedtoolbar_restart.js]
diff --git a/comm/mail/components/extensions/test/xpcshell/xpcshell-nntp.ini b/comm/mail/components/extensions/test/xpcshell/xpcshell-nntp.ini
new file mode 100644
index 0000000000..66e23e03bd
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/xpcshell-nntp.ini
@@ -0,0 +1,7 @@
+[default]
+dupe-manifest = true
+head = head.js head-nntp.js
+support-files = data/utils.js
+tags = nntp webextensions
+
+[include:xpcshell.ini]
diff --git a/comm/mail/components/extensions/test/xpcshell/xpcshell.ini b/comm/mail/components/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..666a67d5da
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/xpcshell.ini
@@ -0,0 +1,17 @@
+[test_ext_experiments.js]
+tags = addrbook
+[test_ext_folders.js] # NNTP disabled (no support for folder operations).
+[test_ext_folders_mv3_event_pages.js] # NNTP disabled (no support for folder operations).
+[test_ext_messages.js] # NNTP disabled (no support for Trash folder).
+[test_ext_messages_attachments.js] # IMAP disabled (doesn't work with test server).
+support-files = messages/**
+[test_ext_messages_get.js] # NNTP disabled for PGP tests.
+support-files = messages/**
+[test_ext_messages_id.js] # NNTP disabled (message move not supported).
+[test_ext_messages_import.js]
+support-files = messages/**
+[test_ext_messages_move_copy_delete.js] # NNTP disabled (no support for Trash folder).
+[test_ext_messages_onNewMailReceived.js]
+[test_ext_messages_query.js]
+support-files = messages/alternative.eml
+[test_ext_messages_update.js] # NNTP disabled (no support for Trash folder).