Revision control

Copy as Markdown

Other Tools

/* 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 descendants. 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.importESModule(
);
const { TreeSelection } = ChromeUtils.importESModule(
);
var { MessageInjection } = ChromeUtils.importESModule(
);
var { dump_view_contents } = ChromeUtils.importESModule(
);
// 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));
const 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
const msg1 = gMessageGenerator.makeMessage();
const msg2 = gMessageGenerator.makeMessage({ inReplyTo: msg1 });
const msg3 = gMessageGenerator.makeMessage({ inReplyTo: msg1 });
const msg4 = gMessageGenerator.makeMessage({ inReplyTo: msg2 });
messages = messages.concat([msg1, msg2, msg3, msg4]);
// test bug 600140, make a thread that Reply message has smaller MsgKey
const msgBiggerKey = gMessageGenerator.makeMessage();
const msgSmallerKey = gMessageGenerator.makeMessage({
inReplyTo: msgBiggerKey,
});
messages = messages.concat([msgSmallerKey, msgBiggerKey]);
const 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
const synMsg = gMessageGenerator.makeMessage(aMessageArgs);
const 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 (const viewIndex of aArgs) {
const 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 (const viewIndex of aArgs) {
const 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 (const thing of aArgs) {
if (typeof thing == "number") {
const 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) {
const 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() {
return "";
},
getRowProperties() {
return "";
},
getImageSrc() {
return null;
},
getSortLongForRow() {
return 0;
},
};
var gDBView;
var gTreeView;
var MSG_VIEW_FLAG_DUMMY = 0x20000000;
var gFakeSelection = new TreeSelection(null);
function setup_view(aViewType, aViewFlags, aTestFolder) {
const 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);
gDBView.open(
aViewType != "search" ? aTestFolder : null,
Ci.nsMsgViewSortType.byDate,
aViewType != "search"
? Ci.nsMsgViewSortOrder.ascending
: Ci.nsMsgViewSortOrder.descending,
aViewFlags
);
// we need to cram messages into the search via nsIMsgSearchNotify interface
if (
aViewType == "search" ||
aViewType == "quicksearch" ||
aViewType == "xfvf"
) {
const searchNotify = gDBView.QueryInterface(Ci.nsIMsgSearchNotify);
searchNotify.onNewSearch();
for (const 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) {
const dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=group";
if (aTestFolder == null) {
aTestFolder = gTestFolder;
}
// grouped view uses these flags
const 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)
const msgFolder = gDBView.msgFolder;
gDBView.close();
gDBView.open(msgFolder, aSortBy, aDirection, gDBView.viewFlags, {});
} else {
gDBView.sort(aSortBy, aDirection);
}
const comparisonValuesByLevel = [];
const expectedLevel0CmpResult =
aDirection == Ci.nsMsgViewSortOrder.ascending ? 1 : -1;
const comparator = generalCmp;
let dummyCount = 0,
emptyDummyCount = 0;
const valueGetter =
typeof aKeyOrValueGetter == "string"
? function (msgHdr) {
return msgHdr[aKeyOrValueGetter];
}
: aKeyOrValueGetter;
const 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.
const previouslySeenGroupValues = {};
for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) {
const msgHdr = gDBView.getMsgHdrAt(iViewIndex);
const 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
const level = gTreeView.getLevel(iViewIndex);
// nuke existing comparison levels
if (level < comparisonValuesByLevel.length - 1) {
comparisonValuesByLevel.splice(level);
}
// get the value for comparison
const curValue = valueGetter(msgHdr);
if (inGroup) {
const 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
const prevValue = comparisonValuesByLevel[level - 1];
const cmpResult = comparator(curValue, prevValue);
const 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() {
// 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() {
// 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() {
if (gDBView.numMsgsInView != gTestFolder.getTotalMessages(false)) {
do_throw(
"numMsgsInView is " +
gDBView.numMsgsInView +
" but should be " +
gTestFolder.getTotalMessages(false) +
"\n"
);
}
// 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++) {
const 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"
);
}
const 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++) {
const 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.
const startCount = gTreeView.rowCount;
const index = 0;
const rows = 3;
const msgKey = rows * 1000;
const flags = 0;
const level = 0;
let folder = null;
const 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);
const 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);
const [synMsg] = await make_and_add_message();
const 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) {
const parentHdrId = possibleParent.messageId;
const 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++) {
const msgHdr = gDBView.getMsgHdrAt(iViewIndex);
const 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;
const 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
const [smsg1] = await make_and_add_message({ from: ["A", "A@a.invalid"] });
await make_and_add_message({ from: ["A", "A@a.invalid"] });
const [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.)
const [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)
const [newer, newerSet] = await make_and_add_message({
age: { days: 2, hours: 1 },
});
const [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
const save_gTestFolder = gTestFolder;
gTestFolder = await messageInjection.makeEmptyFolder();
let messages = [];
// Add messages such that ancestors arrive after their descendants in
// various interesting ways.
// build a hierarchy like this (the UID order corresponds to the date order)
// 3
// 1
// 4
// 2
// 5
const msg3 = gMessageGenerator.makeMessage({ age: { days: 2, hours: 5 } });
const msg1 = gMessageGenerator.makeMessage({
age: { days: 2, hours: 4 },
inReplyTo: msg3,
});
const msg4 = gMessageGenerator.makeMessage({
age: { days: 2, hours: 3 },
inReplyTo: msg1,
});
const msg2 = gMessageGenerator.makeMessage({
age: { days: 2, hours: 1 },
inReplyTo: msg4,
});
const msg5 = gMessageGenerator.makeMessage({
age: { days: 2, hours: 2 },
inReplyTo: msg1,
});
messages = messages.concat([msg1, msg2, msg3, msg4, msg5]);
const 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() {
const 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
const msg1 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 10 } });
const msg2 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 9 } });
const msg3 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 8 } });
const msg4 = gMessageGenerator.makeMessage({
age: { days: 1, hours: 7 },
inReplyTo: msg1,
});
const msg5 = gMessageGenerator.makeMessage({
age: { days: 1, hours: 6 },
inReplyTo: msg2,
});
messages = messages.concat([msg1, msg2, msg3, msg4, msg5]);
const 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);
const [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);
const [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);
const [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);
const [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);
const [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);
const [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;
});