diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mail/base/test/unit/resources/viewWrapperTestUtils.js | 534 |
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; + }); + } +} |