diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mailnews/base/test/unit/test_nsMsgDBView.js | 1212 |
1 files changed, 1212 insertions, 0 deletions
diff --git a/comm/mailnews/base/test/unit/test_nsMsgDBView.js b/comm/mailnews/base/test/unit/test_nsMsgDBView.js new file mode 100644 index 0000000000..cb5527deab --- /dev/null +++ b/comm/mailnews/base/test/unit/test_nsMsgDBView.js @@ -0,0 +1,1212 @@ +/* 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/. */ + +/* + * Attempt to test nsMsgDBView and descendents. Right now this means we: + * - Ensure sorting and grouping sorta works, including using custom columns. + * + * Things we really should do: + * - Test that secondary sorting works, especially when the primary column is + * a custom column. + * + * You may also want to look into the test_viewWrapper_*.js tests as well. + */ + +var { MessageGenerator, MessageScenarioFactory, SyntheticMessageSet } = + ChromeUtils.import("resource://testing-common/mailnews/MessageGenerator.jsm"); +const { TreeSelection } = ChromeUtils.importESModule( + "chrome://messenger/content/tree-selection.mjs" +); +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +var { dump_view_contents } = ChromeUtils.import( + "resource://testing-common/mozmill/ViewHelpers.jsm" +); + +// Items used to add messages to the folder +var gMessageGenerator = new MessageGenerator(); +var gScenarioFactory = new MessageScenarioFactory(gMessageGenerator); +var messageInjection = new MessageInjection({ mode: "local" }); + +var gTestFolder; +var gSiblingsMissingParentsSubject; +var gMessages; + +function setup_messages() { + // build up a diverse list of messages + let messages = []; + messages = messages.concat(gScenarioFactory.directReply(10)); + // the message generator uses a constanty incrementing counter, so we need to + // mix up the order of messages ourselves to ensure that the timestamp + // ordering is not already in order. (a poor test of sorting otherwise.) + messages = gScenarioFactory.directReply(6).concat(messages); + + messages = messages.concat(gScenarioFactory.fullPyramid(3, 3)); + let siblingMessages = gScenarioFactory.siblingsMissingParent(); + // cut off "Re: " part + gSiblingsMissingParentsSubject = siblingMessages[0].subject.slice(4); + dump("siblings subect = " + gSiblingsMissingParentsSubject + "\n"); + messages = messages.concat(siblingMessages); + messages = messages.concat(gScenarioFactory.missingIntermediary()); + // This next line was found to be faulty during linting, but fixing it breaks the test. + // messages.concat(gMessageGenerator.makeMessage({age: {days: 2, hours: 1}})); + + // build a hierarchy like this (the UID order corresponds to the date order) + // 1 + // 2 + // 4 + // 3 + let msg1 = gMessageGenerator.makeMessage(); + let msg2 = gMessageGenerator.makeMessage({ inReplyTo: msg1 }); + let msg3 = gMessageGenerator.makeMessage({ inReplyTo: msg1 }); + let msg4 = gMessageGenerator.makeMessage({ inReplyTo: msg2 }); + messages = messages.concat([msg1, msg2, msg3, msg4]); + + // test bug 600140, make a thread that Reply message has smaller MsgKey + let msgBiggerKey = gMessageGenerator.makeMessage(); + let msgSmallerKey = gMessageGenerator.makeMessage({ + inReplyTo: msgBiggerKey, + }); + messages = messages.concat([msgSmallerKey, msgBiggerKey]); + let msgSet = new SyntheticMessageSet(messages); + return msgSet; +} + +/** + * Sets gTestFolder with msgSet. Ensure that gTestFolder is clean for each test. + * + * @param {SyntheticMessageSet} msgSet + */ +async function set_gTestFolder(msgSet) { + gTestFolder = await messageInjection.makeEmptyFolder(); + await messageInjection.addSetsToFolders([gTestFolder], [msgSet]); +} + +/** + * Create a synthetic message by passing the provided aMessageArgs to + * the message generator, then add the resulting message to the given + * folder (or gTestFolder if no folder is provided). + */ +async function make_and_add_message(aMessageArgs) { + // create the message + let synMsg = gMessageGenerator.makeMessage(aMessageArgs); + let msgSet = new SyntheticMessageSet([synMsg]); + // this is synchronous for local stuff. + await messageInjection.addSetsToFolders([gTestFolder], [msgSet]); + + return [synMsg, msgSet]; +} + +function view_throw(why) { + dump_view_contents(); + do_throw(why); +} + +/** + * Throw if gDBView has any rows. + */ +function assert_view_empty() { + if (gTreeView.rowCount != 0) { + view_throw( + "Expected view to be empty, but it was not! (" + + gTreeView.rowCount + + " rows)" + ); + } +} + +/** + * Throw if gDBView does not have aCount rows. + */ +function assert_view_row_count(aCount) { + if (gTreeView.rowCount != aCount) { + view_throw( + "Expected view to have " + + aCount + + " rows, but it had " + + gTreeView.rowCount + + " rows!" + ); + } +} + +/** + * Throw if any of the arguments (as view indices) do not correspond to dummy + * rows in gDBView. + */ +function assert_view_index_is_dummy(...aArgs) { + for (let viewIndex of aArgs) { + let flags = gDBView.getFlagsAt(viewIndex); + if (!(flags & MSG_VIEW_FLAG_DUMMY)) { + view_throw("Expected index " + viewIndex + " to be a dummy!"); + } + } +} + +/** + * Throw if any of the arguments (as view indices) correspond to dummy rows in + * gDBView. + */ +function assert_view_index_is_not_dummy(...aArgs) { + for (let viewIndex of aArgs) { + let flags = gDBView.getFlagsAt(viewIndex); + if (flags & MSG_VIEW_FLAG_DUMMY) { + view_throw("Expected index " + viewIndex + " to not be a dummy!"); + } + } +} + +function assert_view_level_is(index, level) { + if (gDBView.getLevel(index) != level) { + view_throw( + "Expected index " + + index + + " to be level " + + level + + " not " + + gDBView.getLevel(index) + ); + } +} + +/** + * Given a message, assert that it is present at the given indices. + * + * Usage: + * assert_view_message_at_indices(synMsg, 0); + * assert_view_message_at_indices(synMsg, 0, 1); + * assert_view_message_at_indices(aMsg, 0, bMsg, 1); + */ +function assert_view_message_at_indices(...aArgs) { + let curHdr; + for (let thing of aArgs) { + if (typeof thing == "number") { + let hdrAt = gDBView.getMsgHdrAt(thing); + if (curHdr != hdrAt) { + view_throw( + "Expected hdr at " + + thing + + " to be " + + curHdr.messageKey + + ":" + + curHdr.mime2DecodedSubject.substr(0, 30) + + " not " + + hdrAt.messageKey + + ":" + + hdrAt.mime2DecodedSubject.substr(0, 30) + ); + } + } else { + // synthetic message, get the header... + curHdr = gTestFolder.msgDatabase.getMsgHdrForMessageID(thing.messageId); + } + } +} + +var authorFirstLetterCustomColumn = { + getCellText(row, col) { + let msgHdr = this.dbView.getMsgHdrAt(row); + return msgHdr.mime2DecodedAuthor.charAt(0).toUpperCase() || "?"; + }, + getSortStringForRow(msgHdr) { + // charAt(0) is a quote, charAt(1) is the first letter! + return msgHdr.mime2DecodedAuthor.charAt(1).toUpperCase() || "?"; + }, + isString() { + return true; + }, + + getCellProperties(row, col) { + return ""; + }, + getRowProperties(row) { + return ""; + }, + getImageSrc(row, col) { + return null; + }, + getSortLongForRow(hdr) { + return 0; + }, +}; + +var gDBView; +var gTreeView; + +var MSG_VIEW_FLAG_DUMMY = 0x20000000; + +var gFakeSelection = new TreeSelection(null); + +function setup_view(aViewType, aViewFlags, aTestFolder) { + let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=" + aViewType; + + if (aTestFolder == null) { + aTestFolder = gTestFolder; + } + + // always start out fully expanded + aViewFlags |= Ci.nsMsgViewFlagsType.kExpandAll; + + gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView); + gDBView.init(null, null, null); + var outCount = {}; + gDBView.open( + aViewType != "search" ? aTestFolder : null, + Ci.nsMsgViewSortType.byDate, + aViewType != "search" + ? Ci.nsMsgViewSortOrder.ascending + : Ci.nsMsgViewSortOrder.descending, + aViewFlags, + outCount + ); + // outCount is 0 if byCustom; view is built by addColumnHandler() + dump(" View Out Count: " + outCount.value + "\n"); + + // we need to cram messages into the search via nsIMsgSearchNotify interface + if ( + aViewType == "search" || + aViewType == "quicksearch" || + aViewType == "xfvf" + ) { + let searchNotify = gDBView.QueryInterface(Ci.nsIMsgSearchNotify); + searchNotify.onNewSearch(); + for (let msgHdr of aTestFolder.msgDatabase.enumerateMessages()) { + searchNotify.onSearchHit(msgHdr, msgHdr.folder); + } + searchNotify.onSearchDone(Cr.NS_OK); + } + + gDBView.addColumnHandler( + "authorFirstLetterCol", + authorFirstLetterCustomColumn + ); + // XXX this sets the custom column to use for sorting by the custom column. + // It has been argued (and is generally accepted) that this should not be + // so limited. + gDBView.curCustomColumn = "authorFirstLetterCol"; + + gTreeView = gDBView.QueryInterface(Ci.nsITreeView); + gTreeView.selection = gFakeSelection; + gFakeSelection.view = gTreeView; +} + +function setup_group_view(aSortType, aSortOrder, aTestFolder) { + let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=group"; + + if (aTestFolder == null) { + aTestFolder = gTestFolder; + } + + // grouped view uses these flags + let viewFlags = + Ci.nsMsgViewFlagsType.kGroupBySort | + Ci.nsMsgViewFlagsType.kExpandAll | + Ci.nsMsgViewFlagsType.kThreadedDisplay; + + gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView); + gDBView.init(null, null, null); + var outCount = {}; + gDBView.open(aTestFolder, aSortType, aSortOrder, viewFlags, outCount); + + gDBView.addColumnHandler( + "authorFirstLetterCol", + authorFirstLetterCustomColumn + ); + gDBView.curCustomColumn = "authorFirstLetterCol"; + + gTreeView = gDBView.QueryInterface(Ci.nsITreeView); + gFakeSelection.view = gTreeView; + gTreeView.selection = gFakeSelection; +} + +/** + * Comparison func for built-in types (including strings, so no subtraction.) + */ +function generalCmp(a, b) { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } + return 0; +} + +/** + * Check that sort order and grouping logic (if applicable) are doing the right + * thing. + * + * In the case of groups (indicated by dummy headers), we want to ignore the + * dummies and 1) make sure all the values in the group have the same value, + * 2) verify that the headers meet our total ordering. + * In the case of threads, we want to ensure that each level of the hierarchy + * meets our ordering demands, recursing into children. Because the tree + * representation is rather quite horrible, the easiest thing for us is to + * track a per-level list of comparison values we have seen, nuking older + * values when changes in levels indicate closure of a level. (Namely, + * if we see a node at level N, then all levels >N are no longer valid.) + * + * @param {nsMsgViewType} aSortBy - The sort type. + * @param {nsMsgViewSortOrder} aDirection - The sort direction. + * @param {string|Function} aKeyOrValueGetter - A string naming the attribute on + * the message headerto retrieve, or if that is not sufficient a function that + * takes a message header and returns the sort value for it. + * @param {Function} [aGetGroupValue] - An optional function that takes a + * message header and returns the grouping value for the header. + * If omitted, it is assumed that the sort value is the grouping value. + */ +function ensure_view_ordering( + aSortBy, + aDirection, + aKeyOrValueGetter, + aGetGroupValue +) { + if (!gTreeView.rowCount) { + do_throw("There are no rows in my folder! I can't test anything!"); + } + dump( + " Ensuring sort order for " + + aSortBy + + " (Row count: " + + gTreeView.rowCount + + ")\n" + ); + dump(" cur view flags: " + gDBView.viewFlags + "\n"); + + // standard grouping doesn't re-group when you sort. so we need to actually + // re-initialize the view. + // but search mode is special and does the right thing because asuth didn't + // realize that it shouldn't do the right thing, so it can just change the + // sort. (of course, under the hood, it is actually creating a new view...) + if ( + gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort && + gDBView.viewType != Ci.nsMsgViewType.eShowSearch + ) { + // we must close to re-open (or we could just use a new view) + let msgFolder = gDBView.msgFolder; + gDBView.close(); + gDBView.open(msgFolder, aSortBy, aDirection, gDBView.viewFlags, {}); + } else { + gDBView.sort(aSortBy, aDirection); + } + + let comparisonValuesByLevel = []; + let expectedLevel0CmpResult = + aDirection == Ci.nsMsgViewSortOrder.ascending ? 1 : -1; + let comparator = generalCmp; + + let dummyCount = 0, + emptyDummyCount = 0; + + let valueGetter = + typeof aKeyOrValueGetter == "string" + ? function (msgHdr) { + return msgHdr[aKeyOrValueGetter]; + } + : aKeyOrValueGetter; + let groupValueGetter = aGetGroupValue || valueGetter; + + // don't do group testing until we see a dummy header (which we will see + // before we see any grouped headers, so it's fine to do this) + let inGroup = false; + // the current grouping value for the current group. this allows us to + // detect erroneous grouping of different group values together. + let curGroupValue = null; + // the set of group values observed before the current group. this allows + // us to detect improper grouping where there are multiple groups with the + // same grouping value. + let previouslySeenGroupValues = {}; + + for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) { + let msgHdr = gDBView.getMsgHdrAt(iViewIndex); + let msgViewFlags = gDBView.getFlagsAt(iViewIndex); + + // ignore dummy headers; testing grouping logic happens elsewhere + if (msgViewFlags & MSG_VIEW_FLAG_DUMMY) { + if (dummyCount && curGroupValue == null) { + emptyDummyCount++; + } + dummyCount++; + if (curGroupValue != null) { + previouslySeenGroupValues[curGroupValue] = true; + } + curGroupValue = null; + inGroup = true; + continue; + } + + // level is 0-based + let level = gTreeView.getLevel(iViewIndex); + // nuke existing comparison levels + if (level < comparisonValuesByLevel.length - 1) { + comparisonValuesByLevel.splice(level); + } + + // get the value for comparison + let curValue = valueGetter(msgHdr); + if (inGroup) { + let groupValue = groupValueGetter(msgHdr); + if (groupValue in previouslySeenGroupValues) { + do_throw(`Group value ${groupValue} observed in more than one group!`); + } + if (curGroupValue == null) { + curGroupValue = groupValue; + } else if (curGroupValue != groupValue) { + do_throw( + "Inconsistent grouping! " + groupValue + " != " + curGroupValue + ); + } + } + + // is this level new to our comparisons? then track it... + if (level >= comparisonValuesByLevel.length) { + // null-fill any gaps (due to, say, dummy nodes) + while (comparisonValuesByLevel.length <= level) { + comparisonValuesByLevel.push(null); + } + comparisonValuesByLevel.push(curValue); + } else { + // otherwise compare it + let prevValue = comparisonValuesByLevel[level - 1]; + let cmpResult = comparator(curValue, prevValue); + let expectedCmpResult = level > 0 ? 1 : expectedLevel0CmpResult; + if (cmpResult && cmpResult != expectedCmpResult) { + do_throw( + "Ordering failure on key " + + msgHdr.messageKey + + ". " + + curValue + + " should have been " + + (expectedCmpResult == 1 ? ">=" : "<=") + + " " + + prevValue + + " but was not." + ); + } + } + } + + if (inGroup && curGroupValue == null) { + emptyDummyCount++; + } + if (dummyCount) { + dump( + " saw " + + dummyCount + + " dummy headers (" + + emptyDummyCount + + " empty).\n" + ); + } +} + +/** + * Test sorting functionality. + */ +function test_sort_columns() { + ensure_view_ordering( + Ci.nsMsgViewSortType.byDate, + Ci.nsMsgViewSortOrder.descending, + "date", + function getDateAgeBucket(msgHdr) { + // so, this is a cop-out, but we know that the date age bucket for our + // generated messages is always more than 2-weeks ago! + return 5; + } + ); + ensure_view_ordering( + Ci.nsMsgViewSortType.byDate, + Ci.nsMsgViewSortOrder.ascending, + "date", + function getDateAgeBucket(msgHdr) { + // so, this is a cop-out, but we know that the date age bucket for our + // generated messages is always more than 2-weeks ago! + return 5; + } + ); + // (note, subject doesn't use dummy groups and so won't have grouping tested) + ensure_view_ordering( + Ci.nsMsgViewSortType.bySubject, + Ci.nsMsgViewSortOrder.ascending, + "mime2DecodedSubject" + ); + ensure_view_ordering( + Ci.nsMsgViewSortType.byAuthor, + Ci.nsMsgViewSortOrder.ascending, + "mime2DecodedAuthor" + ); + // Id + // Thread + // Priority + // Status + // Size + // Flagged + // Unread + ensure_view_ordering( + Ci.nsMsgViewSortType.byRecipient, + Ci.nsMsgViewSortOrder.ascending, + "mime2DecodedRecipients" + ); + // Location + // Tags + // JunkStatus + // Attachments + // Account + // Custom + ensure_view_ordering( + Ci.nsMsgViewSortType.byCustom, + Ci.nsMsgViewSortOrder.ascending, + function (msgHdr) { + return authorFirstLetterCustomColumn.getSortStringForRow(msgHdr); + } + ); + // Received +} + +function test_number_of_messages() { + // Bug 574799 + if (gDBView.numMsgsInView != gTestFolder.getTotalMessages(false)) { + do_throw( + "numMsgsInView is " + + gDBView.numMsgsInView + + " but should be " + + gTestFolder.getTotalMessages(false) + + "\n" + ); + } + // Bug 600140 + // Maybe elided so open it, now only consider the first one + if (gDBView.isContainer(0) && !gDBView.isContainerOpen(0)) { + gDBView.toggleOpenState(0); + } + let numMsgInTree = gTreeView.rowCount; + if (gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort) { + for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) { + let flags = gDBView.getFlagsAt(iViewIndex); + if (flags & MSG_VIEW_FLAG_DUMMY) { + numMsgInTree--; + } + } + } + if (gDBView.numMsgsInView != numMsgInTree) { + view_throw( + "message in tree is " + + numMsgInTree + + " but should be " + + gDBView.numMsgsInView + + "\n" + ); + } +} + +function test_selected_messages() { + gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll); + + // Select one message + gTreeView.selection.select(1); + let selectedMessages = gDBView.getSelectedMsgHdrs(); + + if (selectedMessages.length != 1) { + do_throw( + "getSelectedMsgHdrs.length is " + + selectedMessages.length + + " but should be 1\n" + ); + } + + let firstSelectedMsg = gDBView.hdrForFirstSelectedMessage; + if (selectedMessages[0] != firstSelectedMsg) { + do_throw( + "getSelectedMsgHdrs[0] is " + + selectedMessages[0].messageKey + + " but should be " + + firstSelectedMsg.messageKey + + "\n" + ); + } + + // Select all messages + gTreeView.selection.selectAll(); + if (gDBView.numSelected != gTreeView.rowCount) { + do_throw( + "numSelected is " + + gDBView.numSelected + + " but should be " + + gTreeView.rowCount + + "\n" + ); + } + + selectedMessages = gDBView.getSelectedMsgHdrs(); + if (selectedMessages.length != gTestFolder.getTotalMessages(false)) { + do_throw( + "getSelectedMsgHdrs.length is " + + selectedMessages.length + + " but should be " + + gTestFolder.getTotalMessages(false) + + "\n" + ); + } + + for (let i = 0; i < selectedMessages.length; i++) { + let expectedHdr = gDBView.getMsgHdrAt(i); + if (!selectedMessages.includes(expectedHdr)) { + view_throw( + "Expected " + + expectedHdr.messageKey + + ":" + + expectedHdr.mime2DecodedSubject.substr(0, 30) + + " to be selected, but it wasn't\n" + ); + } + } + + gTreeView.selection.clearSelection(); +} + +function test_insert_remove_view_rows() { + // Test insertion/removal into m_keys. + let startCount = gTreeView.rowCount; + let index = 0; + let rows = 3; + let msgKey = rows * 1000; + let flags = 0; + let level = 0; + let folder = null; + let xfview = + gDBView.viewType == Ci.nsMsgViewType.eShowSearch || + gDBView.viewType == Ci.nsMsgViewType.eShowVirtualFolderResults; + if (xfview) { + folder = gDBView.getFolderForViewIndex(index); + } + + gDBView.insertTreeRows(index, rows, msgKey, flags, level, folder); + assert_view_row_count(startCount + rows); + let key = gDBView.getKeyAt(rows - 1); + if (key != msgKey) { + view_throw("msgKey is " + key + " but should be " + msgKey + "\n"); + } + gDBView.removeTreeRows(index, rows); + assert_view_row_count(startCount); + + // These should fail. + try { + gDBView.insertTreeRows(startCount + 10, rows, msgKey, flags, level, folder); + view_throw("expected exception not caught; inserting at illegal index \n"); + } catch (ex) {} + try { + gDBView.insertTreeRows(index, rows, msgKey, flags, level, folder); + gDBView.removeTreeRows(index, gTreeView.rowCount + 10); + view_throw("expected exception not caught; removing illegal rows \n"); + } catch (ex) { + gDBView.removeTreeRows(index, rows); + } + if (xfview) { + try { + gDBView.insertTreeRows(index, rows, msgKey, flags, level, null); + view_throw( + "expected exception not caught; folder required for xfvf view \n" + ); + } catch (ex) {} + } +} + +async function test_msg_added_to_search_view() { + // if the view is a non-grouped search view, test adding a header to + // the search results, and verify it gets put at top. + if (!(gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort)) { + gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.descending); + let [synMsg] = await make_and_add_message(); + let msgHdr = gTestFolder.msgDatabase.getMsgHdrForMessageID( + synMsg.messageId + ); + gDBView + .QueryInterface(Ci.nsIMsgSearchNotify) + .onSearchHit(msgHdr, msgHdr.folder); + assert_view_message_at_indices(synMsg, 0); + } +} + +function IsHdrChildOf(possibleParent, possibleChild) { + let parentHdrId = possibleParent.messageId; + let numRefs = possibleChild.numReferences; + for (let refIndex = 0; refIndex < numRefs; refIndex++) { + if (parentHdrId == possibleChild.getStringReference(refIndex)) { + return true; + } + } + return false; +} + +// This could be part of ensure_view_ordering() but I don't want to make that +// function any harder to read. +function test_threading_levels() { + if (!gTreeView.rowCount) { + do_throw("There are no rows in my folder! I can't test anything!"); + } + // only look at threaded, non-grouped views. + if ( + gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort || + !(gDBView.viewFlags & Ci.nsMsgViewFlagsType.kThreadedDisplay) + ) { + return; + } + + let prevLevel = 1; + let prevMsgHdr; + for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) { + let msgHdr = gDBView.getMsgHdrAt(iViewIndex); + let level = gTreeView.getLevel(iViewIndex); + if (level > prevLevel && msgHdr.subject != gSiblingsMissingParentsSubject) { + if (!IsHdrChildOf(prevMsgHdr, msgHdr)) { + view_throw("indented message not child of parent"); + } + } + prevLevel = level; + prevMsgHdr = msgHdr; + } +} + +function test_expand_collapse() { + let oldRowCount = gDBView.rowCount; + let thirdChild = gDBView.getMsgHdrAt(3); + gDBView.toggleOpenState(0); + if (gDBView.rowCount != oldRowCount - 9) { + view_throw("collapsing first item should have removed 9 items"); + } + + // test that expand/collapse works with killed sub-thread. + oldRowCount = gDBView.rowCount; + gTestFolder.msgDatabase.markHeaderKilled(thirdChild, true, null); + gDBView.toggleOpenState(0); + if (gDBView.rowCount != oldRowCount + 2) { + view_throw("expanding first item should have aded 2 items"); + } + gTestFolder.msgDatabase.markHeaderKilled(thirdChild, false, null); + oldRowCount = gDBView.rowCount; + gDBView.toggleOpenState(0); + if (gDBView.rowCount != oldRowCount - 2) { + view_throw("collapsing first item should have removed 2 items"); + } +} + +function test_qs_results() { + // This just tests that bug 505967 hasn't regressed. + if (gTreeView.getLevel(0) != 0) { + view_throw("first message should be at level 0"); + } + if (gTreeView.getLevel(1) != 1) { + view_throw("second message should be at level 1"); + } + if (gTreeView.getLevel(2) != 2) { + view_throw("third message should be at level 2"); + } + test_threading_levels(); +} + +async function test_group_sort_collapseAll_expandAll_threading() { + // - start with an empty folder + gTestFolder = await messageInjection.makeEmptyFolder(); + + // - create a normal unthreaded view + setup_view("threaded", 0); + + // - ensure it's empty + assert_view_empty(); + + // - add 3 messages: + // msg1: from A, custom column val A, to be starred + // msg2: from A, custom column val A + // msg3: from B, custom column val B + let [smsg1] = await make_and_add_message({ from: ["A", "A@a.invalid"] }); + await make_and_add_message({ from: ["A", "A@a.invalid"] }); + let [smsg3] = await make_and_add_message({ from: ["B", "B@b.invalid"] }); + + assert_view_row_count(3); + gDBView.getMsgHdrAt(0).markFlagged(true); + if (!gDBView.getMsgHdrAt(0).isFlagged) { + view_throw("Expected smsg1 to be flagged"); + } + + // - create grouped view; open folder in byFlagged AZ sort + setup_group_view( + Ci.nsMsgViewSortType.byFlagged, + Ci.nsMsgViewSortOrder.ascending, + gTestFolder + ); + // - make sure there are 5 rows; index 0 and 2 are dummy, 1 is flagged message, + // 3-4 are messages + assert_view_row_count(5); + assert_view_index_is_dummy(0); + assert_view_index_is_not_dummy(1); + assert_view_message_at_indices(smsg1, 1); + if (!gDBView.getMsgHdrAt(1).isFlagged) { + view_throw("Expected grouped smsg1 to be flagged"); + } + assert_view_index_is_dummy(2); + assert_view_index_is_not_dummy(3); + assert_view_index_is_not_dummy(4); + + // - collapse the grouped threads; there should be 2 dummy rows + gDBView.doCommand(Ci.nsMsgViewCommandType.collapseAll); + assert_view_row_count(2); + assert_view_index_is_dummy(0); + assert_view_index_is_dummy(1); + + // - expand the grouped threads; there should be 5 rows + gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll); + assert_view_row_count(5); + assert_view_index_is_dummy(0); + assert_view_index_is_dummy(2); + + // - reverse sort; create grouped view; open folder in byFlagged ZA sort + setup_group_view( + Ci.nsMsgViewSortType.byFlagged, + Ci.nsMsgViewSortOrder.descending, + gTestFolder + ); + // - make sure there are 5 rows; index 0 and 3 are dummy, 1-2 are messages, + // 4 is flagged message + assert_view_row_count(5); + assert_view_index_is_dummy(0); + assert_view_index_is_not_dummy(1); + assert_view_index_is_not_dummy(2); + assert_view_index_is_dummy(3); + assert_view_index_is_not_dummy(4); + assert_view_message_at_indices(smsg1, 4); + if (!gDBView.getMsgHdrAt(4).isFlagged) { + view_throw("Expected reverse sorted grouped smsg1 to be flagged"); + } + + // - test grouped by custom column; the custCol is first letter of author + // - create grouped view; open folder in byCustom ZA sort + setup_group_view( + Ci.nsMsgViewSortType.byCustom, + Ci.nsMsgViewSortOrder.descending, + gTestFolder + ); + + // - make sure there are 5 rows; index 0 and 2 are dummy, 1 is B value message, + // 3-4 are messages with A value + assert_view_row_count(5); + assert_view_index_is_dummy(0); + assert_view_index_is_not_dummy(1); + assert_view_message_at_indices(smsg3, 1); + if ( + authorFirstLetterCustomColumn.getSortStringForRow(gDBView.getMsgHdrAt(1)) != + "B" + ) { + view_throw( + "Expected grouped by custom column, ZA sortOrder smsg3 value to be B" + ); + } + assert_view_index_is_dummy(2); + assert_view_index_is_not_dummy(3); + assert_view_index_is_not_dummy(4); + if ( + authorFirstLetterCustomColumn.getSortStringForRow(gDBView.getMsgHdrAt(4)) != + "A" + ) { + view_throw( + "Expected grouped by custom column, ZA sortOrder smsg2 value to be A" + ); + } +} + +async function test_group_dummies_under_mutation_by_date() { + // - start with an empty folder + gTestFolder = await messageInjection.makeEmptyFolder(); + + // - create the view + setup_view("group", Ci.nsMsgViewFlagsType.kGroupBySort); + gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.ascending); + + // - ensure it's empty + assert_view_empty(); + + // - add a message from this week + // (we want to make sure all the messages end up in the same bucket and that + // the current day changing as we run the test does not change buckets + // either. bucket 1 is same day, bucket 2 is yesterday, bucket 3 is last + // week, so 2 days ago or older is always last week, even if we roll over + // and it becomes 3 days ago.) + let [smsg, synSet] = await make_and_add_message({ + age: { days: 2, hours: 1 }, + }); + + // - make sure the message and a dummy appear + assert_view_row_count(2); + assert_view_index_is_dummy(0); + assert_view_index_is_not_dummy(1); + assert_view_message_at_indices(smsg, 0, 1); + + // we used to display total in tag column - make sure we don't do that. + if (gDBView.cellTextForColumn(0, "tags") != "") { + view_throw("tag column shouldn't display total count in group view"); + } + + // - move the messages to the trash + await messageInjection.trashMessages(synSet); + + // - make sure the message and dummy disappear + assert_view_empty(); + + // - add two messages from this week (same date bucket concerns) + let [newer, newerSet] = await make_and_add_message({ + age: { days: 2, hours: 1 }, + }); + let [older] = await make_and_add_message({ age: { days: 2, hours: 2 } }); + + // - sanity check addition + assert_view_row_count(3); // 2 messages + 1 dummy + assert_view_index_is_dummy(0); + assert_view_index_is_not_dummy(1, 2); + // the dummy should be based off the older guy + assert_view_message_at_indices(older, 0, 1); + assert_view_message_at_indices(newer, 2); + + // - delete the message right under the dummy + // (this will be the newer one) + await messageInjection.trashMessages(newerSet); + + // - ensure we still have the dummy and the right child node + assert_view_row_count(2); + assert_view_index_is_dummy(0); + assert_view_index_is_not_dummy(1); + // now the dummy should be based off the remaining older one + assert_view_message_at_indices(older, 0, 1); +} + +async function test_xfvf_threading() { + // - start with an empty folder + let save_gTestFolder = gTestFolder; + gTestFolder = await messageInjection.makeEmptyFolder(); + + let messages = []; + // Add messages such that ancestors arrive after their descendents in + // various interesting ways. + // build a hierarchy like this (the UID order corresponds to the date order) + // 3 + // 1 + // 4 + // 2 + // 5 + let msg3 = gMessageGenerator.makeMessage({ age: { days: 2, hours: 5 } }); + let msg1 = gMessageGenerator.makeMessage({ + age: { days: 2, hours: 4 }, + inReplyTo: msg3, + }); + let msg4 = gMessageGenerator.makeMessage({ + age: { days: 2, hours: 3 }, + inReplyTo: msg1, + }); + let msg2 = gMessageGenerator.makeMessage({ + age: { days: 2, hours: 1 }, + inReplyTo: msg4, + }); + let msg5 = gMessageGenerator.makeMessage({ + age: { days: 2, hours: 2 }, + inReplyTo: msg1, + }); + messages = messages.concat([msg1, msg2, msg3, msg4, msg5]); + + let msgSet = new SyntheticMessageSet(messages); + + gTestFolder = await messageInjection.makeEmptyFolder(); + + // - create the view + await messageInjection.addSetsToFolders([gTestFolder], [msgSet]); + setup_view("xfvf", Ci.nsMsgViewFlagsType.kThreadedDisplay); + assert_view_row_count(5); + gDBView.toggleOpenState(0); + gDBView.toggleOpenState(0); + + assert_view_message_at_indices(msg3, 0); + assert_view_message_at_indices(msg1, 1); + assert_view_message_at_indices(msg4, 2); + assert_view_message_at_indices(msg2, 3); + assert_view_message_at_indices(msg5, 4); + assert_view_level_is(0, 0); + assert_view_level_is(1, 1); + assert_view_level_is(2, 2); + assert_view_level_is(3, 3); + assert_view_level_is(4, 2); + gTestFolder = save_gTestFolder; +} + +/* + * Tests the sorting order of collapsed threads, not of messages within + * threads. Currently limited to testing the sort-threads-by-date case, + * sorting both by thread root and by newest message. + */ +async function test_thread_sorting() { + let save_gTestFolder = gTestFolder; + gTestFolder = await messageInjection.makeEmptyFolder(); + let messages = []; + // build a hierarchy like this (the UID order corresponds to the date order) + // 1 + // 4 + // 2 + // 5 + // 3 + let msg1 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 10 } }); + let msg2 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 9 } }); + let msg3 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 8 } }); + let msg4 = gMessageGenerator.makeMessage({ + age: { days: 1, hours: 7 }, + inReplyTo: msg1, + }); + let msg5 = gMessageGenerator.makeMessage({ + age: { days: 1, hours: 6 }, + inReplyTo: msg2, + }); + messages = messages.concat([msg1, msg2, msg3, msg4, msg5]); + + let msgSet = new SyntheticMessageSet(messages); + + await messageInjection.addSetsToFolders([gTestFolder], [msgSet]); + + // test the non-default pref state first, so the pref gets left with its + // default value at the end + Services.prefs.setBoolPref("mailnews.sort_threads_by_root", true); + gDBView.open( + gTestFolder, + Ci.nsMsgViewSortType.byDate, + Ci.nsMsgViewSortOrder.ascending, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + {} + ); + + assert_view_row_count(3); + assert_view_message_at_indices(msg1, 0); + assert_view_message_at_indices(msg2, 1); + assert_view_message_at_indices(msg3, 2); + + gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.descending); + assert_view_message_at_indices(msg3, 0); + assert_view_message_at_indices(msg2, 1); + assert_view_message_at_indices(msg1, 2); + + Services.prefs.clearUserPref("mailnews.sort_threads_by_root"); + gDBView.open( + gTestFolder, + Ci.nsMsgViewSortType.byDate, + Ci.nsMsgViewSortOrder.ascending, + Ci.nsMsgViewFlagsType.kThreadedDisplay, + {} + ); + + assert_view_row_count(3); + assert_view_message_at_indices(msg3, 0); + assert_view_message_at_indices(msg1, 1); + assert_view_message_at_indices(msg2, 2); + + gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.descending); + assert_view_message_at_indices(msg2, 0); + assert_view_message_at_indices(msg1, 1); + assert_view_message_at_indices(msg3, 2); + + gDBView.close(); + gTestFolder = save_gTestFolder; +} + +const VIEW_TYPES = [ + ["threaded", Ci.nsMsgViewFlagsType.kThreadedDisplay], + ["quicksearch", Ci.nsMsgViewFlagsType.kThreadedDisplay], + ["search", Ci.nsMsgViewFlagsType.kThreadedDisplay], + ["search", Ci.nsMsgViewFlagsType.kGroupBySort], + ["xfvf", Ci.nsMsgViewFlagsType.kNone], + // group does unspeakable things to gTestFolder, so put it last. + ["group", Ci.nsMsgViewFlagsType.kGroupBySort], +]; + +/** + * These are tests which are for every test configuration. + */ +function tests_for_all_views() { + test_sort_columns(); + test_number_of_messages(); + test_selected_messages(); + test_insert_remove_view_rows(); +} + +add_setup(function () { + gMessages = setup_messages(); +}); + +add_task(async function test_threaded() { + await set_gTestFolder(gMessages); + let [view_type, view_flag] = VIEW_TYPES[0]; + setup_view(view_type, view_flag); + + tests_for_all_views(); + + // Specific tests for threaded. + test_expand_collapse(); + await test_thread_sorting(); +}); + +add_task(async function test_quicksearch_threaded() { + await set_gTestFolder(gMessages); + let [view_type, view_flag] = VIEW_TYPES[1]; + setup_view(view_type, view_flag); + + tests_for_all_views(); + + // Specific tests for quicksearch threaded. + test_qs_results(); +}); + +add_task(async function test_search_threaded() { + await set_gTestFolder(gMessages); + let [view_type, view_flag] = VIEW_TYPES[2]; + setup_view(view_type, view_flag); + + tests_for_all_views(); + + // Specific tests for search threaded. + await test_msg_added_to_search_view(); +}); + +add_task(async function test_search_group_by_sort() { + await set_gTestFolder(gMessages); + let [view_type, view_flag] = VIEW_TYPES[3]; + setup_view(view_type, view_flag); + + tests_for_all_views(); + + // Specific tests for search group by sort. + await test_msg_added_to_search_view(); +}); + +add_task(async function test_xfvf() { + await set_gTestFolder(gMessages); + let [view_type, view_flag] = VIEW_TYPES[4]; + setup_view(view_type, view_flag); + + tests_for_all_views(); + + // Specific tests for xfvf. + await test_xfvf_threading(); +}); + +add_task(async function test_group() { + await set_gTestFolder(gMessages); + let [view_type, view_flag] = VIEW_TYPES[5]; + setup_view(view_type, view_flag); + + tests_for_all_views(); + + // Specific tests for group. + await test_group_sort_collapseAll_expandAll_threading; + await test_group_dummies_under_mutation_by_date; +}); + +add_task(function test_teardown() { + // Delete view reference to avoid a cycle leak. + gFakeSelection.view = null; +}); |