/* 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; }); } }