diff options
Diffstat (limited to '')
18 files changed, 3672 insertions, 0 deletions
diff --git a/comm/mail/base/test/unit/distribution.ini b/comm/mail/base/test/unit/distribution.ini new file mode 100644 index 0000000000..ea07aa3c5e --- /dev/null +++ b/comm/mail/base/test/unit/distribution.ini @@ -0,0 +1,56 @@ +# Partner Distribution Configuration File +# Mozilla Thunderbird with Example Dist settings + +# NOTE! These three are required. +# id: short string unique to this distribution +# about: a short descriptive (ui-visible) string for this +# distribution +# version: version of the extra distribution pieces (not the version +# of Thunderbird) + +[Global] +id=ExampleDist +version=1.0 +about=Example Distribution Edition +about.en-US=Example Distribution Edition EN-US + +# This section contains the global js prefs. You do should not list +# here the localized preferences (see below) + +# Boolean preferences should be 'true' or 'false', w/o quotes. e.g.: +# my.bool.preference=true +# +# Integer preferences should be unquoted numbers. e.g.: +# my.int.preference=123 +# +# String preferences should be in quotes. e.g.: +# my.string.preference="foo" + +[Preferences] +app.distributor="exampledist" +app.test.data="nospecialcharacterssetting" +app.distributor.channel="" +mail.phishing.detection.enabled=false +mail.spam.manualMark=false +test.setting.random="success" + +# This section is used as a template for locale-specific properties +# files. They work similarly to the GlobalPrefs section, except that +# the %LOCALE% string gets substituted with the language string. + +[LocalizablePreferences] +app.releaseNotesURL="http://example.org/%LOCALE%/%LOCALE%/" +mailnews.start_page.welcome_url="http://example.com/%APP%/firstrun?locale=%LOCALE%version=%VERSION%&os=%OS%&buildid=%APPBUILDID%" +test.setting.locale="http://my.senecacacollege.on.example/%LOCALE%" + +# This section is an example of an override for a particular locale. +# The override sections do not interpolate %LOCALE% into strings. +# Preferences set in override sections are *merged* with the +# localizable defaults. That is, if you want a pref in +# [LocalizablePreferences] to not be set in a particular locale, +# you'll need to unset it explicitly ("pref.name=" on a line of its +# own). + +[LocalizablePreferences-en-US] +app.releaseNotesURL="http://example.com/relnotes/" +mailnews.start_page.welcome_url="http://example.com/firstrun/" diff --git a/comm/mail/base/test/unit/head_mailbase.js b/comm/mail/base/test/unit/head_mailbase.js new file mode 100644 index 0000000000..0c275d8abb --- /dev/null +++ b/comm/mail/base/test/unit/head_mailbase.js @@ -0,0 +1,20 @@ +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { mailTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/MailTestUtils.jsm" +); + +var CC = Components.Constructor; + +// Ensure the profile directory is set up +do_get_profile(); + +var gDEPTH = "../../../../"; + +registerCleanupFunction(function () { + load(gDEPTH + "mailnews/resources/mailShutdown.js"); +}); diff --git a/comm/mail/base/test/unit/head_mailbase_maildir.js b/comm/mail/base/test/unit/head_mailbase_maildir.js new file mode 100644 index 0000000000..921a2bd474 --- /dev/null +++ b/comm/mail/base/test/unit/head_mailbase_maildir.js @@ -0,0 +1,9 @@ +/* import-globals-from head_mailbase.js */ + +// alternate head to set maildir as default +load("head_mailbase.js"); +info("Running test with maildir"); +Services.prefs.setCharPref( + "mail.serverDefaultStoreContractID", + "@mozilla.org/msgstore/maildirstore;1" +); diff --git a/comm/mail/base/test/unit/resources/viewWrapperTestUtils.js b/comm/mail/base/test/unit/resources/viewWrapperTestUtils.js new file mode 100644 index 0000000000..5ae301b016 --- /dev/null +++ b/comm/mail/base/test/unit/resources/viewWrapperTestUtils.js @@ -0,0 +1,534 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { DBViewWrapper, IDBViewWrapperListener } = ChromeUtils.import( + "resource:///modules/DBViewWrapper.jsm" +); +var { MailViewManager, MailViewConstants } = ChromeUtils.import( + "resource:///modules/MailViewManager.jsm" +); +var { VirtualFolderHelper } = ChromeUtils.import( + "resource:///modules/VirtualFolderWrapper.jsm" +); +var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { dump_view_state } = ChromeUtils.import( + "resource://testing-common/mozmill/ViewHelpers.jsm" +); + +var gMessageGenerator; +var gMessageScenarioFactory; +var messageInjection; +var gMockViewWrapperListener; + +function initViewWrapperTestUtils(aInjectionConfig) { + if (!aInjectionConfig) { + throw new Error("Please provide an injection config for MessageInjection."); + } + + gMessageGenerator = new MessageGenerator(); + gMessageScenarioFactory = new MessageScenarioFactory(gMessageGenerator); + + messageInjection = new MessageInjection(aInjectionConfig, gMessageGenerator); + messageInjection.registerMessageInjectionListener(VWTU_testHelper); + registerCleanupFunction(() => { + // Cleanup of VWTU_testHelper. + VWTU_testHelper.postTest(); + }); + gMockViewWrapperListener = new MockViewWrapperListener(); +} + +// Something less sucky than do_check_true. +function assert_true(aBeTrue, aWhy, aDumpView) { + if (!aBeTrue) { + if (aDumpView) { + dump_view_state(VWTU_testHelper.active_view_wrappers[0]); + } + do_throw(aWhy); + } +} + +function assert_false(aBeFalse, aWhy, aDumpView) { + if (aBeFalse) { + if (aDumpView) { + dump_view_state(VWTU_testHelper.active_view_wrappers[0]); + } + do_throw(aWhy); + } +} + +function assert_equals(aA, aB, aWhy, aDumpView) { + if (aA != aB) { + if (aDumpView) { + dump_view_state(VWTU_testHelper.active_view_wrappers[0]); + } + do_throw(aWhy); + } +} + +function assert_bit_set(aWhat, aBit, aWhy) { + if (!(aWhat & aBit)) { + do_throw(aWhy); + } +} + +function assert_bit_not_set(aWhat, aBit, aWhy) { + if (aWhat & aBit) { + do_throw(aWhy); + } +} + +var gFakeCommandUpdater = { + updateCommandStatus() {}, + + displayMessageChanged(aFolder, aSubject, aKeywords) {}, + + summarizeSelection() {}, + + updateNextMessageAfterDelete() {}, +}; + +/** + * Track our resources used by each test. This is so we can keep our memory + * usage low by forcing things to be forgotten about (or even nuked) once + * a test completes, but also so we can provide useful information about the + * state of things if a test times out. + */ +var VWTU_testHelper = { + active_view_wrappers: [], + active_real_folders: [], + active_virtual_folders: [], + + onVirtualFolderCreated(aVirtualFolder) { + this.active_virtual_folders.push(aVirtualFolder); + }, + + postTest() { + // Close all the views we opened. + this.active_view_wrappers.forEach(function (wrapper) { + wrapper.close(); + }); + // Verify that the notification helper has no outstanding listeners. + if (IDBViewWrapperListener.prototype._FNH.haveListeners()) { + let msg = "FolderNotificationHelper has listeners, but should not."; + dump("*** " + msg + "\n"); + dump("Pending URIs:\n"); + for (let folderURI in IDBViewWrapperListener.prototype._FNH + ._pendingFolderUriToViewWrapperLists) { + dump(" " + folderURI + "\n"); + } + dump("Interested wrappers:\n"); + for (let folderURI in IDBViewWrapperListener.prototype._FNH + ._interestedWrappers) { + dump(" " + folderURI + "\n"); + } + dump("***\n"); + do_throw(msg); + } + // Force the folder to forget about the message database. + this.active_virtual_folders.forEach(function (folder) { + folder.msgDatabase = null; + }); + this.active_real_folders.forEach(function (folder) { + folder.msgDatabase = null; + }); + + this.active_view_wrappers.splice(0); + this.active_real_folders.splice(0); + this.active_virtual_folders.splice(0); + + gMockViewWrapperListener.allMessagesLoadedEventCount = 0; + }, + onTimeout() { + dump("-----------------------------------------------------------\n"); + dump("Active things at time of timeout:\n"); + for (let folder of this.active_real_folders) { + dump("Real folder: " + folder.prettyName + "\n"); + } + for (let virtFolder of this.active_virtual_folders) { + dump("Virtual folder: " + virtFolder.prettyName + "\n"); + } + for (let [i, viewWrapper] of this.active_view_wrappers.entries()) { + dump("-----------------------------------\n"); + dump("Active view wrapper " + i + "\n"); + dump_view_state(viewWrapper); + } + }, +}; + +function make_view_wrapper() { + let wrapper = new DBViewWrapper(gMockViewWrapperListener); + VWTU_testHelper.active_view_wrappers.push(wrapper); + return wrapper; +} + +/** + * Clone an open and valid view wrapper. + */ +function clone_view_wrapper(aViewWrapper) { + let wrapper = aViewWrapper.clone(gMockViewWrapperListener); + VWTU_testHelper.active_view_wrappers.push(wrapper); + return wrapper; +} + +/** + * Open a folder for view display. This is an async operation, relying on the + * onMessagesLoaded(true) notification to get he test going again. + */ +async function view_open(aViewWrapper, aFolder) { + aViewWrapper.listener.pendingLoad = true; + aViewWrapper.open(aFolder); + await gMockViewWrapperListener.promise; + gMockViewWrapperListener.resetPromise(); +} + +async function view_set_mail_view(aViewWrapper, aMailViewIndex, aData) { + aViewWrapper.listener.pendingLoad = true; + aViewWrapper.setMailView(aMailViewIndex, aData); + await gMockViewWrapperListener.promise; + gMockViewWrapperListener.resetPromise(); +} + +async function view_refresh(aViewWrapper) { + aViewWrapper.listener.pendingLoad = true; + aViewWrapper.refresh(); + await gMockViewWrapperListener.promise; + gMockViewWrapperListener.resetPromise(); +} + +async function view_group_by_sort(aViewWrapper, aGroupBySort) { + aViewWrapper.listener.pendingLoad = true; + aViewWrapper.showGroupedBySort = aGroupBySort; + await gMockViewWrapperListener.promise; + gMockViewWrapperListener.resetPromise(); +} + +/** + * Call endViewUpdate on your wrapper in the async idiom. This is essential if + * you are doing things to a cross-folder view which does its searching in a + * time-sliced fashion. In such a case, you would call beginViewUpdate + * manually, then poke at the view, then call us to end the view update. + */ +function async_view_end_update(aViewWrapper) { + aViewWrapper.listener.pendingLoad = true; + aViewWrapper.endViewUpdate(); + return false; +} + +/** + * The deletion is asynchronous from a view perspective because the view ends + * up re-creating itself which triggers a new search. This function is + * nominally asynchronous because we refresh XFVF views when one of their + * folders gets deleted. In that case, you must pass the view wrapper you + * expect to be affected so we can do our async thing. + * If, however, you are deleting the last folder that belongs to a view, you + * should not pass a view wrapper, because you should expect the view wrapper + * to close itself and destroy the view. (Well, the view might do something + * too, but we don't care what it does.) We provide a |delete_folder| alias + * so code can look clean. + * + * @param aViewWrapper Required when you want us to operate asynchronously. + * @param aDontEmptyTrash This function will empty the trash after deleting the + * folder, unless you set this parameter to true. + */ +async function delete_folder(aFolder, aViewWrapper, aDontEmptyTrash) { + VWTU_testHelper.active_real_folders.splice( + VWTU_testHelper.active_real_folders.indexOf(aFolder), + 1 + ); + // Deleting tries to be helpful and move the folder to the trash... + aFolder.deleteSelf(null); + + // Ugh. So we have the problem where that move above just triggered a + // re-computation of the view... which is an asynchronous operation + // that we don't care about at all. We don't need to wait for it to + // complete, but if we don't, we have a race on enabling this next + // notification. + // So we interrupt the search ourselves. This problem is exclusively + // limited to unit testing and is not something we would need to do + // normally. (Because things are single-threaded we are also + // guaranteed that we can interrupt it without needing locks or anything.) + if (aViewWrapper) { + if (aViewWrapper.searching) { + aViewWrapper.search.session.interruptSearch(); + } + aViewWrapper.listener.pendingLoad = true; + } + + // ...so now the stupid folder is in the stupid trash. + // Let's empty the trash, then, shall we? + // (For local folders it doesn't matter who we call this on.) + if (!aDontEmptyTrash) { + aFolder.emptyTrash(null); + } + + await gMockViewWrapperListener.promise; + gMockViewWrapperListener.resetPromise(); +} + +/** + * For assistance in debugging, dump information about a message header. + */ +function dump_message_header(aMsgHdr) { + dump(" Subject: " + aMsgHdr.mime2DecodedSubject + "\n"); + dump(" Date: " + new Date(aMsgHdr.date / 1000) + "\n"); + dump(" Author: " + aMsgHdr.mime2DecodedAuthor + "\n"); + dump(" Recipients: " + aMsgHdr.mime2DecodedRecipients + "\n"); + let junkScore = aMsgHdr.getStringProperty("junkscore"); + dump( + " Read: " + + aMsgHdr.isRead + + " Flagged: " + + aMsgHdr.isFlagged + + " Killed: " + + aMsgHdr.isKilled + + " Junk: " + + (junkScore == "100") + + "\n" + ); + dump(" Keywords: " + aMsgHdr.getStringProperty("Keywords") + "\n"); + dump( + " Folder: " + + aMsgHdr.folder.prettyName + + " Key: " + + aMsgHdr.messageKey + + "\n" + ); +} + +/** + * Verify that the messages in the provided SyntheticMessageSets are the only + * visible messages in the provided DBViewWrapper. If dummy headers are present + * in the view for group-by-sort, the code will ensure that the dummy header's + * underlying header corresponds to a message in the synthetic sets. However, + * you should generally not rely on this code to test for anything involving + * dummy headers. + * + * In the event the view does not contain all of the messages from the provided + * sets or contains messages not in the provided sets, do_throw will be invoked + * with a human readable explanation of the problem. + * + * @param aSynSets A single SyntheticMessageSet or a list of + * SyntheticMessageSets. + * @param aViewWrapper The DBViewWrapper whose contents you want to validate. + */ +function verify_messages_in_view(aSynSets, aViewWrapper) { + if (!("length" in aSynSets)) { + aSynSets = [aSynSets]; + } + + // - Iterate over all the message sets, retrieving the message header. Use + // this to construct a URI to populate a dictionary mapping. + let synMessageURIs = {}; // map URI to message header + for (let messageSet of aSynSets) { + for (let msgHdr of messageSet.msgHdrs()) { + synMessageURIs[msgHdr.folder.getUriForMsg(msgHdr)] = msgHdr; + } + } + + // - Iterate over the contents of the view, nulling out values in + // synMessageURIs for found messages, and exploding for missing ones. + let dbView = aViewWrapper.dbView; + let treeView = aViewWrapper.dbView.QueryInterface(Ci.nsITreeView); + let rowCount = treeView.rowCount; + + for (let iViewIndex = 0; iViewIndex < rowCount; iViewIndex++) { + let msgHdr = dbView.getMsgHdrAt(iViewIndex); + let uri = msgHdr.folder.getUriForMsg(msgHdr); + // Expected hit, null it out. (in the dummy case, we will just null out + // twice, which is also why we do an 'in' test and not a value test. + if (uri in synMessageURIs) { + synMessageURIs[uri] = null; + } else { + // The view is showing a message that should not be shown, explode. + dump( + "The view is showing the following message header and should not" + + " be:\n" + ); + dump_message_header(msgHdr); + dump("View State:\n"); + dump_view_state(aViewWrapper); + throw new Error( + "view contains header that should not be present! " + msgHdr.messageKey + ); + } + } + + // - Iterate over our URI set and make sure every message got nulled out. + for (let uri in synMessageURIs) { + let msgHdr = synMessageURIs[uri]; + if (msgHdr != null) { + dump("************************\n"); + dump( + "The view should have included the following message header but" + + " did not:\n" + ); + dump_message_header(msgHdr); + dump("View State:\n"); + dump_view_state(aViewWrapper); + throw new Error( + "view does not contain a header that should be present! " + + msgHdr.messageKey + ); + } + } +} + +/** + * Assert if the view wrapper is displaying any messages. + */ +function verify_empty_view(aViewWrapper) { + verify_messages_in_view([], aViewWrapper); +} + +/** + * Build a histogram of the treeview levels and verify it matches the expected + * histogram. Oddly enough, I find this to be a reasonable and concise way to + * verify that threading mode is enabled. Keep in mind that this file is + * currently not used to test the actual thread logic. If/when that day comes, + * something less eccentric is certainly the way that should be tested. + */ +function verify_view_level_histogram(aExpectedHisto, aViewWrapper) { + let treeView = aViewWrapper.dbView.QueryInterface(Ci.nsITreeView); + let rowCount = treeView.rowCount; + + let actualHisto = {}; + for (let iViewIndex = 0; iViewIndex < rowCount; iViewIndex++) { + let level = treeView.getLevel(iViewIndex); + actualHisto[level] = (actualHisto[level] || 0) + 1; + } + + for (let [level, count] of Object.entries(aExpectedHisto)) { + if (actualHisto[level] != count) { + dump_view_state(aViewWrapper); + dump("*******************\n"); + dump( + "Expected count for histogram level " + + level + + " was " + + count + + " but got " + + actualHisto[level] + + "\n" + ); + do_throw("View histogram does not match!"); + } + } +} + +/** + * Given a view wrapper and one or more view indices, verify that the row + * returns true for isContainer. + * + * @param aViewWrapper The view wrapper in question + * @param ... View indices to check. + */ +function verify_view_row_at_index_is_container(aViewWrapper, ...aArgs) { + let treeView = aViewWrapper.dbView.QueryInterface(Ci.nsITreeView); + for (let viewIndex of aArgs) { + if (!treeView.isContainer(viewIndex)) { + dump_view_state(aViewWrapper); + do_throw("Expected isContainer to be true at view index " + viewIndex); + } + } +} + +/** + * Given a view wrapper and one or more view indices, verify that there is a + * dummy header at each provided index. + * + * @param aViewWrapper The view wrapper in question + * @param ... View indices to check. + */ +function verify_view_row_at_index_is_dummy(aViewWrapper, ...aArgs) { + const MSG_VIEW_FLAG_DUMMY = 0x20000000; + for (let viewIndex of aArgs) { + let flags = aViewWrapper.dbView.getFlagsAt(viewIndex); + if (!(flags & MSG_VIEW_FLAG_DUMMY)) { + dump_view_state(aViewWrapper); + do_throw("Expected a dummy header at view index " + viewIndex); + } + } +} + +/** + * Expand all nodes in the view wrapper. This is a debug helper function + * because there's no good reason to have it be on the view wrapper at this + * time. You must call async_view_refresh or async_view_end_update (if you are + * within a view update batch) after calling this! + */ +function view_expand_all(aViewWrapper) { + // We can't use the command because it has assertions about having a tree. + aViewWrapper._viewFlags |= Ci.nsMsgViewFlagsType.kExpandAll; +} + +/** + * Create a name and address pair where the provided word is part of the name. + */ +function make_person_with_word_in_name(aWord) { + let dude = gMessageGenerator.makeNameAndAddress(); + return [aWord, dude[1]]; +} + +/** + * Create a name and address pair where the provided word is part of the mail + * address. + */ +function make_person_with_word_in_address(aWord) { + let dude = gMessageGenerator.makeNameAndAddress(); + return [dude[0], aWord + "@madeup.nul"]; +} + +class MockViewWrapperListener extends IDBViewWrapperListener { + shouldUseMailViews = true; + shouldDeferMessageDisplayUntilAfterServerConnect = false; + messenger = null; + // Use no message window! + msgWindow = null; + threadPaneCommandUpdater = gFakeCommandUpdater; + // Event handlers. + allMessagesLoadedEventCount = 0; + messagesRemovedEventCount = 0; + + constructor() { + super(); + this._promise = new Promise(resolve => { + this._resolve = resolve; + }); + } + + shouldMarkMessagesReadOnLeavingFolder(aMsgFolder) { + return Services.prefs.getBoolPref( + "mailnews.mark_message_read." + aMsgFolder.server.type + ); + } + + onMessagesLoaded(aAll) { + if (!aAll) { + return; + } + this.allMessagesLoadedEventCount++; + if (this.pendingLoad) { + this.pendingLoad = false; + this._resolve(); + } + } + + onMessagesRemoved() { + this.messagesRemovedEventCount++; + } + + get promise() { + return this._promise; + } + resetPromise() { + this._promise = new Promise(resolve => { + this._resolve = resolve; + }); + } +} diff --git a/comm/mail/base/test/unit/test_alertHook.js b/comm/mail/base/test/unit/test_alertHook.js new file mode 100644 index 0000000000..0189930862 --- /dev/null +++ b/comm/mail/base/test/unit/test_alertHook.js @@ -0,0 +1,119 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests the replace of alerts service with our own. This will let us check if we're + * prompting or not. + */ + +var { alertHook } = ChromeUtils.import( + "resource:///modules/activity/alertHook.jsm" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); + +alertHook.init(); + +// Wait time of 1s for slow debug builds. +const TEST_WAITTIME = 1000; + +var gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow +); +var mockAlertsService; +var cid; +var mailnewsURL; + +add_setup(function () { + // First register the mock alerts service. + mockAlertsService = new MockAlertsService(); + cid = MockRegistrar.register( + "@mozilla.org/alerts-service;1", + mockAlertsService + ); + // A random URL. + let uri = Services.io.newURI("news://localhost:80/1@regular.invalid"); + mailnewsURL = uri.QueryInterface(Ci.nsIMsgMailNewsUrl); +}); + +add_task(async function test_not_shown_to_user_no_url_no_window() { + // Just text, no url or window => expect no error shown to user + MailServices.mailSession.alertUser("test error"); + await Promise.race([ + PromiseTestUtils.promiseDelay(TEST_WAITTIME).then(result => { + Assert.ok(true, "Alert is not shown with no window or no url present"); + }), + mockAlertsService.promise.then(result => { + throw new Error( + "Alert is shown to the user although neither window nor url is present" + ); + }), + ]); +}); + +add_task(async function test_shown_to_user() { + // Reset promise state. + mockAlertsService.deferPromise(); + // Set a window for the URL. + mailnewsURL.msgWindow = gMsgWindow; + + // Text, url and window => expect error shown to user + MailServices.mailSession.alertUser("test error 2", mailnewsURL); + let alertShown = await mockAlertsService.promise; + Assert.ok(alertShown); +}); + +add_task(async function test_not_shown_to_user_no_window() { + // Reset promise state. + mockAlertsService.deferPromise(); + // No window for the URL. + mailnewsURL.msgWindow = null; + + // Text, url and no window => export no error shown to user + MailServices.mailSession.alertUser("test error 3", mailnewsURL); + await Promise.race([ + PromiseTestUtils.promiseDelay(TEST_WAITTIME).then(result => { + Assert.ok(true, "Alert is not shown with no window but a url present"); + }), + mockAlertsService.promise.then(result => { + throw new Error( + "Alert is shown to the user although no window in the mailnewsURL present" + ); + }), + ]); +}); + +add_task(function endTest() { + MockRegistrar.unregister(cid); +}); + +class MockAlertsService { + QueryInterface = ChromeUtils.generateQI(["nsIAlertsService"]); + + constructor() { + this._deferredPromise = PromiseUtils.defer(); + } + + showAlert() { + this._deferredPromise.resolve(true); + } + + deferPromise() { + this._deferredPromise = PromiseUtils.defer(); + } + + get promise() { + return this._deferredPromise.promise; + } +} diff --git a/comm/mail/base/test/unit/test_attachmentChecker.js b/comm/mail/base/test/unit/test_attachmentChecker.js new file mode 100644 index 0000000000..b727819231 --- /dev/null +++ b/comm/mail/base/test/unit/test_attachmentChecker.js @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 suite for the attachmentChecker class + * + * Currently tested: + * - getAttachmentKeywords function. + */ + +// Globals + +var { AttachmentChecker } = ChromeUtils.import( + "resource:///modules/AttachmentChecker.jsm" +); + +/* + * UTILITIES + */ + +function assert(aBeTrue, aWhy) { + if (!aBeTrue) { + do_throw(aWhy); + } +} + +function assert_equal(aA, aB, aWhy) { + assert( + aA == aB, + aWhy + + " (" + + unescape(encodeURIComponent(aA)) + + " != " + + unescape(encodeURIComponent(aB)) + + ")." + ); +} + +/* + * TESTS + */ + +function test_getAttachmentKeywords(desc, mailData, keywords, expected) { + let result = AttachmentChecker.getAttachmentKeywords(mailData, keywords); + assert_equal(result, expected, desc + " not equal!"); +} + +var tests = [ + // Desc, mail body Data, keywords to search for, expected keywords found. + ["Simple keyword", "latte.ca", "latte", "latte"], + ["Extension", "testing document.pdf", ".pdf", "document.pdf"], + [ + "Two Extensions", + "testing document.pdf and test.pdf", + ".pdf", + "document.pdf,test.pdf", + ], + [ + "Two+one Extensions", + "testing document.pdf and test.pdf and again document.pdf", + ".pdf", + "document.pdf,test.pdf", + ], + ["Url", "testing http://document.pdf", ".pdf", ""], + ["Both", "testing http://document.pdf test.pdf", ".pdf", "test.pdf"], + ["Greek", "This is a Θεωρία test", "Θεωρία,is", "Θεωρία,is"], + ["Greek missing", "This a Θεωρίαω test", "Θεωρία", ""], + ["Greek and punctuation", "This a:Θεωρία-test", "Θεωρία", "Θεωρία"], + ["Greek and Japanese", "This a 添Θεωρία付 test", "Θεωρία", "Θεωρία"], + ["Japanese", "This is 添付! test", "Θεωρία,添付", "添付"], + ["More Japanese", "添付mailを送る", "添付,cv", "添付"], + ["Japanese and English", "添付mailを送る", "添付,mail", "添付,mail"], + ["Japanese and English Mixed", "添付mailを送る", "添付mail", "添付mail"], + ["Japanese and English Mixed missing", "添付mailing", "添付mail", ""], + ["Japanese trailers", "This is 添添付付! test", "Θεωρία,添付", "添付"], + ["Multi-lang", "cv添付Θεωρία", "Θεωρία,添付,cv", "Θεωρία,添付,cv"], + [ + "Should match", + "I've attached the http/test.pdf file", + ".pdf", + "http/test.pdf", + ], + ["Should still fail", "a https://test.pdf a", ".pdf", ""], + ["Should match Japanese", "a test.添付 a", ".添付", "test.添付"], + ["Should match Greek", "a test.Θεωρία a", ".Θεωρία", "test.Θεωρία"], + ["Should match once", "a test.pdf.doc a", ".pdf,.doc", "test.pdf.doc"], + [ + "Should not match kw in url", + "see https://example.org/attachment.cgi?id=1 test", + "attachment", + "", + ], + [ + "Should not match kw in url ending with kw", + "https://example.org/attachment", + "attachment", + "", + ], + [ + "Should match CV and attachment", + "got my CV as attachment", + "CV,attachment", + "CV,attachment", + ], +]; + +function run_test() { + do_test_pending(); + + for (var i in tests) { + if (typeof tests[i] == "function") { + tests[i](); + } else { + test_getAttachmentKeywords.apply(null, tests[i]); + } + } + + do_test_finished(); +} diff --git a/comm/mail/base/test/unit/test_devtools_url.js b/comm/mail/base/test/unit/test_devtools_url.js new file mode 100644 index 0000000000..d0c8baf21f --- /dev/null +++ b/comm/mail/base/test/unit/test_devtools_url.js @@ -0,0 +1,22 @@ +/** + * This test checks for the URL of the developer tools toolbox. If it fails, + * then the code for opening the toolbox has likely changed, and the code in + * MailGlue that observes command-line-startup will not be working properly. + */ + +Cu.importGlobalProperties(["fetch"]); +var { MailGlue } = ChromeUtils.import("resource:///modules/MailGlue.jsm"); + +add_task(async () => { + let expectedURL = `"${MailGlue.BROWSER_TOOLBOX_WINDOW_URL}"`; + let containingFile = + "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs"; + + let response = await fetch(containingFile); + let text = await response.text(); + + Assert.ok( + text.includes(expectedURL), + `Expected to find ${expectedURL} in ${containingFile}.` + ); +}); diff --git a/comm/mail/base/test/unit/test_emptyTrash_dbViewWrapper.js b/comm/mail/base/test/unit/test_emptyTrash_dbViewWrapper.js new file mode 100644 index 0000000000..d392a9ece2 --- /dev/null +++ b/comm/mail/base/test/unit/test_emptyTrash_dbViewWrapper.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from resources/viewWrapperTestUtils.js */ +load("resources/viewWrapperTestUtils.js"); +initViewWrapperTestUtils({ mode: "imap", offline: false }); + +add_task(async function test_real_folder_load_and_move_to_trash() { + let viewWrapper = make_view_wrapper(); + let [[msgFolder], msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + + await view_open( + viewWrapper, + messageInjection.getRealInjectionFolder(msgFolder) + ); + verify_messages_in_view(msgSet, viewWrapper); + + await messageInjection.trashMessages(msgSet); + verify_empty_view(viewWrapper); +}); + +add_task(async function test_empty_trash() { + let viewWrapper = make_view_wrapper(); + let trashHandle = await messageInjection.getTrashFolder(); + let trashFolder = messageInjection.getRealInjectionFolder(trashHandle); + + await view_open(viewWrapper, trashFolder); + + await messageInjection.emptyTrash(); + verify_empty_view(viewWrapper); + + Assert.ok(viewWrapper.displayedFolder !== null); + + let [msgSet] = await messageInjection.makeNewSetsInFolders( + [trashHandle], + [{ count: 1 }] + ); + + verify_messages_in_view(msgSet, viewWrapper); +}); diff --git a/comm/mail/base/test/unit/test_mailGlue_distribution.js b/comm/mail/base/test/unit/test_mailGlue_distribution.js new file mode 100644 index 0000000000..0eb1bc9574 --- /dev/null +++ b/comm/mail/base/test/unit/test_mailGlue_distribution.js @@ -0,0 +1,120 @@ +var { TBDistCustomizer } = ChromeUtils.import( + "resource:///modules/TBDistCustomizer.jsm" +); + +function run_test() { + do_test_pending(); + + Services.locale.requestedLocales = ["en-US"]; + + // Create an instance of nsIFile out of the current process directory + let distroDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile); + + // Construct a descendant of the distroDir file + distroDir.append("distribution"); + + // Create a clone of distroDir + let iniFile = distroDir.clone(); + + // Create a descendant of iniFile + iniFile.append("distribution.ini"); + // It's a bug if distribution.ini already exists + if (iniFile.exists()) { + do_throw( + "distribution.ini already exists in objdir/mozilla/dist/bin/distribution." + ); + } + + registerCleanupFunction(function () { + // Remove the distribution.ini file + if (iniFile.exists()) { + iniFile.remove(true); + } + }); + + let testDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + let testDistributionFile = testDir.clone(); + + // Construct descendant file + testDistributionFile.append("distribution.ini"); + // Copy to distroDir + testDistributionFile.copyTo(distroDir, "distribution.ini"); + Assert.ok(testDistributionFile.exists()); + + // Set the prefs + TBDistCustomizer.applyPrefDefaults(); + + let testIni = Cc["@mozilla.org/xpcom/ini-parser-factory;1"] + .getService(Ci.nsIINIParserFactory) + .createINIParser(testDistributionFile); + + // Now check that prefs were set - test the Global prefs against the + // Global section in the ini file + let iniValue = testIni.getString("Global", "id"); + let pref = Services.prefs.getCharPref("distribution.id"); + Assert.equal(iniValue, pref); + + iniValue = testIni.getString("Global", "version"); + pref = Services.prefs.getCharPref("distribution.version"); + Assert.equal(iniValue, pref); + + let aboutLocale; + try { + aboutLocale = testIni.getString("Global", "about.en-US"); + } catch (e) { + console.error(e); + } + + if (aboutLocale == undefined) { + aboutLocale = testIni.getString("Global", "about"); + } + + pref = Services.prefs.getCharPref("distribution.about"); + Assert.equal(aboutLocale, pref); + + // Test Preferences section + let s = "Preferences"; + for (let key of testIni.getKeys(s)) { + let value = TBDistCustomizer.parseValue(testIni.getString(s, key)); + switch (typeof value) { + case "boolean": + Assert.equal(value, Services.prefs.getBoolPref(key)); + break; + case "number": + Assert.equal(value, Services.prefs.getIntPref(key)); + break; + case "string": + Assert.equal(value, Services.prefs.getCharPref(key)); + break; + default: + do_throw( + "The preference " + key + " is of unknown type: " + typeof value + ); + } + } + + // Test the LocalizablePreferences-[locale] section + // Add any prefs found in it to the overrides array + let overrides = []; + s = "LocalizablePreferences-en-US"; + for (let key of testIni.getKeys(s)) { + let value = TBDistCustomizer.parseValue(testIni.getString(s, key)); + value = "data:text/plain," + key + "=" + value; + Assert.equal(value, Services.prefs.getCharPref(key)); + overrides.push(key); + } + + // Test the LocalizablePreferences section + // Any prefs here that aren't found in overrides are not overridden + // by LocalizablePrefs-[locale] and should be tested + s = "LocalizablePreferences"; + for (let key of testIni.getKeys(s)) { + if (!overrides.includes(key)) { + let value = TBDistCustomizer.parseValue(testIni.getString(s, key)); + value = value.replace(/%LOCALE%/g, "en-US"); + value = "data:text/plain," + key + "=" + value; + Assert.equal(value, Services.prefs.getCharPref(key)); + } + } + do_test_finished(); +} diff --git a/comm/mail/base/test/unit/test_oauth_migration.js b/comm/mail/base/test/unit/test_oauth_migration.js new file mode 100644 index 0000000000..1130757a97 --- /dev/null +++ b/comm/mail/base/test/unit/test_oauth_migration.js @@ -0,0 +1,319 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 migrating Yahoo/AOL users to OAuth2, since "normal password" is going away + * on October 20, 2020. + */ + +var { MailMigrator } = ChromeUtils.import( + "resource:///modules/MailMigrator.jsm" +); +var { localAccountUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/LocalAccountUtils.jsm" +); + +var gAccountList = [ + // POP Yahoo account + Yahoo Server. + { + type: "pop3", + port: 1234, + user: "pop3user", + password: "pop3password", + hostname: "pop3.mail.yahoo.com", + socketType: Ci.nsMsgSocketType.plain, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + smtpServers: [ + { + port: 3456, + user: "imapout", + password: "imapoutpassword", + isDefault: true, + hostname: "smtp.mail.yahoo.com", + socketType: Ci.nsMsgSocketType.alwaysSTARTTLS, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + }, + ], + }, + // IMAP Yahoo account + Google Server. + { + type: "imap", + port: 2345, + user: "imapuser", + password: "imappassword", + hostname: "imap.mail.yahoo.com", + socketType: Ci.nsMsgSocketType.trySTARTTLS, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + smtpServers: [ + { + port: 3456, + user: "imapout", + password: "imapoutpassword", + isDefault: false, + hostname: "smtp.gmail.com", + socketType: Ci.nsMsgSocketType.alwaysSTARTTLS, + authMethod: Ci.nsMsgAuthMethod.passwordEncrypted, + }, + ], + }, + // IMAP Google account + Yahoo Server. + { + type: "imap", + port: 2345, + user: "imap2user", + password: "imap2password", + hostname: "imap.gmail.com", + socketType: Ci.nsMsgSocketType.trySTARTTLS, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + smtpServers: [ + { + port: 3456, + user: "imapout", + password: "imapoutpassword", + isDefault: false, + hostname: "smtp.mail.yahoo.com", + socketType: Ci.nsMsgSocketType.alwaysSTARTTLS, + authMethod: Ci.nsMsgAuthMethod.passwordEncrypted, + }, + ], + }, + // IMAP Invalid account + Invalid Server. + { + type: "imap", + port: 2345, + user: "imap2user", + password: "imap2password", + hostname: "imap.mail.foo.invalid", + socketType: Ci.nsMsgSocketType.trySTARTTLS, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + smtpServers: [ + { + port: 3456, + user: "imapout", + password: "imapoutpassword", + isDefault: false, + hostname: "smtp.mail.foo.invalid", + socketType: Ci.nsMsgSocketType.alwaysSTARTTLS, + authMethod: Ci.nsMsgAuthMethod.passwordEncrypted, + }, + ], + }, + // AOL IMAP account. + { + type: "imap", + port: 993, + user: "aolimap", + password: "imap2password", + hostname: "imap.aol.com", + socketType: Ci.nsMsgSocketType.SSL, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + smtpServers: [ + { + port: 465, + user: "imapout2", + password: "imapoutpassword2", + isDefault: false, + hostname: "smtp.aol.com", + socketType: Ci.nsMsgSocketType.SSL, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + }, + ], + }, + // AOL POP3 account. + { + type: "pop3", + port: 995, + user: "aolpop3", + password: "abc", + hostname: "pop.aol.com", + socketType: Ci.nsMsgSocketType.SSL, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + smtpServers: [ + { + port: 465, + user: "popout", + password: "aaa", + isDefault: false, + hostname: "smtp.aol.com", + socketType: Ci.nsMsgSocketType.SSL, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + }, + ], + }, + // Google POP3 account. + { + type: "pop3", + port: 995, + user: "gmailpop3", + password: "abc", + hostname: "pop.gmail.com", + socketType: Ci.nsMsgSocketType.trySTARTTLS, + authMethod: Ci.nsMsgAuthMethod.passwordEncrypted, + smtpServers: [ + { + port: 465, + user: "gmailpopout", + password: "aaa", + isDefault: true, + hostname: "smtp.gmail.com", + socketType: Ci.nsMsgAuthMethod.alwaysSTARTTLS, + authMethod: Ci.nsMsgAuthMethod.passwordEncrypted, + }, + ], + }, + // Microsoft IMAP account + { + type: "imap", + port: 993, + user: "msimap", + password: "abc", + hostname: "outlook.office365.com", + socketType: Ci.nsMsgSocketType.SSL, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + smtpServers: [ + { + port: 587, + user: "msimap", + password: "abc", + isDefault: true, + hostname: "smtp.office365.com", + socketType: Ci.nsMsgAuthMethod.alwaysSTARTTLS, + authMethod: Ci.nsMsgAuthMethod.passwordCleartext, + }, + ], + }, +]; + +// An array of the incoming servers created from the setup_accounts() method. +var gIncomingServers = []; + +// An array of the outgoing servers created from the setup_accounts() method. +var gOutgoingServers = []; + +// An array of the accounts created from the setup_accounts() method. +var gAccounts = []; + +/** + * Set up accounts based on the given data. + */ +function setup_accounts() { + for (let details of gAccountList) { + let server = localAccountUtils.create_incoming_server( + details.type, + details.port, + details.user, + details.password, + details.hostname + ); + server.socketType = details.socketType; + server.authMethod = details.authMethod; + + // Add the newly created server to the array for testing. + gIncomingServers.push(server); + + let account = MailServices.accounts.FindAccountForServer(server); + for (let smtpDetails of details.smtpServers) { + let outgoing = localAccountUtils.create_outgoing_server( + smtpDetails.port, + smtpDetails.user, + smtpDetails.password, + smtpDetails.hostname + ); + outgoing.socketType = smtpDetails.socketType; + outgoing.authMethod = smtpDetails.authMethod; + localAccountUtils.associate_servers( + account, + outgoing, + smtpDetails.isDefault + ); + + // Add the newly created server to the array for testing. + gOutgoingServers.push(outgoing); + + // Add the newly created account to the array for cleanup. + gAccounts.push(account); + } + } +} + +add_task(function test_oauth_migration() { + setup_accounts(); + + for (let server of gIncomingServers) { + // Confirm all the incoming servers are not using OAuth2 after the setup. + Assert.notEqual( + server.authMethod, + Ci.nsMsgAuthMethod.OAuth2, + "Incoming server should not use OAuth2" + ); + } + + for (let server of gOutgoingServers) { + // Confirm all the outgoing servers are not using OAuth2 after the setup. + Assert.notEqual( + server.authMethod, + Ci.nsMsgAuthMethod.OAuth2, + "Outgoing server should not use OAuth2" + ); + } + + // Run the migration. + Services.prefs.setIntPref("mail.ui-rdf.version", 21); + MailMigrator._migrateUI(); + + for (let server of gIncomingServers) { + // Confirm only the correct incoming servers are using OAuth2 after migration. + if ( + !server.hostName.endsWith("mail.yahoo.com") && + !server.hostName.endsWith("aol.com") && + !server.hostName.endsWith("gmail.com") && + !server.hostName.endsWith("office365.com") + ) { + Assert.notEqual( + server.authMethod, + Ci.nsMsgAuthMethod.OAuth2, + `Incoming server ${server.hostName} should not use OAuth2 after migration` + ); + continue; + } + + Assert.equal( + server.authMethod, + Ci.nsMsgAuthMethod.OAuth2, + `Incoming server ${server.hostName} should use OAuth2 after migration` + ); + } + + for (let server of gOutgoingServers) { + // Confirm only the correct outgoing servers are using OAuth2 after migration. + if ( + !server.hostname.endsWith("mail.yahoo.com") && + !server.hostname.endsWith("aol.com") && + !server.hostname.endsWith("gmail.com") && + !server.hostname.endsWith("office365.com") + ) { + Assert.notEqual( + server.authMethod, + Ci.nsMsgAuthMethod.OAuth2, + `Outgoing server ${server.hostname} should not use OAuth2 after migration` + ); + continue; + } + + Assert.equal( + server.authMethod, + Ci.nsMsgAuthMethod.OAuth2, + `Outgoing server ${server.hostname} should use OAuth2 after migration` + ); + } + + // Remove our test accounts and servers to leave the profile clean. + for (let account of gAccounts) { + MailServices.accounts.removeAccount(account); + } + + for (let server of gOutgoingServers) { + MailServices.smtp.deleteServer(server); + } +}); diff --git a/comm/mail/base/test/unit/test_treeSelection.js b/comm/mail/base/test/unit/test_treeSelection.js new file mode 100644 index 0000000000..7e1b3193d1 --- /dev/null +++ b/comm/mail/base/test/unit/test_treeSelection.js @@ -0,0 +1,581 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { TreeSelection } = ChromeUtils.importESModule( + "chrome://messenger/content/tree-selection.mjs" +); + +var fakeView = { + rowCount: 101, + selectionChanged() {}, + QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), +}; + +var sel = new TreeSelection(null); +sel.view = fakeView; + +var tree = { + view: fakeView, + + _invalidationCount: 0, + invalidate() { + this._invalidationCount++; + }, + invalidateRange(startIndex, endIndex) { + for (let index = startIndex; index <= endIndex; index++) { + this.invalidateRow(index); + } + }, + _invalidatedRows: [], + invalidateRow(index) { + this._invalidatedRows.push(index); + }, + + assertInvalidated() { + Assert.equal(this._invalidationCount, 1, "invalidated once"); + this._invalidationCount = 0; + this.assertInvalidatedRows(); + }, + assertDidntInvalidate() { + Assert.equal(this._invalidationCount, 0, "didn't invalidate"); + }, + assertInvalidatedRows(expected) { + if (expected) { + this.assertDidntInvalidate(); + } else { + expected = []; + } + let numericSort = (a, b) => a - b; + Assert.deepEqual( + this._invalidatedRows.sort(numericSort), + expected.sort(numericSort), + "invalidated rows" + ); + this._invalidatedRows.length = 0; + }, +}; +sel.tree = tree; + +function createRangeArray(low, high) { + let array = []; + for (let i = low; i <= high; i++) { + array.push(i); + } + return array; +} + +function assertSelectionRanges(expected) { + Assert.deepEqual(sel._ranges, expected, "selected ranges"); +} + +function assertCurrentIndex(index) { + Assert.equal(sel.currentIndex, index, `current index should be ${index}`); +} + +function assertShiftPivot(index) { + Assert.equal( + sel.shiftSelectPivot, + index, + `shift select pivot should be ${index}` + ); +} + +function assertSelected(index) { + Assert.ok(sel.isSelected(index), `${index} should be selected`); +} + +function assertNotSelected(index) { + Assert.ok(!sel.isSelected(index), `${index} should not be selected`); +} + +function run_test() { + // -- select + sel.select(1); + tree.assertInvalidatedRows([1]); + assertSelected(1); + assertNotSelected(0); + assertNotSelected(2); + assertSelectionRanges([[1, 1]]); + assertCurrentIndex(1); + + sel.select(2); + tree.assertInvalidatedRows([1, 2]); + assertSelected(2); + assertNotSelected(1); + assertNotSelected(3); + assertSelectionRanges([[2, 2]]); + assertCurrentIndex(2); + + // -- clearSelection + sel.clearSelection(); + tree.assertInvalidatedRows([2]); + assertSelectionRanges([]); + assertCurrentIndex(2); // should still be the same... + + // -- toggleSelect + // start from nothing + sel.clearSelection(); + tree.assertInvalidatedRows([]); + sel.toggleSelect(1); + tree.assertInvalidatedRows([1]); + assertSelectionRanges([[1, 1]]); + assertCurrentIndex(1); + + // lower fusion + sel.select(2); + tree.assertInvalidatedRows([1, 2]); + sel.toggleSelect(1); + tree.assertInvalidatedRows([1]); + assertSelectionRanges([[1, 2]]); + assertCurrentIndex(1); + + // upper fusion + sel.toggleSelect(3); + tree.assertInvalidatedRows([3]); + assertSelectionRanges([[1, 3]]); + assertCurrentIndex(3); + + // splitting + sel.toggleSelect(2); + tree.assertInvalidatedRows([2]); + assertSelectionRanges([ + [1, 1], + [3, 3], + ]); + assertSelected(1); + assertSelected(3); + assertNotSelected(0); + assertNotSelected(2); + assertNotSelected(4); + assertCurrentIndex(2); + + // merge + sel.toggleSelect(2); + tree.assertInvalidatedRows([2]); + assertSelectionRanges([[1, 3]]); + assertCurrentIndex(2); + + // lower shrinkage + sel.toggleSelect(1); + tree.assertInvalidatedRows([1]); + assertSelectionRanges([[2, 3]]); + assertCurrentIndex(1); + + // upper shrinkage + sel.toggleSelect(3); + tree.assertInvalidatedRows([3]); + assertSelectionRanges([[2, 2]]); + assertCurrentIndex(3); + + // nukage + sel.toggleSelect(2); + tree.assertInvalidatedRows([2]); + assertSelectionRanges([]); + assertCurrentIndex(2); + + // -- rangedSelect + // simple non-augment + sel.rangedSelect(0, 0, false); + tree.assertInvalidatedRows([0]); + assertSelectionRanges([[0, 0]]); + assertShiftPivot(0); + assertCurrentIndex(0); + + // slightly less simple non-augment + sel.rangedSelect(2, 4, false); + tree.assertInvalidatedRows([0, 2, 3, 4]); + assertSelectionRanges([[2, 4]]); + assertShiftPivot(2); + assertCurrentIndex(4); + + // higher distinct range + sel.rangedSelect(7, 9, true); + tree.assertInvalidatedRows([7, 8, 9]); + assertSelectionRanges([ + [2, 4], + [7, 9], + ]); + assertShiftPivot(7); + assertCurrentIndex(9); + + // lower distinct range + sel.rangedSelect(0, 0, true); + tree.assertInvalidatedRows([0]); + assertSelectionRanges([ + [0, 0], + [2, 4], + [7, 9], + ]); + assertShiftPivot(0); + assertCurrentIndex(0); + + // lower fusion + sel.rangedSelect(6, 6, true); + tree.assertInvalidatedRows([6, 7, 8, 9]); // Ideally this would just be 6. + assertSelectionRanges([ + [0, 0], + [2, 4], + [6, 9], + ]); + assertShiftPivot(6); + assertCurrentIndex(6); + + // upper fusion + sel.rangedSelect(10, 11, true); + tree.assertInvalidatedRows([6, 7, 8, 9, 10, 11]); // 10, 11 + assertSelectionRanges([ + [0, 0], + [2, 4], + [6, 11], + ]); + assertShiftPivot(10); + assertCurrentIndex(11); + + // notch merge + sel.rangedSelect(5, 5, true); + tree.assertInvalidatedRows([2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); // 5 + assertSelectionRanges([ + [0, 0], + [2, 11], + ]); + assertShiftPivot(5); + assertCurrentIndex(5); + + // ambiguous consume with merge + sel.rangedSelect(0, 5, true); + tree.assertInvalidatedRows(createRangeArray(0, 11)); // 1 + assertSelectionRanges([[0, 11]]); + assertShiftPivot(0); + assertCurrentIndex(5); + + // aligned consumption + sel.rangedSelect(0, 15, true); + tree.assertInvalidatedRows(createRangeArray(0, 15)); // 12, 13, 14, 15 + assertSelectionRanges([[0, 15]]); + assertShiftPivot(0); + assertCurrentIndex(15); + + // excessive consumption + sel.rangedSelect(5, 7, false); + tree.assertInvalidatedRows(createRangeArray(0, 15)); // 0 to 4, 8 to 15 + sel.rangedSelect(3, 10, true); + tree.assertInvalidatedRows([3, 4, 5, 6, 7, 8, 9, 10]); // 3, 4, 8, 9, 10 + assertSelectionRanges([[3, 10]]); + assertShiftPivot(3); + assertCurrentIndex(10); + + // overlap merge + sel.rangedSelect(5, 10, false); + tree.assertInvalidatedRows([3, 4, 5, 6, 7, 8, 9, 10]); // 3, 4 + sel.rangedSelect(15, 20, true); + tree.assertInvalidatedRows([15, 16, 17, 18, 19, 20]); + sel.rangedSelect(7, 17, true); + tree.assertInvalidatedRows(createRangeArray(5, 20)); // 11, 12, 13, 14 + assertSelectionRanges([[5, 20]]); + assertShiftPivot(7); + assertCurrentIndex(17); + + // big merge and consume + sel.rangedSelect(5, 10, false); + tree.assertInvalidatedRows(createRangeArray(5, 20)); // 11 to 20 + sel.rangedSelect(15, 20, true); + tree.assertInvalidatedRows([15, 16, 17, 18, 19, 20]); + sel.rangedSelect(25, 30, true); + tree.assertInvalidatedRows([25, 26, 27, 28, 29, 30]); + sel.rangedSelect(35, 40, true); + tree.assertInvalidatedRows([35, 36, 37, 38, 39, 40]); + sel.rangedSelect(7, 37, true); + tree.assertInvalidatedRows(createRangeArray(5, 40)); // 11 to 14, 21 to 24, 31 to 34 + assertSelectionRanges([[5, 40]]); + assertShiftPivot(7); + assertCurrentIndex(37); + + // broad lower fusion + sel.rangedSelect(10, 20, false); + tree.assertInvalidatedRows(createRangeArray(5, 40)); // 5 to 9, 21 to 40 + sel.rangedSelect(3, 15, true); + tree.assertInvalidatedRows(createRangeArray(3, 20)); // 3 to 9 + assertSelectionRanges([[3, 20]]); + assertShiftPivot(3); + assertCurrentIndex(15); + + // -- clearRange + sel.rangedSelect(10, 30, false); + tree.assertInvalidatedRows(createRangeArray(3, 30)); // 3 to 9, 21 to 30 + + // irrelevant low + sel.clearRange(0, 5); + tree.assertInvalidatedRows([]); + assertSelectionRanges([[10, 30]]); + + // irrelevant high + sel.clearRange(40, 45); + tree.assertInvalidatedRows([]); + assertSelectionRanges([[10, 30]]); + + // lower shrinkage tight + sel.clearRange(10, 10); + tree.assertInvalidatedRows([10]); + assertSelectionRanges([[11, 30]]); + + // lower shrinkage broad + sel.clearRange(0, 13); + tree.assertInvalidatedRows(createRangeArray(0, 13)); // 11, 12, 13 + assertSelectionRanges([[14, 30]]); + + // upper shrinkage tight + sel.clearRange(30, 30); + tree.assertInvalidatedRows([30]); + assertSelectionRanges([[14, 29]]); + + // upper shrinkage broad + sel.clearRange(27, 50); + tree.assertInvalidatedRows(createRangeArray(27, 50)); // 27, 28, 29 + assertSelectionRanges([[14, 26]]); + + // split tight + sel.clearRange(20, 20); + tree.assertInvalidatedRows([20]); + assertSelectionRanges([ + [14, 19], + [21, 26], + ]); + + // split broad + sel.toggleSelect(20); + tree.assertInvalidatedRows([20]); + sel.clearRange(19, 21); + tree.assertInvalidatedRows([19, 20, 21]); + assertSelectionRanges([ + [14, 18], + [22, 26], + ]); + + // hit two with tight shrinkage + sel.clearRange(18, 22); + tree.assertInvalidatedRows([18, 19, 20, 21, 22]); // 18, 22 + assertSelectionRanges([ + [14, 17], + [23, 26], + ]); + + // hit two with broad shrinkage + sel.clearRange(15, 25); + tree.assertInvalidatedRows(createRangeArray(15, 25)); // 15, 16, 17, 23, 24, 25 + assertSelectionRanges([ + [14, 14], + [26, 26], + ]); + + // obliterate + sel.clearRange(0, 100); + tree.assertInvalidatedRows(createRangeArray(0, 100)); // 14, 26 + assertSelectionRanges([]); + + // multi-obliterate + sel.rangedSelect(10, 20, true); + tree.assertInvalidatedRows(createRangeArray(10, 20)); + sel.rangedSelect(30, 40, true); + tree.assertInvalidatedRows(createRangeArray(30, 40)); + sel.clearRange(0, 100); + tree.assertInvalidatedRows(createRangeArray(0, 100)); // 10 to 20, 30 to 40 + assertSelectionRanges([]); + + // obliterate with shrinkage + sel.rangedSelect(5, 10, true); + tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]); + sel.rangedSelect(15, 20, true); + tree.assertInvalidatedRows([15, 16, 17, 18, 19, 20]); + sel.rangedSelect(25, 30, true); + tree.assertInvalidatedRows([25, 26, 27, 28, 29, 30]); + sel.rangedSelect(35, 40, true); + tree.assertInvalidatedRows([35, 36, 37, 38, 39, 40]); + sel.clearRange(7, 37); + tree.assertInvalidatedRows(createRangeArray(7, 37)); // 7 to 10, 15 to 20, 25 to 30, 35 to 37 + assertSelectionRanges([ + [5, 6], + [38, 40], + ]); + + // -- selectAll + sel.selectAll(); + tree.assertInvalidated(); + assertSelectionRanges([[0, 100]]); + + // -- adjustSelection + // bump due to addition on simple select + sel.select(5); + tree.assertInvalidatedRows(createRangeArray(0, 100)); + sel.adjustSelection(5, 1); + tree.assertInvalidatedRows(createRangeArray(5, 100)); + assertSelectionRanges([[6, 6]]); + assertCurrentIndex(6); + + sel.select(5); + tree.assertInvalidatedRows([5, 6]); + sel.adjustSelection(0, 1); + tree.assertInvalidatedRows(createRangeArray(0, 100)); + assertSelectionRanges([[6, 6]]); + assertCurrentIndex(6); + + // bump due to addition on ranged simple select + sel.rangedSelect(5, 5, false); + tree.assertInvalidatedRows([5, 6]); + sel.adjustSelection(5, 1); + tree.assertInvalidatedRows(createRangeArray(5, 100)); + assertSelectionRanges([[6, 6]]); + assertShiftPivot(6); + assertCurrentIndex(6); + + sel.rangedSelect(5, 5, false); + tree.assertInvalidatedRows([5, 6]); + sel.adjustSelection(0, 1); + tree.assertInvalidatedRows(createRangeArray(0, 100)); + assertSelectionRanges([[6, 6]]); + assertShiftPivot(6); + assertCurrentIndex(6); + + // bump due to addition on ranged select + sel.rangedSelect(5, 7, false); + tree.assertInvalidatedRows([5, 6, 7]); + sel.adjustSelection(5, 1); + tree.assertInvalidatedRows(createRangeArray(5, 100)); + assertSelectionRanges([[6, 8]]); + assertShiftPivot(6); + assertCurrentIndex(8); + + // no-op with addition + sel.rangedSelect(0, 3, false); + tree.assertInvalidatedRows([0, 1, 2, 3, 6, 7, 8]); + sel.adjustSelection(10, 1); + tree.assertInvalidatedRows(createRangeArray(10, 100)); + assertSelectionRanges([[0, 3]]); + assertShiftPivot(0); + assertCurrentIndex(3); + + // split due to addition + sel.rangedSelect(5, 6, false); + tree.assertInvalidatedRows([0, 1, 2, 3, 5, 6]); + sel.adjustSelection(6, 1); + tree.assertInvalidatedRows(createRangeArray(6, 100)); + assertSelectionRanges([ + [5, 5], + [7, 7], + ]); + assertShiftPivot(5); + assertCurrentIndex(7); + + // shift due to removal on simple select + sel.select(5); + tree.assertInvalidatedRows([5, 7]); + sel.adjustSelection(0, -1); + tree.assertInvalidatedRows(createRangeArray(0, 100)); + assertSelectionRanges([[4, 4]]); + assertCurrentIndex(4); + + // shift due to removal on ranged simple select + sel.rangedSelect(5, 5, false); + tree.assertInvalidatedRows([4, 5]); + sel.adjustSelection(0, -1); + tree.assertInvalidatedRows(createRangeArray(0, 100)); + assertSelectionRanges([[4, 4]]); + assertShiftPivot(4); + assertCurrentIndex(4); + + // nuked due to removal on simple select + sel.select(5); + tree.assertInvalidatedRows([4, 5]); + sel.adjustSelection(5, -1); + tree.assertInvalidatedRows(createRangeArray(5, 100)); + assertSelectionRanges([]); + assertCurrentIndex(-1); + + // upper tight shrinkage due to removal + sel.rangedSelect(5, 10, false); + tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]); + sel.adjustSelection(10, -1); + tree.assertInvalidatedRows(createRangeArray(10, 100)); + assertSelectionRanges([[5, 9]]); + assertShiftPivot(5); + assertCurrentIndex(-1); + + // upper broad shrinkage due to removal + sel.rangedSelect(5, 10, false); + tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]); + sel.adjustSelection(6, -10); + tree.assertInvalidatedRows(createRangeArray(6, 100)); + assertSelectionRanges([[5, 5]]); + assertShiftPivot(5); + assertCurrentIndex(-1); + + // lower tight shrinkage due to removal + sel.rangedSelect(5, 10, false); + tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]); + sel.adjustSelection(5, -1); + tree.assertInvalidatedRows(createRangeArray(5, 100)); + assertSelectionRanges([[5, 9]]); + assertShiftPivot(-1); + assertCurrentIndex(9); + + // lower broad shrinkage due to removal + sel.rangedSelect(5, 10, false); + tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]); + sel.adjustSelection(0, -10); + tree.assertInvalidatedRows(createRangeArray(0, 100)); + assertSelectionRanges([[0, 0]]); + assertShiftPivot(-1); + assertCurrentIndex(0); + + // tight nuke due to removal + sel.rangedSelect(5, 10, false); + tree.assertInvalidatedRows([0, 5, 6, 7, 8, 9, 10]); + sel.adjustSelection(5, -6); + tree.assertInvalidatedRows(createRangeArray(5, 100)); + assertSelectionRanges([]); + assertShiftPivot(-1); + assertCurrentIndex(-1); + + // broad nuke due to removal + sel.rangedSelect(5, 10, false); + tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]); + sel.adjustSelection(0, -20); + tree.assertInvalidatedRows(createRangeArray(0, 100)); + assertSelectionRanges([]); + assertShiftPivot(-1); + assertCurrentIndex(-1); + + // duplicateSelection (please keep this right at the end, as this modifies + // sel) + // no guarantees for the shift pivot yet, so don't test that + let oldSel = sel; + let newSel = new TreeSelection(null); + newSel.view = fakeView; + // multiple selections + oldSel.rangedSelect(1, 3, false); + oldSel.rangedSelect(5, 5, true); + oldSel.rangedSelect(10, 10, true); + oldSel.rangedSelect(6, 7, true); + + oldSel.duplicateSelection(newSel); + // from now on we're only going to be checking newSel + sel = newSel; + assertSelectionRanges([ + [1, 3], + [5, 7], + [10, 10], + ]); + assertCurrentIndex(7); + + // single selection + oldSel.select(4); + oldSel.duplicateSelection(newSel); + assertSelectionRanges([[4, 4]]); + assertCurrentIndex(4); + + // nothing selected + oldSel.clearSelection(); + oldSel.duplicateSelection(newSel); + assertSelectionRanges([]); + assertCurrentIndex(4); +} diff --git a/comm/mail/base/test/unit/test_viewWrapper_imapFolder.js b/comm/mail/base/test/unit/test_viewWrapper_imapFolder.js new file mode 100644 index 0000000000..5e91f587fc --- /dev/null +++ b/comm/mail/base/test/unit/test_viewWrapper_imapFolder.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 DBViewWrapper against a single imap folder. Try and test all the + * features we can without having a fake newsgroup. (Some features are + * newsgroup specific.) + */ + +/* import-globals-from resources/viewWrapperTestUtils.js */ +load("resources/viewWrapperTestUtils.js"); +initViewWrapperTestUtils({ mode: "imap", offline: false }); + +/** + * Create an empty folder, inject messages into it without triggering an + * updateFolder, sanity check that we believe there are no messages in the + * folder, then enter, making sure we immediately enter and that the view + * properly updates to reflect there being the right set of messages. + * (It will fail to update if the db change listener ended up detaching itself + * and not reattaching correctly when the updateFolder completes.) + */ +add_task( + async function test_enter_imap_folder_requiring_update_folder_immediately() { + // - create the folder and wait for the IMAP op to complete + let folderHandle = await messageInjection.makeEmptyFolder(); + let msgFolder = messageInjection.getRealInjectionFolder(folderHandle); + + // - add the messages + let [msgSet] = await messageInjection.makeNewSetsInFolders( + [folderHandle], + [{ count: 1 }], + true + ); + + let viewWrapper = make_view_wrapper(); + + // - make sure we don't know about the message! + Assert.equal(msgFolder.getTotalMessages(false), 0); + + // - sync open the folder, verify we claim we entered, and make sure it has + // nothing in it! + viewWrapper.listener.pendingLoad = true; + viewWrapper.open(msgFolder); + Assert.ok(viewWrapper._enteredFolder); + verify_empty_view(viewWrapper); + + // Wait for all the messages to load. + await gMockViewWrapperListener.promise; + gMockViewWrapperListener.resetPromise(); + + // - make sure the view sees the message though... + verify_messages_in_view(msgSet, viewWrapper); + } +); diff --git a/comm/mail/base/test/unit/test_viewWrapper_logic.js b/comm/mail/base/test/unit/test_viewWrapper_logic.js new file mode 100644 index 0000000000..ef56cb3997 --- /dev/null +++ b/comm/mail/base/test/unit/test_viewWrapper_logic.js @@ -0,0 +1,359 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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("../../../../mailnews/resources/abSetup.js"); + +/* import-globals-from resources/viewWrapperTestUtils.js */ +load("resources/viewWrapperTestUtils.js"); +initViewWrapperTestUtils({ mode: "local" }); + +/** + * Verify that flipping between threading and grouped by sort settings properly + * clears the other flag. (Because they're mutually exclusive, you see.) + */ +add_task(async function test_threading_grouping_mutual_exclusion() { + let viewWrapper = make_view_wrapper(); + let folder = await messageInjection.makeEmptyFolder(); + + await view_open(viewWrapper, folder); + // enter an update that will never conclude. this is fine. + viewWrapper.beginViewUpdate(); + viewWrapper.showThreaded = true; + assert_true(viewWrapper.showThreaded, "view should be threaded"); + assert_false( + viewWrapper.showGroupedBySort, + "view should not be grouped by sort" + ); + + viewWrapper.showGroupedBySort = true; + assert_false(viewWrapper.showThreaded, "view should not be threaded"); + assert_true(viewWrapper.showGroupedBySort, "view should be grouped by sort"); +}); + +/** + * Verify that flipping between the "View... Threads..." menu cases supported by + * |showUnreadOnly| / |specialViewThreadsWithUnread| / + * |specialViewThreadsWithUnread| has them all be properly mutually exclusive. + */ +add_task(async function test_threads_special_views_mutual_exclusion() { + let viewWrapper = make_view_wrapper(); + let folder = await messageInjection.makeEmptyFolder(); + + await view_open(viewWrapper, folder); + // enter an update that will never conclude. this is fine. + viewWrapper.beginViewUpdate(); + + // turn on the special view, make sure we think it took + viewWrapper.specialViewThreadsWithUnread = true; + Assert.ok(viewWrapper.specialViewThreadsWithUnread); + Assert.ok(!viewWrapper.specialViewWatchedThreadsWithUnread); + + // hit showUnreadOnly which should already be false, so this makes sure that + // just writing to it forces the special view off. + viewWrapper.showUnreadOnly = false; + Assert.ok(!viewWrapper.showUnreadOnly); + Assert.ok(!viewWrapper.specialViewThreadsWithUnread); + Assert.ok(!viewWrapper.specialViewWatchedThreadsWithUnread); + + // turn on the other special view + viewWrapper.specialViewWatchedThreadsWithUnread = true; + Assert.ok(!viewWrapper.specialViewThreadsWithUnread); + Assert.ok(viewWrapper.specialViewWatchedThreadsWithUnread); + + // turn on show unread only mode, make sure special view is cleared + viewWrapper.showUnreadOnly = true; + Assert.ok(viewWrapper.showUnreadOnly); + Assert.ok(!viewWrapper.specialViewThreadsWithUnread); + Assert.ok(!viewWrapper.specialViewWatchedThreadsWithUnread); + + // turn off show unread only mode just to make sure the transition happens + viewWrapper.showUnreadOnly = false; + Assert.ok(!viewWrapper.showUnreadOnly); + Assert.ok(!viewWrapper.specialViewThreadsWithUnread); + Assert.ok(!viewWrapper.specialViewWatchedThreadsWithUnread); +}); + +/** + * Do a quick test of primary sorting to make sure we're actually changing the + * sort order. (However, we are not responsible for verifying correctness of + * the sort.) + */ +add_task(async function test_sort_primary() { + let viewWrapper = make_view_wrapper(); + // we need to put messages in the folder or the sort logic doesn't actually + // save the sort state. (this is the C++ view's fault.) + let [[folder]] = await messageInjection.makeFoldersWithSets(1, [{}]); + + await view_open(viewWrapper, folder); + viewWrapper.sort( + Ci.nsMsgViewSortType.byDate, + Ci.nsMsgViewSortOrder.ascending + ); + assert_equals( + viewWrapper.dbView.sortType, + Ci.nsMsgViewSortType.byDate, + "sort should be by date", + true + ); + assert_equals( + viewWrapper.dbView.sortOrder, + Ci.nsMsgViewSortOrder.ascending, + "sort order should be ascending", + true + ); + + viewWrapper.sort( + Ci.nsMsgViewSortType.byAuthor, + Ci.nsMsgViewSortOrder.descending + ); + assert_equals( + viewWrapper.dbView.sortType, + Ci.nsMsgViewSortType.byAuthor, + "sort should be by author", + true + ); + assert_equals( + viewWrapper.dbView.sortOrder, + Ci.nsMsgViewSortOrder.descending, + "sort order should be descending", + true + ); +}); + +/** + * Verify that we handle explicit secondary sorts correctly. + */ +add_task(async function test_sort_secondary_explicit() { + let viewWrapper = make_view_wrapper(); + // we need to put messages in the folder or the sort logic doesn't actually + // save the sort state. (this is the C++ view's fault.) + let [[folder]] = await messageInjection.makeFoldersWithSets(1, [{}]); + + await view_open(viewWrapper, folder); + viewWrapper.sort( + Ci.nsMsgViewSortType.byAuthor, + Ci.nsMsgViewSortOrder.ascending, + Ci.nsMsgViewSortType.bySubject, + Ci.nsMsgViewSortOrder.descending + ); + // check once for what we just did, then again after refreshing to make + // sure the sort order 'stuck' + for (let i = 0; i < 2; i++) { + assert_equals( + viewWrapper.dbView.sortType, + Ci.nsMsgViewSortType.byAuthor, + "sort should be by author" + ); + assert_equals( + viewWrapper.dbView.sortOrder, + Ci.nsMsgViewSortOrder.ascending, + "sort order should be ascending" + ); + assert_equals( + viewWrapper.dbView.secondarySortType, + Ci.nsMsgViewSortType.bySubject, + "secondary sort should be by subject" + ); + assert_equals( + viewWrapper.dbView.secondarySortOrder, + Ci.nsMsgViewSortOrder.descending, + "secondary sort order should be descending" + ); + await view_refresh(viewWrapper); + } +}); + +/** + * Verify that we handle implicit secondary sorts correctly. + * An implicit secondary sort is when we sort by Y, then we sort by X, and it's + * okay to have the effective sort of [X, Y]. The UI has/wants this, so, uh, + * let's make sure we obey its assumptions unless we have gone and made the UI + * be explicit about these things. We can't simply depend on the view to do + * this for us. Why? Because we re-create the view all the bloody time. + */ +add_task(async function test_sort_secondary_implicit() { + let viewWrapper = make_view_wrapper(); + // we need to put messages in the folder or the sort logic doesn't actually + // save the sort state. (this is the C++ view's fault.) + let [[folder]] = await messageInjection.makeFoldersWithSets(1, [{}]); + + await view_open(viewWrapper, folder); + viewWrapper.magicSort( + Ci.nsMsgViewSortType.bySubject, + Ci.nsMsgViewSortOrder.descending + ); + viewWrapper.magicSort( + Ci.nsMsgViewSortType.byAuthor, + Ci.nsMsgViewSortOrder.ascending + ); + // check once for what we just did, then again after refreshing to make + // sure the sort order 'stuck' + for (let i = 0; i < 2; i++) { + assert_equals( + viewWrapper.dbView.sortType, + Ci.nsMsgViewSortType.byAuthor, + "sort should be by author" + ); + assert_equals( + viewWrapper.dbView.sortOrder, + Ci.nsMsgViewSortOrder.ascending, + "sort order should be ascending" + ); + assert_equals( + viewWrapper.dbView.secondarySortType, + Ci.nsMsgViewSortType.bySubject, + "secondary sort should be by subject" + ); + assert_equals( + viewWrapper.dbView.secondarySortOrder, + Ci.nsMsgViewSortOrder.descending, + "secondary sort order should be descending" + ); + await view_refresh(viewWrapper); + } +}); + +/** + * Test that group-by-sort does not explode even if we try and get it to use + * sorts that are illegal for group-by-sort mode. It is important that we + * test both illegal primary sorts (fixed a while back) plus illegal + * secondary sorts (fixing now). + * + * Note: Sorting changes are synchronous, but toggling grouped by sort requires + * a view rebuild. + */ +add_task(async function test_sort_group_by_sort() { + let viewWrapper = make_view_wrapper(); + // we need to put messages in the folder or the sort logic doesn't actually + // save the sort state. (this is the C++ view's fault.) + let [[folder]] = await messageInjection.makeFoldersWithSets(1, [{}]); + await view_open(viewWrapper, folder); + + // - start out by being in an illegal (for group-by-sort) sort mode and + // switch to group-by-sort. + // (sorting changes are synchronous) + viewWrapper.sort(Ci.nsMsgViewSortType.byId, Ci.nsMsgViewSortOrder.descending); + await view_group_by_sort(viewWrapper, true); + + // there should have been no explosion, and we should have changed to date + assert_equals( + viewWrapper.primarySortType, + Ci.nsMsgViewSortType.byDate, + "sort should have reset to date" + ); + + // - return to unthreaded, have an illegal secondary sort, go group-by-sort + await view_group_by_sort(viewWrapper, false); + + viewWrapper.sort( + Ci.nsMsgViewSortType.byDate, + Ci.nsMsgViewSortOrder.descending, + Ci.nsMsgViewSortType.byId, + Ci.nsMsgViewSortOrder.descending + ); + + await view_group_by_sort(viewWrapper, true); + // we should now only have a single sort type and it should be date + assert_equals( + viewWrapper._sort.length, + 1, + "we should only have one sort type active" + ); + assert_equals( + viewWrapper.primarySortType, + Ci.nsMsgViewSortType.byDate, + "remaining (primary) sort type should be date" + ); + + // - try and make group-by-sort sort by something illegal + // (we're still in group-by-sort mode) + viewWrapper.magicSort( + Ci.nsMsgViewSortType.byId, + Ci.nsMsgViewSortOrder.descending + ); + assert_equals( + viewWrapper.primarySortType, + Ci.nsMsgViewSortType.byDate, + "remaining (primary) sort type should be date" + ); +}); + +/** + * Verify that mailview changes are properly persisted but that we only use them + * when the listener indicates we should use them (because the widget is + * presumably visible). + */ +add_task(async function test_mailviews_persistence() { + let viewWrapper = make_view_wrapper(); + let folder = await messageInjection.makeEmptyFolder(); + + // open the folder, ensure it is using the default mail view + await view_open(viewWrapper, folder); + Assert.equal(viewWrapper.mailViewIndex, MailViewConstants.kViewItemAll); + + // set the view so as to be persisted + viewWrapper.setMailView(MailViewConstants.kViewItemUnread); + // ...but first make sure it took at all + Assert.equal(viewWrapper.mailViewIndex, MailViewConstants.kViewItemUnread); + + // close, re-open and verify it took + viewWrapper.close(); + await view_open(viewWrapper, folder); + Assert.equal(viewWrapper.mailViewIndex, MailViewConstants.kViewItemUnread); + + // close, turn off the mailview usage indication by the listener... + viewWrapper.close(); + gMockViewWrapperListener.shouldUseMailViews = false; + // ...open and verify that it did not take! + await view_open(viewWrapper, folder); + Assert.equal(viewWrapper.mailViewIndex, MailViewConstants.kViewItemAll); + + // put the mailview setting back so other tests work + gMockViewWrapperListener.shouldUseMailViews = true; +}); + +/** + * Make sure: + * - View update depth increments / decrements as expected, and triggers a + * view rebuild when expected. + * - View update depth can't go below zero resulting in odd happenings. + * - That the view update depth is zeroed by a close so that we don't + * get into awkward states. + * + * @bug 498145 + */ +add_task(function test_view_update_depth_logic() { + let viewWrapper = make_view_wrapper(); + + // create an instance-specific dummy method that counts calls t + // _applyViewChanges + let applyViewCount = 0; + viewWrapper._applyViewChanges = function () { + applyViewCount++; + }; + + // - view update depth basics + Assert.equal(viewWrapper._viewUpdateDepth, 0); + viewWrapper.beginViewUpdate(); + Assert.equal(viewWrapper._viewUpdateDepth, 1); + viewWrapper.beginViewUpdate(); + Assert.equal(viewWrapper._viewUpdateDepth, 2); + viewWrapper.endViewUpdate(); + Assert.equal(applyViewCount, 0); + Assert.equal(viewWrapper._viewUpdateDepth, 1); + viewWrapper.endViewUpdate(); + Assert.equal(applyViewCount, 1); + Assert.equal(viewWrapper._viewUpdateDepth, 0); + + // - don't go below zero! (and don't trigger.) + applyViewCount = 0; + viewWrapper.endViewUpdate(); + Assert.equal(applyViewCount, 0); + Assert.equal(viewWrapper._viewUpdateDepth, 0); + + // - depth zeroed on clear + viewWrapper.beginViewUpdate(); + viewWrapper.close(); // this does little else because there is nothing open + Assert.equal(viewWrapper._viewUpdateDepth, 0); +}); diff --git a/comm/mail/base/test/unit/test_viewWrapper_realFolder.js b/comm/mail/base/test/unit/test_viewWrapper_realFolder.js new file mode 100644 index 0000000000..fbcef1abe8 --- /dev/null +++ b/comm/mail/base/test/unit/test_viewWrapper_realFolder.js @@ -0,0 +1,666 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 DBViewWrapper against a single local folder. Try and test all the + * features we can without having a fake newsgroup. (Some features are + * newsgroup specific.) + */ + +/* import-globals-from resources/viewWrapperTestUtils.js */ +load("resources/viewWrapperTestUtils.js"); +initViewWrapperTestUtils({ mode: "local" }); + +var { SyntheticMessageSet } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +/* ===== Real Folder, no features ===== */ + +/** + * Open a pre-populated real folder, make sure all the messages show up. + */ +add_task(async function test_real_folder_load() { + let viewWrapper = make_view_wrapper(); + let [[msgFolder], msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + viewWrapper.open(msgFolder); + verify_messages_in_view(msgSet, viewWrapper); + Assert.ok("test ran to completion"); +}); + +/** + * Open a real folder, add some messages, make sure they show up, remove some + * messages, make sure they go away. + */ +add_task(async function test_real_folder_update() { + let viewWrapper = make_view_wrapper(); + + // start with an empty folder + let msgFolder = await messageInjection.makeEmptyFolder(); + viewWrapper.open(msgFolder); + verify_empty_view(viewWrapper); + + // add messages (none -> some) + let [setOne] = await messageInjection.makeNewSetsInFolders([msgFolder], [{}]); + verify_messages_in_view(setOne, viewWrapper); + + // add more messages! (some -> more) + let [setTwo] = await messageInjection.makeNewSetsInFolders([msgFolder], [{}]); + verify_messages_in_view([setOne, setTwo], viewWrapper); + + // remove the first set of messages (more -> some) + await messageInjection.trashMessages(setOne); + verify_messages_in_view(setTwo, viewWrapper); + + // remove the second set of messages (some -> none) + await messageInjection.trashMessages(setTwo); + verify_empty_view(viewWrapper); +}); + +/** + * Open a real folder, verify, open another folder, verify. We are testing + * ability to change folders without exploding. + */ +add_task(async function test_real_folder_load_after_real_folder_load() { + let viewWrapper = make_view_wrapper(); + + let [[folderOne], setOne] = await messageInjection.makeFoldersWithSets(1, [ + {}, + ]); + viewWrapper.open(folderOne); + verify_messages_in_view(setOne, viewWrapper); + + let [[folderTwo], setTwo] = await messageInjection.makeFoldersWithSets(1, [ + {}, + ]); + viewWrapper.open(folderTwo); + verify_messages_in_view(setTwo, viewWrapper); +}); + +/* ===== Real Folder, Threading Modes ==== */ +/* + * The first three tests that verify setting the threading flags has the + * expected outcome do this by creating the view from scratch with the view + * flags applied. The view threading persistence test handles making sure + * that changes in threading on-the-fly work from the perspective of the + * bits and what not. None of these are tests of the view implementation's + * threading/grouping logic, just sanity checking that we are doing the right + * thing. + */ + +add_task(async function test_real_folder_threading_unthreaded() { + let viewWrapper = make_view_wrapper(); + let folder = await messageInjection.makeEmptyFolder(); + + // create a single maximally nested thread. + const count = 10; + let messageSet = new SyntheticMessageSet( + gMessageScenarioFactory.directReply(count) + ); + await messageInjection.addSetsToFolders([folder], [messageSet]); + + // verify that we are not threaded (or grouped) + viewWrapper.open(folder); + viewWrapper.beginViewUpdate(); + viewWrapper.showUnthreaded = true; + // whitebox test view flags (we've gotten them wrong before...) + assert_bit_not_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + "View threaded bit should not be set." + ); + assert_bit_not_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kGroupBySort, + "View group-by-sort bit should not be set." + ); + viewWrapper.endViewUpdate(); + verify_view_level_histogram({ 0: count }, viewWrapper); +}); + +add_task(async function test_real_folder_threading_threaded() { + let viewWrapper = make_view_wrapper(); + let folder = await messageInjection.makeEmptyFolder(); + + // create a single maximally nested thread. + const count = 10; + let messageSet = new SyntheticMessageSet( + gMessageScenarioFactory.directReply(count) + ); + await messageInjection.addSetsToFolders([folder], [messageSet]); + + // verify that we are threaded (in such a way that we can't be grouped) + viewWrapper.open(folder); + viewWrapper.beginViewUpdate(); + viewWrapper.showThreaded = true; + // whitebox test view flags (we've gotten them wrong before...) + assert_bit_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + "View threaded bit should be set." + ); + assert_bit_not_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kGroupBySort, + "View group-by-sort bit should not be set." + ); + // expand everything so our logic below works. + view_expand_all(viewWrapper); + viewWrapper.endViewUpdate(); + // blackbox test view flags: make sure IsContainer is true for the root + verify_view_row_at_index_is_container(viewWrapper, 0); + // do the histogram test to verify threading... + let expectedHisto = {}; + for (let i = 0; i < count; i++) { + expectedHisto[i] = 1; + } + verify_view_level_histogram(expectedHisto, viewWrapper); +}); + +add_task(async function test_real_folder_threading_grouped_by_sort() { + let viewWrapper = make_view_wrapper(); + + // create some messages that belong to the 'in this week' bucket when sorting + // by date and grouping by date. + const count = 5; + let [[folder]] = await messageInjection.makeFoldersWithSets(1, [ + { count, age: { days: 2 }, age_incr: { mins: 1 } }, + ]); + + // group-by-sort sorted by date + viewWrapper.open(folder); + viewWrapper.beginViewUpdate(); + viewWrapper.showGroupedBySort = true; + // whitebox test view flags (we've gotten them wrong before...) + assert_bit_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + "View threaded bit should be set." + ); + assert_bit_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kGroupBySort, + "View group-by-sort bit should be set." + ); + viewWrapper.sort( + Ci.nsMsgViewSortType.byDate, + Ci.nsMsgViewSortOrder.ascending + ); + // expand everyone + view_expand_all(viewWrapper); + viewWrapper.endViewUpdate(); + + // make sure the level depths are correct + verify_view_level_histogram({ 0: 1, 1: count }, viewWrapper); + // and make sure the first dude is a dummy + verify_view_row_at_index_is_dummy(viewWrapper, 0); +}); + +/** + * Verify that we the threading modes are persisted. We are only checking + * flags here; we trust the previous tests to have done their job. + */ +add_task(async function test_real_folder_threading_persistence() { + let viewWrapper = make_view_wrapper(); + let folder = await messageInjection.makeEmptyFolder(); + + // create a single maximally nested thread. + const count = 10; + let messageSet = new SyntheticMessageSet( + gMessageScenarioFactory.directReply(count) + ); + await messageInjection.addSetsToFolders([folder], [messageSet]); + + // open the folder, set threaded mode, close it + viewWrapper.open(folder); + viewWrapper.showThreaded = true; // should be instantaneous + verify_view_row_at_index_is_container(viewWrapper, 0); + assert_bit_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + "View threaded bit should be set." + ); + assert_bit_not_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kGroupBySort, + "View group-by-sort bit should not be set." + ); + viewWrapper.close(); + + // open it again, make sure we're threaded, go unthreaded, close + viewWrapper.open(folder); + assert_true(viewWrapper.showThreaded, "view should be threaded"); + assert_false(viewWrapper.showUnthreaded, "view is lying about threading"); + assert_false(viewWrapper.showGroupedBySort, "view is lying about threading"); + verify_view_row_at_index_is_container(viewWrapper, 0); + assert_bit_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + "View threaded bit should be set." + ); + assert_bit_not_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kGroupBySort, + "View group-by-sort bit should not be set." + ); + + viewWrapper.showUnthreaded = true; + assert_bit_not_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + "View threaded bit should not be set." + ); + assert_bit_not_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kGroupBySort, + "View group-by-sort bit should not be set." + ); + viewWrapper.close(); + + // open it again, make sure we're unthreaded, go grouped, close + viewWrapper.open(folder); + assert_true(viewWrapper.showUnthreaded, "view should be unthreaded"); + assert_false(viewWrapper.showThreaded, "view is lying about threading"); + assert_false(viewWrapper.showGroupedBySort, "view is lying about threading"); + assert_bit_not_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + "View threaded bit should not be set." + ); + assert_bit_not_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kGroupBySort, + "View group-by-sort bit should not be set." + ); + + viewWrapper.showGroupedBySort = true; + assert_bit_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + "View threaded bit should be set." + ); + assert_bit_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kGroupBySort, + "View group-by-sort bit should be set." + ); + viewWrapper.close(); + + // open it again, make sure we're grouped. + viewWrapper.open(folder); + assert_true(viewWrapper.showGroupedBySort, "view should be grouped"); + assert_false(viewWrapper.showThreaded, "view is lying about threading"); + assert_false(viewWrapper.showUnthreaded, "view is lying about threading"); + assert_bit_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + "View threaded bit should be set." + ); + assert_bit_set( + viewWrapper._viewFlags, + Ci.nsMsgViewFlagsType.kGroupBySort, + "View group-by-sort bit should be set." + ); +}); + +/* ===== Real Folder, View Flags ===== */ + +/* + * We cannot test the ignored flag for a local folder because we cannot ignore + * threads in a local folder. Only newsgroups can do that and that's not + * easily testable at this time. + * XXX ^^^ ignoring now works on mail as well. + */ + +/** + * Test the kUnreadOnly flag usage. This functionality is equivalent to the + * mailview kViewItemUnread case, so it uses roughly the same test as + * test_real_folder_mail_views_unread. + */ +add_task(async function test_real_folder_flags_show_unread() { + let viewWrapper = make_view_wrapper(); + + let [[folder], setOne, setTwo] = await messageInjection.makeFoldersWithSets( + 1, + [{}, {}] + ); + + // everything is unread to start with! #1 + viewWrapper.open(folder); + viewWrapper.beginViewUpdate(); + viewWrapper.showUnreadOnly = true; + viewWrapper.endViewUpdate(); + verify_messages_in_view([setOne, setTwo], viewWrapper); + + // add some more things (unread!), make sure they appear. #2 + let [setThree] = await messageInjection.makeNewSetsInFolders([folder], [{}]); + verify_messages_in_view([setOne, setTwo, setThree], viewWrapper); + + // make some things read, make sure they disappear. #3 (after refresh) + setTwo.setRead(true); + viewWrapper.refresh(); // refresh to get the messages to disappear + + verify_messages_in_view([setOne, setThree], viewWrapper); + + // make those things un-read again. #2 + setTwo.setRead(false); + viewWrapper.refresh(); // QUICKSEARCH-VIEW-LIMITATION-REMOVE or not? + verify_messages_in_view([setOne, setTwo, setThree], viewWrapper); +}); + +/* ===== Real Folder, Mail Views ===== */ + +/* + * For these tests, we are testing the filtering logic, not grouping or sorting + * logic. The view tests are responsible for that stuff. We test that: + * + * 1) The view is populated correctly on open. + * 2) The view adds things that become relevant. + * 3) The view removes things that are no longer relevant. Because views like + * to be stable (read: messages don't disappear as you look at them), this + * requires refreshing the view (unless the message has been deleted). + */ + +/** + * Test the kViewItemUnread mail-view case. This functionality is equivalent + * to the kUnreadOnly view flag case, so it uses roughly the same test as + * test_real_folder_flags_show_unread. + */ +add_task(async function test_real_folder_mail_views_unread() { + let viewWrapper = make_view_wrapper(); + + let [[folder], setOne, setTwo] = await messageInjection.makeFoldersWithSets( + 1, + [{}, {}] + ); + + // everything is unread to start with! #1 + viewWrapper.open(folder); + await new Promise(resolve => setTimeout(resolve)); + viewWrapper.setMailView(MailViewConstants.kViewItemUnread, null); + verify_messages_in_view([setOne, setTwo], viewWrapper); + + // add some more things (unread!), make sure they appear. #2 + let [setThree] = await messageInjection.makeNewSetsInFolders([folder], [{}]); + verify_messages_in_view([setOne, setTwo, setThree], viewWrapper); + + // make some things read, make sure they disappear. #3 (after refresh) + setTwo.setRead(true); + viewWrapper.refresh(); // refresh to get the messages to disappear + verify_messages_in_view([setOne, setThree], viewWrapper); + + // make those things un-read again. #2 + setTwo.setRead(false); + viewWrapper.refresh(); // QUICKSEARCH-VIEW-LIMITATION-REMOVE + verify_messages_in_view([setOne, setTwo, setThree], viewWrapper); +}); + +add_task(async function test_real_folder_mail_views_tags() { + let viewWrapper = make_view_wrapper(); + + // setup the initial set with the tag + let [[folder], setOne, setTwo] = await messageInjection.makeFoldersWithSets( + 1, + [{}, {}] + ); + setOne.addTag("$label1"); + + // open, apply mail view constraint, see those messages + viewWrapper.open(folder); + await new Promise(resolve => setTimeout(resolve)); + viewWrapper.setMailView(MailViewConstants.kViewItemTags, "$label1"); + verify_messages_in_view(setOne, viewWrapper); + + // add some more with the tag + setTwo.addTag("$label1"); + + // make sure they showed up + viewWrapper.refresh(); // QUICKSEARCH-VIEW-LIMITATION-REMOVE + verify_messages_in_view([setOne, setTwo], viewWrapper); + + // remove them all + setOne.removeTag("$label1"); + setTwo.removeTag("$label1"); + + // make sure they all disappeared. #3 + viewWrapper.refresh(); + verify_empty_view(viewWrapper); +}); + +/* +add_task(async function test_real_folder_mail_views_not_deleted() { + // not sure how to test this in the absence of an IMAP account with the IMAP + // deletion model... +}); + +add_task(async function test_real_folder_mail_views_custom_people_i_know() { + // blurg. address book. +}); +*/ + +// recent mail = less than 1 day +add_task(async function test_real_folder_mail_views_custom_recent_mail() { + let viewWrapper = make_view_wrapper(); + + // create a set that meets the threshold and a set that does not + let [[folder], setRecent] = await messageInjection.makeFoldersWithSets(1, [ + { age: { mins: 0 } }, + { age: { days: 2 }, age_incr: { mins: 1 } }, + ]); + + // open the folder, ensure only the recent guys show. #1 + viewWrapper.open(folder); + await new Promise(resolve => setTimeout(resolve)); + viewWrapper.setMailView("Recent Mail", null); + verify_messages_in_view(setRecent, viewWrapper); + + // add two more sets, one that meets, and one that doesn't. #2 + let [setMoreRecent] = await messageInjection.makeNewSetsInFolders( + [folder], + [ + { age: { mins: 0 } }, + { age: { days: 2, hours: 1 }, age_incr: { mins: 1 } }, + ] + ); + // make sure that all we see is our previous recent set and our new recent set + verify_messages_in_view([setRecent, setMoreRecent], viewWrapper); + + // we aren't going to mess with the system clock, so no #3. + // (we are assuming that the underlying code handles message deletion. also, + // we are taking the position that message timestamps should not change.) +}); + +add_task(async function test_real_folder_mail_views_custom_last_5_days() { + let viewWrapper = make_view_wrapper(); + + // create a set that meets the threshold and a set that does not + let [[folder], setRecent] = await messageInjection.makeFoldersWithSets(1, [ + { age: { days: 2 }, age_incr: { mins: 1 } }, + { age: { days: 6 }, age_incr: { mins: 1 } }, + ]); + + // open the folder, ensure only the recent guys show. #1 + viewWrapper.open(folder); + await new Promise(resolve => setTimeout(resolve)); + viewWrapper.setMailView("Last 5 Days", null); + verify_messages_in_view(setRecent, viewWrapper); + + // add two more sets, one that meets, and one that doesn't. #2 + let [setMoreRecent] = await messageInjection.makeNewSetsInFolders( + [folder], + [ + { age: { mins: 0 } }, + { age: { days: 5, hours: 1 }, age_incr: { mins: 1 } }, + ] + ); + // make sure that all we see is our previous recent set and our new recent set + verify_messages_in_view([setRecent, setMoreRecent], viewWrapper); + + // we aren't going to mess with the system clock, so no #3. + // (we are assuming that the underlying code handles message deletion. also, + // we are taking the position that message timestamps should not change.) +}); + +add_task(async function test_real_folder_mail_views_custom_not_junk() { + let viewWrapper = make_view_wrapper(); + + let [[folder], setJunk, setNotJunk] = + await messageInjection.makeFoldersWithSets(1, [{}, {}]); + setJunk.setJunk(true); + setNotJunk.setJunk(false); + + // open, see non-junk messages. #1 + viewWrapper.open(folder); + await new Promise(resolve => setTimeout(resolve)); + viewWrapper.setMailView("Not Junk", null); + verify_messages_in_view(setNotJunk, viewWrapper); + + // add some more messages, have them be non-junk for now. #2 + let [setFlippy] = await messageInjection.makeNewSetsInFolders([folder], [{}]); + setFlippy.setJunk(false); + viewWrapper.refresh(); // QUICKSEARCH-VIEW-LIMITATION-REMOVE + verify_messages_in_view([setNotJunk, setFlippy], viewWrapper); + + // oops! they should be junk! #3 + setFlippy.setJunk(true); + viewWrapper.refresh(); + verify_messages_in_view(setNotJunk, viewWrapper); +}); + +add_task(async function test_real_folder_mail_views_custom_has_attachments() { + let viewWrapper = make_view_wrapper(); + + let attachSetDef = { + attachments: [ + { + filename: "foo.png", + contentType: "image/png", + encoding: "base64", + charset: null, + body: "YWJj\n", + format: null, + }, + ], + }; + let noAttachSetDef = {}; + + let [[folder], , setAttach] = await messageInjection.makeFoldersWithSets(1, [ + noAttachSetDef, + attachSetDef, + ]); + viewWrapper.open(folder); + await new Promise(resolve => setTimeout(resolve)); + viewWrapper.setMailView("Has Attachments", null); + verify_messages_in_view(setAttach, viewWrapper); + + let [setMoreAttach] = await messageInjection.makeNewSetsInFolders( + [folder], + [attachSetDef, noAttachSetDef] + ); + verify_messages_in_view([setAttach, setMoreAttach], viewWrapper); +}); + +/* ===== Real Folder, Special Views ===== */ + +add_task(async function test_real_folder_special_views_threads_with_unread() { + let viewWrapper = make_view_wrapper(); + let folder = await messageInjection.makeEmptyFolder(); + + // create two maximally nested threads and add them to the folder. + const count = 10; + let setThreadOne = new SyntheticMessageSet( + gMessageScenarioFactory.directReply(count) + ); + let setThreadTwo = new SyntheticMessageSet( + gMessageScenarioFactory.directReply(count) + ); + await messageInjection.addSetsToFolders( + [folder], + [setThreadOne, setThreadTwo] + ); + + // open the view, set it to this special view + viewWrapper.open(folder); + viewWrapper.beginViewUpdate(); + viewWrapper.specialViewThreadsWithUnread = true; + view_expand_all(viewWrapper); + viewWrapper.endViewUpdate(); + + // no one is read at this point, make sure both threads show up. + verify_messages_in_view([setThreadOne, setThreadTwo], viewWrapper); + + // mark both threads read, make sure they disappear (after a refresh) + setThreadOne.setRead(true); + setThreadTwo.setRead(true); + viewWrapper.refresh(); + verify_empty_view(viewWrapper); + + // make the first thread visible by marking his last message unread + setThreadOne.slice(-1).setRead(false); + + view_expand_all(viewWrapper); + viewWrapper.refresh(); + verify_messages_in_view(setThreadOne, viewWrapper); + + // make the second thread visible by marking some message in the middle + setThreadTwo.slice(5, 6).setRead(false); + view_expand_all(viewWrapper); + viewWrapper.refresh(); + verify_messages_in_view([setThreadOne, setThreadTwo], viewWrapper); +}); + +/** + * Make sure that we restore special views from their persisted state when + * opening the view. + */ +add_task(async function test_real_folder_special_views_persist() { + let viewWrapper = make_view_wrapper(); + let folder = await messageInjection.makeEmptyFolder(); + + viewWrapper.open(folder); + viewWrapper.beginViewUpdate(); + viewWrapper.specialViewThreadsWithUnread = true; + viewWrapper.endViewUpdate(); + viewWrapper.close(); + + viewWrapper.open(folder); + assert_true( + viewWrapper.specialViewThreadsWithUnread, + "We should be in threads-with-unread special view mode." + ); +}); + +add_task(async function test_real_folder_mark_read_on_exit() { + // set a pref so that the local folders account will think we should + // mark messages read when leaving the folder. + Services.prefs.setBoolPref("mailnews.mark_message_read.none", true); + + let viewWrapper = make_view_wrapper(); + let folder = await messageInjection.makeEmptyFolder(); + viewWrapper.open(folder); + + // add some unread messages. + let [setOne] = await messageInjection.makeNewSetsInFolders([folder], [{}]); + setOne.setRead(false); + // verify that we have unread messages. + assert_equals( + folder.getNumUnread(false), + setOne.synMessages.length, + "all messages should have been added as unread" + ); + viewWrapper.close(false); + // verify that closing the view does the expected marking of the messages + // as read. + assert_equals( + folder.getNumUnread(false), + 0, + "messages should have been marked read on view close" + ); + Services.prefs.clearUserPref("mailnews.mark_message_read.none"); +}); diff --git a/comm/mail/base/test/unit/test_viewWrapper_virtualFolder.js b/comm/mail/base/test/unit/test_viewWrapper_virtualFolder.js new file mode 100644 index 0000000000..f33124b9d4 --- /dev/null +++ b/comm/mail/base/test/unit/test_viewWrapper_virtualFolder.js @@ -0,0 +1,552 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 DBViewWrapper against virtual folders. + * + * Things we do not test and our rationalizations: + * - threading stuff. This is not the view wrapper's problem. That is the db + * view's problem! (We test it in the real folder to make sure we are telling + * it to do things correctly.) + * - view flags. Again, it's a db view issue once we're sure we set the bits. + * - special view with threads. same deal. + * + * We could test all these things, but my patch is way behind schedule... + */ + +/* import-globals-from resources/viewWrapperTestUtils.js */ +load("resources/viewWrapperTestUtils.js"); +initViewWrapperTestUtils({ mode: "local" }); + +// -- single-folder backed virtual folder + +/** + * Make sure we open a virtual folder backed by a single underlying folder + * correctly; no constraints. + */ +add_task(async function test_virtual_folder_single_load_no_pred() { + let viewWrapper = make_view_wrapper(); + + let [[folderOne], setOne] = await messageInjection.makeFoldersWithSets(1, [ + {}, + ]); + + let virtFolder = messageInjection.makeVirtualFolder([folderOne], {}); + await view_open(viewWrapper, virtFolder); + + Assert.ok(viewWrapper.isVirtual); + + assert_equals( + gMockViewWrapperListener.allMessagesLoadedEventCount, + 1, + "Should only have received a single all messages loaded notification!" + ); + + verify_messages_in_view(setOne, viewWrapper); + virtFolder.parent.propagateDelete(virtFolder, true); +}); + +/** + * Make sure we open a virtual folder backed by a single underlying folder + * correctly; one constraint. + */ +add_task(async function test_virtual_folder_single_load_simple_pred() { + let viewWrapper = make_view_wrapper(); + + let [[folderOne], oneSubjFoo] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "foo" }, {}] + ); + + let virtFolder = messageInjection.makeVirtualFolder([folderOne], { + subject: "foo", + }); + await view_open(viewWrapper, virtFolder); + + verify_messages_in_view(oneSubjFoo, viewWrapper); + virtFolder.parent.propagateDelete(virtFolder, true); +}); + +/** + * Make sure we open a virtual folder backed by a single underlying folder + * correctly; two constraints ANDed together. + */ +add_task(async function test_virtual_folder_single_load_complex_pred() { + let viewWrapper = make_view_wrapper(); + + let whoBar = make_person_with_word_in_name("bar"); + + let [[folderOne], , , oneBoth] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "foo" }, { from: whoBar }, { subject: "foo", from: whoBar }, {}] + ); + + let virtFolder = messageInjection.makeVirtualFolder( + [folderOne], + { subject: "foo", from: "bar" }, + /* and? */ true + ); + await view_open(viewWrapper, virtFolder); + + verify_messages_in_view(oneBoth, viewWrapper); + virtFolder.parent.propagateDelete(virtFolder, true); +}); + +/** + * Open a single-backed virtual folder, verify, open another single-backed + * virtual folder, verify. We are testing our ability to change folders + * without exploding. + */ +add_task(async function test_virtual_folder_single_load_after_load() { + let viewWrapper = make_view_wrapper(); + + let [[folderOne], oneSubjFoo] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "foo" }, {}] + ); + let virtOne = messageInjection.makeVirtualFolder([folderOne], { + subject: "foo", + }); + await view_open(viewWrapper, virtOne); + verify_messages_in_view([oneSubjFoo], viewWrapper); + + // use "bar" instead of "foo" to make sure constraints are properly changing + let [[folderTwo], twoSubjBar] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "bar" }, {}] + ); + let virtTwo = messageInjection.makeVirtualFolder([folderTwo], { + subject: "bar", + }); + await view_open(viewWrapper, virtTwo); + verify_messages_in_view([twoSubjBar], viewWrapper); + virtOne.parent.propagateDelete(virtOne, true); + virtTwo.parent.propagateDelete(virtTwo, true); +}); + +// -- multi-folder backed virtual folder + +/** + * Make sure we open a virtual folder backed by multiple underlying folders + * correctly; no constraints. + */ +add_task(async function test_virtual_folder_multi_load_no_pred() { + let viewWrapper = make_view_wrapper(); + + let [[folderOne], setOne] = await messageInjection.makeFoldersWithSets(1, [ + {}, + ]); + let [[folderTwo], setTwo] = await messageInjection.makeFoldersWithSets(1, [ + {}, + ]); + + let virtFolder = messageInjection.makeVirtualFolder( + [folderOne, folderTwo], + {} + ); + await view_open(viewWrapper, virtFolder); + + verify_messages_in_view([setOne, setTwo], viewWrapper); + virtFolder.parent.propagateDelete(virtFolder, true); +}); + +/** + * Make sure the sort order of a virtual folder backed by multiple underlying + * folders is persistent. + */ +add_task(async function test_virtual_folder_multi_sortorder_persistence() { + let viewWrapper = make_view_wrapper(); + + let [[folderOne], setOne] = await messageInjection.makeFoldersWithSets(1, [ + {}, + ]); + let [[folderTwo], setTwo] = await messageInjection.makeFoldersWithSets(1, [ + {}, + ]); + + let virtFolder = messageInjection.makeVirtualFolder( + [folderOne, folderTwo], + {} + ); + await view_open(viewWrapper, virtFolder); + + verify_messages_in_view([setOne, setTwo], viewWrapper); + viewWrapper.showThreaded = true; + viewWrapper.sort( + Ci.nsMsgViewSortType.bySubject, + Ci.nsMsgViewSortOrder.ascending + ); + + viewWrapper.close(); + await view_open(viewWrapper, virtFolder); + assert_equals( + viewWrapper.primarySortType, + Ci.nsMsgViewSortType.bySubject, + "should have remembered sort type." + ); + assert_equals( + viewWrapper.primarySortOrder, + Ci.nsMsgViewSortOrder.ascending, + "should have remembered sort order." + ); + virtFolder.parent.propagateDelete(virtFolder, true); +}); + +/** + * Make sure we open a virtual folder backed by multiple underlying folders + * correctly; one constraint. + */ +add_task(async function test_virtual_folder_multi_load_simple_pred() { + let viewWrapper = make_view_wrapper(); + + let [[folderOne], oneSubjFoo] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "foo" }, {}] + ); + let [[folderTwo], twoSubjFoo] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "foo" }, {}] + ); + + let virtFolder = messageInjection.makeVirtualFolder([folderOne, folderTwo], { + subject: "foo", + }); + await view_open(viewWrapper, virtFolder); + + verify_messages_in_view([oneSubjFoo, twoSubjFoo], viewWrapper); + virtFolder.parent.propagateDelete(virtFolder, true); +}); + +/** + * Make sure we open a virtual folder backed by multiple underlying folders + * correctly; two constraints ANDed together. + */ +add_task(async function test_virtual_folder_multi_load_complex_pred() { + let viewWrapper = make_view_wrapper(); + + let whoBar = make_person_with_word_in_name("bar"); + + let [[folderOne], , , oneBoth] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "foo" }, { from: whoBar }, { subject: "foo", from: whoBar }, {}] + ); + let [[folderTwo], , , twoBoth] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "foo" }, { from: whoBar }, { subject: "foo", from: whoBar }, {}] + ); + + let virtFolder = messageInjection.makeVirtualFolder( + [folderOne, folderTwo], + { subject: "foo", from: "bar" }, + /* and? */ true + ); + await view_open(viewWrapper, virtFolder); + + verify_messages_in_view([oneBoth, twoBoth], viewWrapper); + virtFolder.parent.propagateDelete(virtFolder, true); +}); + +add_task( + async function test_virtual_folder_multi_load_alotta_folders_no_pred() { + let viewWrapper = make_view_wrapper(); + + const folderCount = 4; + const messageCount = 64; + + let [folders, setOne] = await messageInjection.makeFoldersWithSets( + folderCount, + [{ count: messageCount }] + ); + + let virtFolder = messageInjection.makeVirtualFolder(folders, {}); + await view_open(viewWrapper, virtFolder); + + verify_messages_in_view([setOne], viewWrapper); + virtFolder.parent.propagateDelete(virtFolder, true); + } +); + +add_task( + async function test_virtual_folder_multi_load_alotta_folders_simple_pred() { + let viewWrapper = make_view_wrapper(); + + const folderCount = 16; + const messageCount = 256; + + let [folders, setOne] = await messageInjection.makeFoldersWithSets( + folderCount, + [{ subject: "foo", count: messageCount }] + ); + + let virtFolder = messageInjection.makeVirtualFolder(folders, { + subject: "foo", + }); + await view_open(viewWrapper, virtFolder); + + verify_messages_in_view([setOne], viewWrapper); + virtFolder.parent.propagateDelete(virtFolder, true); + } +); + +/** + * Make sure that opening a virtual folder backed by multiple real folders, then + * opening another virtual folder of the same variety works without explosions. + */ +add_task(async function test_virtual_folder_multi_load_after_load() { + let viewWrapper = make_view_wrapper(); + + let [foldersOne, oneSubjFoo] = await messageInjection.makeFoldersWithSets(2, [ + { subject: "foo" }, + {}, + ]); + let virtOne = messageInjection.makeVirtualFolder(foldersOne, { + subject: "foo", + }); + await view_open(viewWrapper, virtOne); + verify_messages_in_view([oneSubjFoo], viewWrapper); + + // use "bar" instead of "foo" to make sure constraints are properly changing + let [foldersTwo, twoSubjBar] = await messageInjection.makeFoldersWithSets(3, [ + { subject: "bar" }, + {}, + ]); + let virtTwo = messageInjection.makeVirtualFolder(foldersTwo, { + subject: "bar", + }); + await view_open(viewWrapper, virtTwo); + verify_messages_in_view([twoSubjBar], viewWrapper); + + await view_open(viewWrapper, virtOne); + verify_messages_in_view([oneSubjFoo], viewWrapper); + virtOne.parent.propagateDelete(virtOne, true); + virtTwo.parent.propagateDelete(virtTwo, true); +}); + +// -- mixture of single-backed and multi-backed + +/** + * Make sure that opening a virtual folder backed by a single real folder, then + * a multi-backed one, then the single-backed one again doesn't explode. + * + * This is just test_virtual_folder_multi_load_after_load with foldersOne told + * to create just a single folder. + */ +add_task(async function test_virtual_folder_combo_load_after_load() { + let viewWrapper = make_view_wrapper(); + + let [foldersOne, oneSubjFoo] = await messageInjection.makeFoldersWithSets(1, [ + { subject: "foo" }, + {}, + ]); + let virtOne = messageInjection.makeVirtualFolder(foldersOne, { + subject: "foo", + }); + await view_open(viewWrapper, virtOne); + verify_messages_in_view([oneSubjFoo], viewWrapper); + + // use "bar" instead of "foo" to make sure constraints are properly changing + let [foldersTwo, twoSubjBar] = await messageInjection.makeFoldersWithSets(3, [ + { subject: "bar" }, + {}, + ]); + let virtTwo = messageInjection.makeVirtualFolder(foldersTwo, { + subject: "bar", + }); + await view_open(viewWrapper, virtTwo); + verify_messages_in_view([twoSubjBar], viewWrapper); + + await view_open(viewWrapper, virtOne); + verify_messages_in_view([oneSubjFoo], viewWrapper); + virtOne.parent.propagateDelete(virtOne, true); + virtTwo.parent.propagateDelete(virtTwo, true); +}); + +// -- ignore things we should ignore + +/** + * Make sure that if a server is listed in a virtual folder's search Uris that + * it does not get into our list of _underlyingFolders. + */ +add_task(async function test_virtual_folder_filters_out_servers() { + let viewWrapper = make_view_wrapper(); + + let [folders] = await messageInjection.makeFoldersWithSets(2, []); + folders.push(folders[0].rootFolder); + let virtFolder = messageInjection.makeVirtualFolder(folders, {}); + await view_open(viewWrapper, virtFolder); + + assert_equals( + viewWrapper._underlyingFolders.length, + 2, + "Server folder should have been filtered out." + ); + virtFolder.parent.propagateDelete(virtFolder, true); +}); + +// -- rare/edge cases! + +/** + * Verify that if one of the folders backing our virtual folder is deleted that + * we do not explode. Then verify that if we remove the rest of them that the + * view wrapper closes itself. + */ +add_task(async function test_virtual_folder_underlying_folder_deleted() { + let viewWrapper = make_view_wrapper(); + + let [[folderOne]] = await messageInjection.makeFoldersWithSets(1, [ + { subject: "foo" }, + {}, + ]); + let [[folderTwo], twoSubjFoo] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "foo" }, {}] + ); + + let virtFolder = messageInjection.makeVirtualFolder([folderOne, folderTwo], { + subject: "foo", + }); + await view_open(viewWrapper, virtFolder); + + // this triggers the search (under the view's hood), so it's async + await delete_folder(folderOne, viewWrapper); + + // only messages from the surviving folder should be present + verify_messages_in_view([twoSubjFoo], viewWrapper); + + // this one is not async though, because we are expecting to close the wrapper + // and ignore the view entirely, no resolving action. + delete_folder(folderTwo); + + // now the view wrapper should have closed itself. + Assert.equal(null, viewWrapper.displayedFolder); + // This fails because virtFolder.parent is null, not sure why + // virtFolder.parent.propagateDelete(virtFolder, true); +}); + +/* ===== Virtual Folder, Mail Views ===== */ + +/* + * We do not need to test all of the mail view permutations, realFolder + * already did that. We just need to make sure it works at all. + */ + +add_task( + async function test_virtual_folder_mail_views_unread_with_one_folder() { + let viewWrapper = make_view_wrapper(); + + let [folders, fooOne, fooTwo] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "foo 1" }, { subject: "foo 2" }, {}, {}] + ); + let virtFolder = messageInjection.makeVirtualFolder(folders, { + subject: "foo", + }); + + // everything is unread to start with! + await view_open(viewWrapper, virtFolder); + await view_set_mail_view(viewWrapper, MailViewConstants.kViewItemUnread); + verify_messages_in_view([fooOne, fooTwo], viewWrapper); + + // add some more things (unread!), make sure they appear. + let [fooThree] = await messageInjection.makeNewSetsInFolders(folders, [ + { subject: "foo 3" }, + {}, + ]); + verify_messages_in_view([fooOne, fooTwo, fooThree], viewWrapper); + + // make some things read, make sure they disappear. (after a refresh) + fooTwo.setRead(true); + await view_refresh(viewWrapper); + verify_messages_in_view([fooOne, fooThree], viewWrapper); + + // make those things un-read again. + fooTwo.setRead(false); + // I thought this was a quick search limitation, but XFVF needs it to, at + // least for the unread case. + await view_refresh(viewWrapper); + verify_messages_in_view([fooOne, fooTwo, fooThree], viewWrapper); + virtFolder.parent.propagateDelete(virtFolder, true); + } +); + +// -- mail views + +add_task( + async function test_virtual_folder_mail_views_unread_with_four_folders() { + let viewWrapper = make_view_wrapper(); + + let [folders, fooOne, fooTwo] = await messageInjection.makeFoldersWithSets( + 4, + [{ subject: "foo 1" }, { subject: "foo 2" }, {}, {}] + ); + let virtFolder = messageInjection.makeVirtualFolder(folders, { + subject: "foo", + }); + + // everything is unread to start with! + await view_open(viewWrapper, virtFolder); + await view_set_mail_view(viewWrapper, MailViewConstants.kViewItemUnread); + verify_messages_in_view([fooOne, fooTwo], viewWrapper); + + // add some more things (unread!), make sure they appear. + let [fooThree] = await messageInjection.makeNewSetsInFolders(folders, [ + { subject: "foo 3" }, + {}, + ]); + verify_messages_in_view([fooOne, fooTwo, fooThree], viewWrapper); + + // make some things read, make sure they disappear. (after a refresh) + fooTwo.setRead(true); + await view_refresh(viewWrapper); + verify_messages_in_view([fooOne, fooThree], viewWrapper); + + // make those things un-read again. + fooTwo.setRead(false); + // I thought this was a quick search limitation, but XFVF needs it to, at + // least for the unread case. + await view_refresh(viewWrapper); + verify_messages_in_view([fooOne, fooTwo, fooThree], viewWrapper); + virtFolder.parent.propagateDelete(virtFolder, true); + } +); + +// This tests that clearing the new messages in a folder also clears the +// new flag on saved search folders based on the real folder. This could be a +// core view test, or a mozmill test, but I think the view wrapper stuff +// is involved in some of the issues here, so this is a compromise. +add_task(async function test_virtual_folder_mail_new_handling() { + let viewWrapper = make_view_wrapper(); + + let [folders] = await messageInjection.makeFoldersWithSets(1, [ + { subject: "foo 1" }, + { subject: "foo 2" }, + ]); + let folder = folders[0]; + let virtFolder = messageInjection.makeVirtualFolder(folders, { + subject: "foo", + }); + + await view_open(viewWrapper, folder); + + await messageInjection.makeNewSetsInFolders(folders, [ + { subject: "foo 3" }, + {}, + ]); + + if (!virtFolder.hasNewMessages) { + do_throw("saved search should have new messages!"); + } + + if (!folder.hasNewMessages) { + do_throw("folder should have new messages!"); + } + + viewWrapper.close(); + folder.msgDatabase = null; + folder.clearNewMessages(); + if (virtFolder.hasNewMessages) { + do_throw("saved search should not have new messages!"); + } + virtFolder.parent.propagateDelete(virtFolder, true); +}); diff --git a/comm/mail/base/test/unit/test_viewWrapper_virtualFolderCustomTerm.js b/comm/mail/base/test/unit/test_viewWrapper_virtualFolderCustomTerm.js new file mode 100644 index 0000000000..29890db68b --- /dev/null +++ b/comm/mail/base/test/unit/test_viewWrapper_virtualFolderCustomTerm.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 DBViewWrapper against a virtual folder with a custom search term. + * + * This test uses an imap message to specifically test the issues from + * bug 549336. The code is derived from test_viewWrapper_virtualFolder.js + * + * Original author: Kent James + */ + +/* import-globals-from resources/viewWrapperTestUtils.js */ +load("resources/viewWrapperTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +initViewWrapperTestUtils({ mode: "imap", offline: false }); + +/** + * A custom search term, that just does Subject Contains + */ +var gCustomSearchTermSubject = { + id: "mailnews@mozilla.org#test", + name: "Test-mailbase Subject", + getEnabled(scope, op) { + return true; + }, + getAvailable(scope, op) { + return true; + }, + getAvailableOperators(scope) { + return [Ci.nsMsgSearchOp.Contains]; + }, + match(aMsgHdr, aSearchValue, aSearchOp) { + return aMsgHdr.subject.includes(aSearchValue); + }, + needsBody: false, +}; + +MailServices.filters.addCustomTerm(gCustomSearchTermSubject); + +/** + * Make sure we open a virtual folder backed by a single underlying folder + * correctly, with a custom search term. + */ +add_task(async function test_virtual_folder_single_load_custom_pred() { + let viewWrapper = make_view_wrapper(); + + let [[folderOne], oneSubjFoo] = await messageInjection.makeFoldersWithSets( + 1, + [{ subject: "foo" }, {}] + ); + + let virtFolder = messageInjection.makeVirtualFolder(folderOne, { + custom: "foo", + }); + + await view_open(viewWrapper, virtFolder); + + verify_messages_in_view(oneSubjFoo, viewWrapper); +}); diff --git a/comm/mail/base/test/unit/xpcshell.ini b/comm/mail/base/test/unit/xpcshell.ini new file mode 100644 index 0000000000..ad48cee56f --- /dev/null +++ b/comm/mail/base/test/unit/xpcshell.ini @@ -0,0 +1,25 @@ +[DEFAULT] +head = head_mailbase.js +dupe-manifest = +support-files = distribution.ini resources/* + +[test_alertHook.js] +[test_attachmentChecker.js] +[test_devtools_url.js] +[test_emptyTrash_dbViewWrapper.js] +run-sequentially = Avoid bustage. +[test_viewWrapper_imapFolder.js] +run-sequentially = Avoid bustage. +[test_viewWrapper_logic.js] +[test_viewWrapper_realFolder.js] +skip-if = os == "mac" && !debug +reason = osx shippable perma failures +[test_viewWrapper_virtualFolder.js] +[test_viewWrapper_virtualFolderCustomTerm.js] +run-sequentially = Avoid bustage. +[test_oauth_migration.js] +[test_mailGlue_distribution.js] +skip-if = os == 'win' && msix # MSIX has a distribution.ini and it's unwritable. Tests fail. +[test_treeSelection.js] + +[include:xpcshell_maildir.ini] diff --git a/comm/mail/base/test/unit/xpcshell_maildir.ini b/comm/mail/base/test/unit/xpcshell_maildir.ini new file mode 100644 index 0000000000..511295fcbe --- /dev/null +++ b/comm/mail/base/test/unit/xpcshell_maildir.ini @@ -0,0 +1,6 @@ +[DEFAULT] +head = head_mailbase_maildir.js +dupe-manifest = +run-sequentially = + +[test_viewWrapper_virtualFolder.js] |