summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/test/unit/test_nsMsgDBView.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/base/test/unit/test_nsMsgDBView.js')
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgDBView.js1212
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;
+});