/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; var { ExtensionTestUtils } = ChromeUtils.importESModule( "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" ); var { TestUtils } = ChromeUtils.importESModule( "resource://testing-common/TestUtils.sys.mjs" ); var { ExtensionsUI } = ChromeUtils.import( "resource:///modules/ExtensionsUI.jsm" ); var { AddonTestUtils } = ChromeUtils.importESModule( "resource://testing-common/AddonTestUtils.sys.mjs" ); ExtensionTestUtils.mockAppInfo(); AddonTestUtils.maybeInit(this); registerCleanupFunction(async () => { // Remove the temporary MozillaMailnews folder, which is not deleted in time when // the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over // files in the temp folder. // Note: PathUtils.tempDir points to the system temp folder, which is different. let path = PathUtils.join( Services.dirsvc.get("TmpD", Ci.nsIFile).path, "MozillaMailnews" ); await IOUtils.remove(path, { recursive: true }); }); // Function to start an event page extension (MV3), which can be called whenever // the main test is about to trigger an event. The extension terminates its // background and listens for that single event, verifying it is waking up correctly. async function event_page_extension(eventName, actionCallback) { let ext = ExtensionTestUtils.loadExtension({ files: { "background.js": async () => { // Whenever the extension starts or wakes up, hasFired is set to false. In // case of a wake-up, the first fired event is the one that woke up the background. let hasFired = false; let _eventName = browser.runtime.getManifest().description; browser.messages[_eventName].addListener(async (...args) => { // Only send the first event after background wake-up, this should // be the only one expected. if (!hasFired) { hasFired = true; browser.test.sendMessage(`${_eventName} received`, args); } }); browser.test.sendMessage("background started"); }, }, manifest: { manifest_version: 3, description: eventName, background: { scripts: ["background.js"] }, browser_specific_settings: { gecko: { id: "event_page_extension@mochi.test" }, }, permissions: ["accountsRead", "messagesRead", "messagesMove"], }, }); await ext.startup(); await ext.awaitMessage("background started"); // The listener should be persistent, but not primed. assertPersistentListeners(ext, "messages", eventName, { primed: false }); await ext.terminateBackground({ disableResetIdleForTest: true }); // Verify the primed persistent listener. assertPersistentListeners(ext, "messages", eventName, { primed: true }); await actionCallback(); let rv = await ext.awaitMessage(`${eventName} received`); await ext.awaitMessage("background started"); // The listener should be persistent, but not primed. assertPersistentListeners(ext, "messages", eventName, { primed: false }); await ext.unload(); return rv; } add_task( { skip_if: () => IS_NNTP, }, async function test_update() { await AddonTestUtils.promiseStartupManager(); let account = createAccount(); let rootFolder = account.incomingServer.rootFolder; let testFolder0 = await createSubfolder(rootFolder, "test0"); await createMessages(testFolder0, 1); testFolder0.addKeywordsToMessages( [[...testFolder0.messages][0]], "testkeyword" ); let files = { "background.js": async () => { async function capturePrimedEvent(eventName, callback) { let eventPageExtensionReadyPromise = window.waitForMessage(); browser.test.sendMessage("capturePrimedEvent", eventName); await eventPageExtensionReadyPromise; let eventPageExtensionFinishedPromise = window.waitForMessage(); callback(); return eventPageExtensionFinishedPromise; } function newUpdatePromise(numberOfEventsToCollapse = 1) { return new Promise(resolve => { let seenEvents = {}; const listener = (msg, props) => { if (!seenEvents.hasOwnProperty(msg.id)) { seenEvents[msg.id] = { counts: 0, props: {}, }; } seenEvents[msg.id].counts++; for (let prop of Object.keys(props)) { seenEvents[msg.id].props[prop] = props[prop]; } if (seenEvents[msg.id].counts == numberOfEventsToCollapse) { browser.messages.onUpdated.removeListener(listener); resolve({ msg, props: seenEvents[msg.id].props }); } }; browser.messages.onUpdated.addListener(listener); }); } let tags = await browser.messages.listTags(); let [data] = await window.sendMessage("getFolder"); let messageList = await browser.messages.list(data.folder); browser.test.assertEq(1, messageList.messages.length); let message = messageList.messages[0]; browser.test.assertFalse(message.flagged); browser.test.assertFalse(message.read); browser.test.assertFalse(message.junk); browser.test.assertEq(0, message.junkScore); browser.test.assertEq(0, message.tags.length); browser.test.assertEq(data.size, message.size); browser.test.assertEq("0@made.up.invalid", message.headerMessageId); // Test that setting flagged works. let updatePromise = newUpdatePromise(); let primedUpdatedInfo = await capturePrimedEvent("onUpdated", () => browser.messages.update(message.id, { flagged: true }) ); let updateInfo = await updatePromise; window.assertDeepEqual( [updateInfo.msg, updateInfo.props], primedUpdatedInfo, "The primed and non-primed onUpdated events should return the same values", { strict: true } ); browser.test.assertEq(message.id, updateInfo.msg.id); window.assertDeepEqual({ flagged: true }, updateInfo.props); await window.sendMessage("flagged"); // Test that setting read works. updatePromise = newUpdatePromise(); primedUpdatedInfo = await capturePrimedEvent("onUpdated", () => browser.messages.update(message.id, { read: true }) ); updateInfo = await updatePromise; window.assertDeepEqual( [updateInfo.msg, updateInfo.props], primedUpdatedInfo, "The primed and non-primed onUpdated events should return the same values", { strict: true } ); browser.test.assertEq(message.id, updateInfo.msg.id); window.assertDeepEqual({ read: true }, updateInfo.props); await window.sendMessage("read"); // Test that setting junk works. updatePromise = newUpdatePromise(); primedUpdatedInfo = await capturePrimedEvent("onUpdated", () => browser.messages.update(message.id, { junk: true }) ); updateInfo = await updatePromise; window.assertDeepEqual( [updateInfo.msg, updateInfo.props], primedUpdatedInfo, "The primed and non-primed onUpdated events should return the same values", { strict: true } ); browser.test.assertEq(message.id, updateInfo.msg.id); window.assertDeepEqual({ junk: true }, updateInfo.props); await window.sendMessage("junk"); // Test that setting one tag works. updatePromise = newUpdatePromise(); primedUpdatedInfo = await capturePrimedEvent("onUpdated", () => browser.messages.update(message.id, { tags: [tags[0].key] }) ); updateInfo = await updatePromise; window.assertDeepEqual( [updateInfo.msg, updateInfo.props], primedUpdatedInfo, "The primed and non-primed onUpdated events should return the same values", { strict: true } ); browser.test.assertEq(message.id, updateInfo.msg.id); window.assertDeepEqual({ tags: [tags[0].key] }, updateInfo.props); await window.sendMessage("tags1"); // Test that setting two tags works. We get 3 events: one removing tags0, // one adding tags1 and one adding tags2. updatePromise is waiting for // the third one before resolving. updatePromise = newUpdatePromise(3); await browser.messages.update(message.id, { tags: [tags[1].key, tags[2].key], }); updateInfo = await updatePromise; browser.test.assertEq(message.id, updateInfo.msg.id); window.assertDeepEqual( { tags: [tags[1].key, tags[2].key] }, updateInfo.props ); await window.sendMessage("tags2"); // Test that unspecified properties aren't changed. let listenerCalls = 0; const listenerFunc = (msg, props) => { listenerCalls++; }; browser.messages.onUpdated.addListener(listenerFunc); await browser.messages.update(message.id, {}); await window.sendMessage("empty"); // Check if the no-op update call triggered a listener. await new Promise(resolve => setTimeout(resolve)); browser.messages.onUpdated.removeListener(listenerFunc); browser.test.assertEq( 0, listenerCalls, "Not expecting listener callbacks on no-op updates." ); message = await browser.messages.get(message.id); browser.test.assertTrue(message.flagged); browser.test.assertTrue(message.read); browser.test.assertTrue(message.junk); browser.test.assertEq(100, message.junkScore); browser.test.assertEq(2, message.tags.length); browser.test.assertEq(tags[1].key, message.tags[0]); browser.test.assertEq(tags[2].key, message.tags[1]); browser.test.assertEq("0@made.up.invalid", message.headerMessageId); // Test that clearing properties works. updatePromise = newUpdatePromise(5); await browser.messages.update(message.id, { flagged: false, read: false, junk: false, tags: [], }); updateInfo = await updatePromise; window.assertDeepEqual( { flagged: false, read: false, junk: false, tags: [], }, updateInfo.props ); await window.sendMessage("clear"); message = await browser.messages.get(message.id); browser.test.assertFalse(message.flagged); browser.test.assertFalse(message.read); browser.test.assertFalse(message.external); browser.test.assertFalse(message.junk); browser.test.assertEq(0, message.junkScore); browser.test.assertEq(0, message.tags.length); browser.test.assertEq("0@made.up.invalid", message.headerMessageId); browser.test.notifyPass("finished"); }, "utils.js": await getUtilsJS(), }; let extension = ExtensionTestUtils.loadExtension({ files, manifest: { background: { scripts: ["utils.js", "background.js"] }, permissions: ["accountsRead", "messagesRead"], browser_specific_settings: { gecko: { id: "messages.update@mochi.test" }, }, }, }); let message = [...testFolder0.messages][0]; ok(!message.isFlagged); ok(!message.isRead); equal(message.getStringProperty("keywords"), "testkeyword"); extension.onMessage("capturePrimedEvent", async eventName => { let primedEventData = await event_page_extension(eventName, () => { // Resume execution in the main test, after the event page extension is // ready to capture the event with deactivated background. extension.sendMessage(); }); extension.sendMessage(...primedEventData); }); extension.onMessage("flagged", async () => { await TestUtils.waitForCondition(() => message.isFlagged); extension.sendMessage(); }); extension.onMessage("read", async () => { await TestUtils.waitForCondition(() => message.isRead); extension.sendMessage(); }); extension.onMessage("junk", async () => { await TestUtils.waitForCondition( () => message.getStringProperty("junkscore") == 100 ); extension.sendMessage(); }); extension.onMessage("tags1", async () => { if (IS_IMAP) { // Only IMAP sets the junk/nonjunk keyword. await TestUtils.waitForCondition( () => message.getStringProperty("keywords") == "testkeyword junk $label1" ); } else { await TestUtils.waitForCondition( () => message.getStringProperty("keywords") == "testkeyword $label1" ); } extension.sendMessage(); }); extension.onMessage("tags2", async () => { if (IS_IMAP) { await TestUtils.waitForCondition( () => message.getStringProperty("keywords") == "testkeyword junk $label2 $label3" ); } else { await TestUtils.waitForCondition( () => message.getStringProperty("keywords") == "testkeyword $label2 $label3" ); } extension.sendMessage(); }); extension.onMessage("empty", async () => { await TestUtils.waitForCondition(() => message.isFlagged); await TestUtils.waitForCondition(() => message.isRead); if (IS_IMAP) { await TestUtils.waitForCondition( () => message.getStringProperty("keywords") == "testkeyword junk $label2 $label3" ); } else { await TestUtils.waitForCondition( () => message.getStringProperty("keywords") == "testkeyword $label2 $label3" ); } extension.sendMessage(); }); extension.onMessage("clear", async () => { await TestUtils.waitForCondition(() => !message.isFlagged); await TestUtils.waitForCondition(() => !message.isRead); await TestUtils.waitForCondition( () => message.getStringProperty("junkscore") == 0 ); if (IS_IMAP) { await TestUtils.waitForCondition( () => message.getStringProperty("keywords") == "testkeyword nonjunk" ); } else { await TestUtils.waitForCondition( () => message.getStringProperty("keywords") == "testkeyword" ); } extension.sendMessage(); }); extension.onMessage("getFolder", async () => { extension.sendMessage({ folder: { accountId: account.key, path: "/test0" }, size: message.messageSize, }); }); await extension.startup(); await extension.awaitFinish("finished"); await extension.unload(); cleanUpAccount(account); await AddonTestUtils.promiseShutdownManager(); } );