summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/base/test/unit')
-rw-r--r--comm/mail/base/test/unit/distribution.ini56
-rw-r--r--comm/mail/base/test/unit/head_mailbase.js20
-rw-r--r--comm/mail/base/test/unit/head_mailbase_maildir.js9
-rw-r--r--comm/mail/base/test/unit/resources/viewWrapperTestUtils.js534
-rw-r--r--comm/mail/base/test/unit/test_alertHook.js119
-rw-r--r--comm/mail/base/test/unit/test_attachmentChecker.js121
-rw-r--r--comm/mail/base/test/unit/test_devtools_url.js22
-rw-r--r--comm/mail/base/test/unit/test_emptyTrash_dbViewWrapper.js43
-rw-r--r--comm/mail/base/test/unit/test_mailGlue_distribution.js120
-rw-r--r--comm/mail/base/test/unit/test_oauth_migration.js319
-rw-r--r--comm/mail/base/test/unit/test_treeSelection.js581
-rw-r--r--comm/mail/base/test/unit/test_viewWrapper_imapFolder.js55
-rw-r--r--comm/mail/base/test/unit/test_viewWrapper_logic.js359
-rw-r--r--comm/mail/base/test/unit/test_viewWrapper_realFolder.js666
-rw-r--r--comm/mail/base/test/unit/test_viewWrapper_virtualFolder.js552
-rw-r--r--comm/mail/base/test/unit/test_viewWrapper_virtualFolderCustomTerm.js65
-rw-r--r--comm/mail/base/test/unit/xpcshell.ini25
-rw-r--r--comm/mail/base/test/unit/xpcshell_maildir.ini6
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]