From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../extensions/test/browser/.eslintrc.js | 7 + .../components/extensions/test/browser/browser.ini | 135 ++ .../test/browser/browser_ext_addressBooksUI.js | 116 + .../browser_ext_browserAction_customized.js | 29 + .../browser_ext_browserAction_not_customized.js | 17 + .../browser_ext_browserAction_popup_click.js | 399 ++++ ...xt_browserAction_popup_click_mv3_event_pages.js | 82 + .../browser_ext_browserAction_properties.js | 348 +++ .../test/browser/browser_ext_bug1812530.js | 200 ++ .../test/browser/browser_ext_clickHandler.js | 614 ++++++ .../test/browser/browser_ext_cloudFile.js | 1444 +++++++++++++ .../browser_ext_commands_execute_browser_action.js | 226 ++ .../browser_ext_commands_execute_compose_action.js | 138 ++ ..._ext_commands_execute_message_display_action.js | 168 ++ .../test/browser/browser_ext_commands_getAll.js | 142 ++ .../test/browser/browser_ext_commands_onChanged.js | 59 + .../test/browser/browser_ext_commands_onCommand.js | 577 +++++ .../browser_ext_commands_onCommand_bug1845236.js | 74 + .../test/browser/browser_ext_commands_update.js | 357 +++ .../test/browser/browser_ext_composeAction.js | 268 +++ .../browser_ext_composeAction_popup_click.js | 266 +++ ...xt_composeAction_popup_click_mv3_event_pages.js | 52 + .../browser_ext_composeAction_properties.js | 125 ++ .../test/browser/browser_ext_composeScripts.js | 531 +++++ .../browser/browser_ext_compose_attachments.js | 2268 ++++++++++++++++++++ .../browser_ext_compose_begin_attachments.js | 116 + .../test/browser/browser_ext_compose_begin_body.js | 397 ++++ .../browser_ext_compose_begin_bug1691254.js | 141 ++ .../browser/browser_ext_compose_begin_forward.js | 339 +++ .../browser/browser_ext_compose_begin_headers.js | 178 ++ .../browser/browser_ext_compose_begin_identity.js | 102 + .../test/browser/browser_ext_compose_begin_new.js | 136 ++ .../browser/browser_ext_compose_begin_reply.js | 146 ++ .../test/browser/browser_ext_compose_bug1692439.js | 160 ++ .../test/browser/browser_ext_compose_bug1804796.js | 80 + .../test/browser/browser_ext_compose_details.js | 725 +++++++ .../browser/browser_ext_compose_details_body.js | 469 ++++ .../browser/browser_ext_compose_details_headers.js | 727 +++++++ .../browser/browser_ext_compose_dictionaries.js | 214 ++ .../browser/browser_ext_compose_onBeforeSend.js | 1010 +++++++++ .../test/browser/browser_ext_compose_saveDraft.js | 416 ++++ .../browser/browser_ext_compose_saveTemplate.js | 432 ++++ .../browser/browser_ext_compose_sendMessage.js | 733 +++++++ .../test/browser/browser_ext_contentScripts.js | 438 ++++ .../test/browser/browser_ext_content_handler.js | 334 +++ .../browser_ext_content_tabs_navigation_menu.js | 250 +++ .../test/browser/browser_ext_mailTabs.js | 898 ++++++++ .../test/browser/browser_ext_mailTabs_mv3.js | 162 ++ .../browser/browser_ext_menus_context_action.js | 424 ++++ .../browser/browser_ext_menus_context_compose.js | 179 ++ .../browser/browser_ext_menus_context_content.js | 253 +++ .../browser_ext_menus_context_folder_pane.js | 97 + .../browser_ext_menus_context_message_panes.js | 180 ++ .../test/browser/browser_ext_menus_context_tabs.js | 77 + .../browser_ext_menus_context_tools_main_menu.js | 156 ++ .../browser_ext_menus_message_one_attachment.js | 395 ++++ .../browser_ext_menus_message_two_attachments.js | 397 ++++ .../test/browser/browser_ext_menus_popup_action.js | 405 ++++ .../test/browser/browser_ext_menus_replace_menu.js | 582 +++++ .../browser_ext_menus_replace_menu_context.js | 375 ++++ .../test/browser/browser_ext_messageDisplay.js | 1016 +++++++++ .../browser/browser_ext_messageDisplayAction.js | 337 +++ ...browser_ext_messageDisplayAction_popup_click.js | 294 +++ ...ageDisplayAction_popup_click_mv3_event_pages.js | 113 + .../browser_ext_messageDisplayAction_properties.js | 184 ++ .../browser/browser_ext_messageDisplayScripts.js | 636 ++++++ .../browser_ext_messageDisplay_bug1827032.js | 38 + .../browser_ext_messageDisplay_bug1828056.js | 212 ++ .../browser_ext_messageDisplay_open_file.js | 221 ++ ...wser_ext_messageDisplay_open_headerMessageId.js | 221 ++ .../browser_ext_messageDisplay_open_messageId.js | 221 ++ .../test/browser/browser_ext_message_external.js | 427 ++++ .../browser_ext_messages_open_attachment.js | 107 + .../test/browser/browser_ext_quickFilter.js | 132 ++ .../test/browser/browser_ext_sessions.js | 90 + .../extensions/test/browser/browser_ext_spaces.js | 1047 +++++++++ .../test/browser/browser_ext_spacesToolbar.js | 755 +++++++ .../test/browser/browser_ext_tabs_content.js | 336 +++ .../test/browser/browser_ext_tabs_cookieStoreId.js | 275 +++ .../test/browser/browser_ext_tabs_events.js | 591 +++++ .../test/browser/browser_ext_tabs_move.js | 306 +++ .../browser_ext_tabs_onCreated_bug1817872.js | 226 ++ .../test/browser/browser_ext_tabs_query.js | 113 + .../test/browser/browser_ext_tabs_update_reload.js | 578 +++++ .../test/browser/browser_ext_themes_onUpdated.js | 150 ++ .../browser_ext_tooltip_in_extension_pages.js | 685 ++++++ .../extensions/test/browser/browser_ext_windows.js | 439 ++++ .../test/browser/browser_ext_windows_bug1732559.js | 94 + ...wser_ext_windows_create_normal_cookieStoreId.js | 116 + ...owser_ext_windows_create_popup_cookieStoreId.js | 255 +++ .../test/browser/browser_ext_windows_events.js | 405 ++++ .../test/browser/browser_ext_windows_types.js | 121 ++ .../extensions/test/browser/data/cloudFile1.txt | 1 + .../extensions/test/browser/data/cloudFile2.txt | 1 + .../extensions/test/browser/data/content.html | 12 + .../extensions/test/browser/data/content_body.html | 1 + .../extensions/test/browser/data/linktest.html | 11 + .../extensions/test/browser/data/tb-logo.png | Bin 0 -> 6462 bytes .../components/extensions/test/browser/head.js | 1533 +++++++++++++ .../extensions/test/browser/head_menus.js | 733 +++++++ .../browser/messages/attachedMessageSample.eml | 186 ++ .../test/browser/messages/messageWithLink.eml | 26 + .../extensions/test/browser/test_browserAction.js | 845 ++++++++ 103 files changed, 34855 insertions(+) create mode 100644 comm/mail/components/extensions/test/browser/.eslintrc.js create mode 100644 comm/mail/components/extensions/test/browser/browser.ini create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_addressBooksUI.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_browserAction_customized.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_browserAction_not_customized.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click_mv3_event_pages.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_browserAction_properties.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_bug1812530.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_clickHandler.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_cloudFile.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_execute_compose_action.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_execute_message_display_action.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_getAll.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_onChanged.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand_bug1845236.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_update.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_composeAction.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click_mv3_event_pages.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_composeAction_properties.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_composeScripts.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_attachments.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_body.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_bug1691254.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_forward.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_headers.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_identity.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_new.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_reply.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_bug1692439.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_bug1804796.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_details.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_details_body.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_details_headers.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_dictionaries.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_onBeforeSend.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_saveDraft.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_saveTemplate.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_sendMessage.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_contentScripts.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_content_handler.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_content_tabs_navigation_menu.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_mailTabs.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_mailTabs_mv3.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_action.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_compose.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_content.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_folder_pane.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_message_panes.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_tabs.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_tools_main_menu.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_message_one_attachment.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_message_two_attachments.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_popup_action.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click_mv3_event_pages.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_properties.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplayScripts.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1827032.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1828056.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_file.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_headerMessageId.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_messageId.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_message_external.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messages_open_attachment.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_quickFilter.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_sessions.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_spaces.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_spacesToolbar.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_content.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_events.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_move.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_onCreated_bug1817872.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_query.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_update_reload.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_themes_onUpdated.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tooltip_in_extension_pages.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows_bug1732559.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows_create_normal_cookieStoreId.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows_create_popup_cookieStoreId.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows_events.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows_types.js create mode 100644 comm/mail/components/extensions/test/browser/data/cloudFile1.txt create mode 100644 comm/mail/components/extensions/test/browser/data/cloudFile2.txt create mode 100644 comm/mail/components/extensions/test/browser/data/content.html create mode 100644 comm/mail/components/extensions/test/browser/data/content_body.html create mode 100644 comm/mail/components/extensions/test/browser/data/linktest.html create mode 100644 comm/mail/components/extensions/test/browser/data/tb-logo.png create mode 100644 comm/mail/components/extensions/test/browser/head.js create mode 100644 comm/mail/components/extensions/test/browser/head_menus.js create mode 100644 comm/mail/components/extensions/test/browser/messages/attachedMessageSample.eml create mode 100644 comm/mail/components/extensions/test/browser/messages/messageWithLink.eml create mode 100644 comm/mail/components/extensions/test/browser/test_browserAction.js (limited to 'comm/mail/components/extensions/test/browser') 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": ` + + + Popup + + +

Hello

+ + + `, + "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": ` + + + Popup + + +

Hello

+ + + `, + "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": ` + + + EXAMPLE + + + +

This is an example page

+ + `, + "test.html": ` + + + TEST + + + + + + `, + }; +}; + +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": ` + + + + + + + + Popup + + + `, + "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": ` + + + + + + + + Popup + + + `, + "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": ` + + + + + + + + Popup + + + `, + "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": ` + + + + + + + + Popup + + + `, + "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": ` + + + EXAMPLE + + + +

This is an example page

+ + `, + }, + 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 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 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": ` + + + Popup + + +

Hello

+ + + `, + "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": ` + + + Popup + + +

Hello

+ + + `, + "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: [""], + 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: [""], + 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(""); +}); +add_task(async function testContentScriptRegister() { + await subtestContentScriptRegister("", "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 = "


"; + // Editor content after composeWindow.SetComposeDetails() has been used + // to clear the body. + let setEmptyHTML = "
"; + let plainTextBodyTag = + ''; + 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: "

I'm an HTML message!

" }], + expected: { + isHTML: true, + htmlIncludes: "

I'm an HTML message!

", + 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!", + 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!", + 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: "

This is some HTML text

", + identityId: plainTextIdentity.id, + }, + ], + expected: { + isHTML: true, + htmlIncludes: "

This is some HTML text

", + }, + }, + { + // 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 = `\r\n\r\n \r\n\r\n\r\n \r\n \r\n

This is some
HTML text

\r\n

\r\n\r\n \r\n\r\n\r\n\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(/
/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 "], + 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 "], + 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 ", + cc: "John Watson ", + subject: "Did you miss me?", + }); + await checkHeaders({ + to: ["Sherlock Holmes "], + cc: ["John Watson "], + 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 "], + cc: ["John Watson "], + subject: "Did you miss me?", + }); + await checkHeaders({ + to: ["Sherlock Holmes "], + cc: ["John Watson "], + 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 "], + cc: ["John Watson "], + 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 "], + 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 "], + } + ); + await checkHeaders({ + to: ["Mycroft Holmes "], + 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 "], + }); + await checkHeaders({ + to: ["Mycroft Holmes "], + 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 "], + 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 "], + 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: "

This is some HTML text.

", + 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("

This is some HTML text.

") + ); + 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 ", + 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 ", + 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   \n " }, + expected: { isPlainText: true, plainTextBody: "123456   \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 = "

This is some HTML text.

"; + + 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 = + ''; + + // 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("

This is some HTML text.

") + ); + browser.test.assertEq( + "This is some /HTML/ text.", + htmlDetails.plainTextBody + ); + + // Set details, HTML message. + + await browser.compose.setComposeDetails(htmlTabId, { + body: htmlDetails.body.replace("HTML", "HTML"), + }); + htmlDetails = await browser.compose.getComposeDetails(htmlTabId); + browser.test.log(JSON.stringify(htmlDetails)); + browser.test.assertTrue(!htmlDetails.isPlainText); + browser.test.assertTrue( + htmlDetails.body.includes("

This is some HTML text.

") + ); + 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." + ) + ); + 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.
Indeed, it is plain." + ) + ); + 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, " was removed"); + is(htmlDocument.querySelectorAll("code").length, 1, " 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 = + ''; + + // 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 " }, + expected: { to: ["Greg Lestrade "] }, + }, + { + // Empty string. Done here so we have something to clear. + input: { to: "" }, + expected: {}, + }, + { + // Single input, array with string. + input: { to: ["John Watson "] }, + expected: { to: ["John Watson "] }, + }, + { + // Name with a comma, not quoted per RFC 822. This is how + // getComposeDetails returns names with a comma. + input: { to: ["Holmes, Mycroft "] }, + expected: { to: ["Holmes, Mycroft "] }, + }, + { + // Name with a comma, quoted per RFC 822. This should work too. + input: { to: [`"Holmes, Mycroft" `] }, + expected: { to: ["Holmes, Mycroft "] }, + }, + { + // Name and address with non-ASCII characters. + input: { to: ["Jïm Morïarty "] }, + expected: { to: ["Jïm Morïarty "] }, + }, + { + // 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 "] }, + }, + { + // Null input. This should not clear the field. + input: { to: null }, + expected: { to: ["Sherlock Holmes "] }, + }, + { + // Single input, array with mailing list. + input: { to: [{ id: list, type: "mailingList" }] }, + expected: { to: ["Holmes and Watson "] }, + }, + { + // Multiple inputs, string. + input: { + to: "Molly Hooper , Mrs Hudson ", + }, + expected: { + to: [ + "Molly Hooper ", + "Mrs Hudson ", + ], + }, + }, + { + // Multiple inputs, array with strings. + input: { + to: [ + "Irene Adler ", + "Mary Watson ", + ], + }, + expected: { + to: [ + "Irene Adler ", + "Mary Watson ", + ], + }, + }, + { + // Multiple inputs, mixed. + input: { + to: [ + { id: contacts.sherlock, type: "contact" }, + "Mycroft Holmes ", + ], + }, + expected: { + to: [ + "Sherlock Holmes ", + "Mycroft Holmes ", + ], + }, + }, + { + // 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 " }, + expected: { from: "Mycroft Holmes " }, + }, + { + // Override from with contact id + input: { from: { id: contacts.sherlock, type: "contact" } }, + expected: { from: "Sherlock Holmes " }, + }, + { + // Override from with multiple string address + input: { + from: "Mycroft Holmes , Mary Watson ", + }, + 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 "], + 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 ", + 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 "], + 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 ", + 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 , John Watson ", + "list in unchanged field was expanded" + ); + is( + sentMessage7.bccList, + "Sherlock Holmes , John Watson ", + "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 , John Watson ", + "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: [""], + 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": ` + + + EXAMPLE + + + +

This is an example page

+ + `, + "test.html": ` + + + TEST + + + + + + `, + }; +}; + +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": ` + + + EXAMPLE + + + +

This is text.

+ + `, + "test.html": ` + + + TEST + + + +

This is text.

+ + + `, + }; +}; + +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: [""], + }); + + 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: [""], + }); + + 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: [""], + }); + + 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: [""], + }); + + 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: [""], + }); + + 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: [""], + }); + + 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 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 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 = `Link`; + 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": ` + + Link +
+ + `, + "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 = `Link2`; + 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": ` + + Link1 +
+ + `, + "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": ` + + Link + + `, + "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": ` + + + Popup + + +

Hello

+ + + `, + "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", ""], + }, + }); + + 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: [""], + 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: [""], + 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], ""); +}); +add_task(async function testContentScriptRegister() { + about3Pane.threadTree.selectedIndex = 10; + await awaitBrowserLoaded(messagePane); + await subtestContentScriptRegister( + messages[10], + "", + "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 ", + ccList: ["Robin "], + subject: "Attached message with attachments", + attachments: 2, + size: 9754, + external: true, + read: null, + recipients: ["Heinz "], + date: 958796995000, + body: "This message has one normal attachment and one email attachment", + }, + openExternalAttachedMessage: { + headerMessageId: "sample-attached.eml@mime.sample", + author: "Superman ", + ccList: ["Jimmy "], + subject: "Test message", + attachments: 3, + size: platformInfo.os == "win" ? 6947 : 6825, // Line endings. + external: true, + read: null, + recipients: ["Heinz Müller "], + 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 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": "I'm a real page!", + "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": "I'm a real page!", + }, + 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": "Page 1", + "page2.html": "Page 2", + "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": "I'm page #1!", + "test2.html": "I'm page #2!", + "test3.html": "I'm page #3!", + "test4.html": "I'm page #4!", + "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": "I'm a real page!", + "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 "] }; + 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": "

Test Protocol Handler

", + }; + 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 "] }; + 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": ` + + + Page + + +

Tooltip test

+

I am an element with a tooltip

+ + + `, + "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": ` + + + Page + + +

Tooltip test

+

I am an element with a tooltip

+ + + `, + "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": ` + + + Page + + +

Tooltip test

+

I am an element with a tooltip

+ + + `, + "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": ` + + + Page + + +

Tooltip test

+

I am an element with a tooltip

+ + + `, + "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": ` + + + Page + + +

Tooltip test

+

I am an element with a tooltip

+ + + `, + "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": ` + + + Page + + +

Tooltip test

+

I am an element with a tooltip

+ + + `, + "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": ` + + + Page + + +

Tooltip test

+

I am an element with a tooltip

+ + + `, + "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": ` + + + Page + + +

Tooltip test

+

I am an element with a tooltip

+ + + `, + "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": ` + + + + + A test document + + + +

This is text.

+ + + `, + "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": ` + + + TEST + + + +

Test body

+ + `, + "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": ` + + + + + Focus Test + + + + + + `, + "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 @@ + + + + + A test document + + +

This is text.

+

This is a link with text.

+

+ + 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 @@ +

This is text.

This is a link with text.

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 @@ + + + + + A test document + + +

self

+

other

+ + 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 Binary files /dev/null and b/comm/mail/components/extensions/test/browser/data/tb-logo.png 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": ` + + + Popup + + +

Hello

+ + + `, + "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 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 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 that should appear. + * @param {string} selector - CSS selector of the element to be clicked on. + * @param {Element} 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: +Date: Fri, 20 May 2000 00:29:55 -0400 +To: Heinz +Cc: Robin +From: Batman +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 + + + + + + + +

This message has one normal attachment and one email attachment, + which itself has 3 attachments.
+

+ + +--------------49CVLb1N6p6Spdka4qq7Naeg +Content-Type: message/rfc822; charset=UTF-8; name="sample02.eml" +Content-Disposition: attachment; filename="sample02.eml" +Content-Transfer-Encoding: 7bit + +Message-ID: +From: Superman +To: =?iso-8859-1?Q?Heinz_M=FCller?= +Cc: Jimmy +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: +Date: Fri, 20 May 2000 00:29:55 -0400 +To: Heinz +Cc: Robin +From: Batman +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 + + + + + + + +

This is an interesting link

+ + + +--------------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(); +}); -- cgit v1.2.3