The AsyncTestUtils Extended Framework is one mechanism for testing the MailNews component of Thunderbird. See MailNews automated testing for a description of the other testing mechanisms.
Add the following code to the top of your test file to import everything you need:
load("../../mailnews/resources/logHelper.js");
load("../../mailnews/resources/asyncTestUtils.js");
load("../../mailnews/resources/messageGenerator.js");
load("../../mailnews/resources/messageModifier.js");
load("../../mailnews/resources/messageInjection.js");
If the directory where you are adding the tests does not have a head_*.js
file that has the two following lines, add them at the top of your test file (before the lines shown above):
load("../../mailnews/resources/mailDirService.js");
load("../../mailnews/resources/mailTestUtils.js");
At the bottom of the test file, add the following:
var tests =[
// list your tests here
];
function run_test() {
configure_message_injection({mode: "local"});
async_run_tests(tests);
}
Sometimes in your tests you need to wait for an operation to complete that does not occur synchronously (that is, it is not done when the function call you made to initiate the operation returns control to you). This is likely to happen when I/O is involved or a potentially expensive process wants to break itself up into smaller chunks (like a search operation) so that the UI stays responsive.
In the Mozilla platform, the "top level" of a program is the event loop. It is a never-ending loop that dequeues pending events and runs them. This is how asynchronous callbacks get their chance to run again. When I/O results in newly read data it places an event in the queue. When a timer fires because the requested duration is up, it also places an event in the queue.
In order to give these events a chance to run we either need to make sure we yield control to the top-level event loop or spin our own nested event loop. AsyncTestUtils is currently implemented by the first method (yielding control to the top-level event loop). MozMill is an example of a testing framework that uses a nested event loop. In the future we will probably move the AsyncTestUtils framework to a nested event loop in a backwards-compatible fashion.
Thanks to JavaScript enhancements available on the Mozilla platform, it is possible for a function to yield control in such a way that the function stops running at the line where you use a yield
statement and resumes execution on the next line when resumed. This allows you to write reasonably normal looking functions instead of having to chain together a whole bunch of functions and callbacks. Your test functions need to agree to the following contract:
async_driver()
. Returning false tells the asynchronous driver that it should yield control up to the top-level. Calling async_driver()
lets it know to start up again.Most of the things you will want to do already have helper functions that take care of all of this, so all you need to do is pass their return values through. For example, you would do "yield async_move_messages(...);
" and be done with it.
It can be annoying to have to write interface boilerplate just to call async_driver
. If you have the following types of listeners you can use the following pre-defined helpers:
nsIUrlListener
: asyncUrlListener
(predefined). If you need to wrap an existing url listener or need a callback or fancy promise, create an instance of AsyncUrlListener
.asyncCopyListener
.Most of the code involved in creating synthetic messages takes an object that defines how to generate the set. The following is a list of frequently used attributes where the default value is listed after the attribute name. There are more attributes you can specify; consult the documentation for more information. (See the bottom of this page for links to the source files.)
MessageScenarioFactory
.minutes
, hours
, days
, weeks
. These attributes specify how old the message should be. For example, {weeks: 2, days: 3}
would be a message sent exactly 17 days ago. If you pass age, you should also pass age_incr
.{count: 3, age: {days: 7}, age_incr: {days: -1}}
, then you would generate messages from 7, 6, and 5 days ago.{count: 1, subject: "my suitcase"}
would result in a single message with the subject "my suitcase" with a random sender and random recipient.{to: [["John Doe", "john_doe@example.com"], ["John Smith", "smitty@example.com"]]}
would specify two recipients.{count: 4, from: ["John Doe", "john_doe@example.com"]}
would result in four messages from John Doe to random recipients.The code that creates synthetic message sets returns instances of the SyntheticMessageSet
class. This class not only holds references to the SyntheticMessage
instances, but it also tracks what folders they were injected into as well as what folders you move them to. This allows you to get at the nsIMsgDBHdr instances directly. Keep in mind that the class is not magic and will lose track of the message headers if you manipulate them without referencing the message set.
SyntheticMessages
held in the set. While you should not modify this list, you can get its length and read from it.nsIMsgDBHdr
at the given index.nsIMsgDbHdrs
for the messages that have been injected into folders.nsIMutableArray
of the message headers that have been injected into folders.nsIMsgFolder
and the second is a JS list of all the nsIMsgDBHdrs
for the messages that were inserted into the folder. This is used by routines that need to process the messages grouped by the folder they belong to, such as initiating message moves.foldersWithMsgHdrs
but the second element in each sub-list is an nsIMutableArray
instead of a JS list.setRead(true)
marks all the messages in the set as read, setRead(false)
marks them as unread.setStarred(true)
marks all the messages in the set as starred ("flagged" in IMAP parlance), setStarred(false)
makes them not be starred.$label1
and $label2
, but that is not what we show to the user.addTag
notes.JunkStatusChanged
notification via the nsIMsgFolderNotificationService
's itemEvent
mechanism.async_move_messages / async_trash_messages
on the resulting set, the original sets won't know the messages moved and will get confused if you try and access headers via them again.Array.slice
semantics to slice the set. Same warnings as union apply.let inboxFolder = configure_message_injection({mode: "local"});
addMessage
, although we try and approximate what would happen with POP in terms of still invoking filters and such.let inboxFolder = configure_message_injection({mode: "imap", offline: false});
make_folder_and_contents_offline
function.let inboxFolder = configure_message_injection({mode: "imap", offline: true});
All of these functions take synthetic set definitions. Look at the section on Synthetic Message Sets above to understand how to specify these.
let [folder, set1, set2, ...] = make_folder_with_sets([aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();
let [folder, messageSet] = make_folder_with_sets([{count: 3}]);
let [fooBarFolder, fooSet, barSet] = make_folder_with_sets([{count: 3, subject: "foo"}, {count: 3, subject: "bar"}]);
let [[folder1, folder2, ...], set1, set2, ...] = make_folders_with_sets(aFolderCount, [aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();
make_folder_with_sets
but multiple folders are created and the messages are spread across the folders. You would only want to do this when you are testing logic that could be impacted by having multiple folders.let [set1, set2, ...] = make_new_sets_in_folder(aFolder, [aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();
let [set1, set2, ...] = make_new_sets_in_folder([aFolder1, aFolder2, ...], [aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();
make_folders_with_sets
, the messages are spread across the folders.let folderHandle = make_empty_folder(aOptionalFolderName);
let junkFolder = get_junk_folder();
let virtualFolder = make_virtual_folder([aFolderToSearch1, aFolderToSearch2, ...],
{subject: "", body: "", from: "", to: "", cc: "", recipient: "", involves: ""},
aAndTermsTogether, aOptionalName);
yield make_folder_and_contents_offline(folderHandle);
yield async_move_messages(aSynMessageSet, aDestFolder);
yield async_trash_messages(aSynMessageSet);
yield async_empty_trash();
yield async_delete_messages(aSynMessageSet);
The following files make up the framework:
logHelper.js
so it can log what test it is on, etc.SyntheticMessage
abstraction and MessageGenerator
class that can generate one or more SyntheticMessages
at a time. The MessageScenarioFactory
produces specific pre-configured message threading configurations using fresh messages (important in avoiding duplicate messages for code that cares, like gloda).SyntheticMessageSet
abstraction that is a set abstraction for SyntheticMessages
that also tracks what folders they got injected into and provides code to directly manipulate or aid other code that wants to directly manipulate them.