summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/test/unit/resources/viewWrapperTestUtils.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/base/test/unit/resources/viewWrapperTestUtils.js')
-rw-r--r--comm/mail/base/test/unit/resources/viewWrapperTestUtils.js534
1 files changed, 534 insertions, 0 deletions
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;
+ });
+ }
+}