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