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.
Boilerplate
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);
}
Asynchronous testing basics
Why do we need it?
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.
How does this affect my tests?
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:
The Asynchronous Function Contract
- A function should yield false or return false when something asynchronous is going on. When the asynchronous operation is complete, something needs to call
async_driver()
. Returning false tells the asynchronous driver that it should yield control up to the top-level. Callingasync_driver()
lets it know to start up again. - A function should yield true or return true when the asynchronous driver should continue executing without stopping.
- A function that does not return anything (or returns undefined) and is not a generator is treated like a function that returned true.
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.
Helpers for the Asynchronous Function Contract
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 ofAsyncUrlListener
.- copy listener:
asyncCopyListener
.
Bringing messages into the picture
Synthetic set definitions
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.)
- count: 10
- The number of messages that should be in the set. Note that the default is subject to change, so if you want 10, say 10, instead of relying on the default.
- msgsPerThread: 1
-
How many messages should be in each thread? The default is that every message is its own thread; no message is a reply to any other message. If you pass a number greater than 1, then you get a direct reply-chain that long. For example, if you pass 3, then 1 <- 2 <- 3 is what the reply chain looks like. If you need more complicated threads you will need to use the
MessageScenarioFactory
. - age: (strictly incrementing from arbitrary origin)
-
The default starts at Jan 1, 2000 and adds an hour for every message. If you pass an object, it should be an object with one or more of the following attributes:
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 passage_incr
. - age_incr: (no increment)
-
Takes an object of the same style taken by age. This specifies the time interval we should add to the date for each message. Due to a bug, you should pass negative values. (We will probably auto-correct that in the future.) If your set definition was
{count: 3, age: {days: 7}, age_incr: {days: -1}}
, then you would generate messages from 7, 6, and 5 days ago. - subject: (automatically generated random subject)
-
Force all the messages to have the same subject you pass in. For example,
{count: 1, subject: "my suitcase"}
would result in a single message with the subject "my suitcase" with a random sender and random recipient. - to: (automatically generated single recipient)
-
A list of recipients, where each recipient is a list whose first element is a (display) name and second element is an email address. For example,
{to: [["John Doe", "[email protected]"], ["John Smith", "[email protected]"]]}
would specify two recipients. - cc: (none)
- A list of recipients like 'to', but in this case the default is zero recipients.
- from: (automatically generated)
-
A list whose first element is a (display) name and second element is an email address. For example,
{count: 4, from: ["John Doe", "[email protected]"]}
would result in four messages from John Doe to random recipients.
Synthetic message sets
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.
Accessing synthetic messages and headers
- synMessages [attribute]
-
The JS list of
SyntheticMessages
held in the set. While you should not modify this list, you can get its length and read from it. - getMsgHdr(aIndex) [function]
-
Retrieve the
nsIMsgDBHdr
at the given index. - getMsgURI(aIndex) [function]
- Retrieve the URI of the message header at the given index.
- msgHdrList [getter]
-
Return a JS list with all the
nsIMsgDbHdrs
for the messages that have been injected into folders. - xpcomHdrArray [getter]
-
Return an
nsIMutableArray
of the message headers that have been injected into folders. - foldersWithMsgHdrs [getter]
-
Return a list where each element is a list with two sub-elements. The first is an
nsIMsgFolder
and the second is a JS list of all thensIMsgDBHdrs
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. - foldersWithXpcomHdrArrays [getter]
-
Same as
foldersWithMsgHdrs
but the second element in each sub-list is annsIMutableArray
instead of a JS list.
Manipulating messages
- setRead(aBeRead)
-
setRead(true)
marks all the messages in the set as read,setRead(false)
marks them as unread. - setStarred(aBeStarred)
-
setStarred(true)
marks all the messages in the set as starred ("flagged" in IMAP parlance),setStarred(false)
makes them not be starred. - addTag(aTagKey)
-
Adds the given tag to all the messages in the set. The tag key must correspond to an existing tag. The tag key is not the label, but what is actually stored on the IMAP server. For example, all our default tags are actually things like
$label1
and$label2
, but that is not what we show to the user. - removeTag(aTagKey)
-
Removes the given tag from all the messages in the set. See the
addTag
notes. - setJunk(aBeJunk)
-
Sets the junk score for the messages in the set as junk (true) / not junk (false). This does not involves the bayesian classifier and does not do anything like moving the message to the junk folder. It does, however, send a
JunkStatusChanged
notification via thensIMsgFolderNotificationService
'sitemEvent
mechanism.
Set manipulation
- union(aOtherSet)
-
Take the union of this set and the provided other set and return the (new, not modified) result. You should stop using both the message set that you called this on and the other set that you passed in unless you are very careful. Namely, if you use
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. - slice(...)
-
Uses the JS
Array.slice
semantics to slice the set. Same warnings as union apply.
Configuring message injection
- Local Injection
-
let inboxFolder = configure_message_injection({mode: "local"});
Set up message injection to happen locally. This is different from using a POP fake-server. We just cram them usingaddMessage
, although we try and approximate what would happen with POP in terms of still invoking filters and such. - IMAP Injection, do not bring messages offline
-
let inboxFolder = configure_message_injection({mode: "imap", offline: false});
Use an IMAP fake-server to inject messages. We do not mark the folders as offline and therefore do not attempt to download the messages so that they are immediately available for offline use. If you later want to bring the messages offline, use themake_folder_and_contents_offline
function. - IMAP Injection, do bring messages offline
-
let inboxFolder = configure_message_injection({mode: "imap", offline: true});
Use an IMAP fake-server to inject messages. We mark the folders as offline and download the messages so that they are immediately available for offline use.
Creating / injecting messages
All of these functions take synthetic set definitions. Look at the section on Synthetic Message Sets above to understand how to specify these.
- Create a single folder with messages in it
-
let [folder, set1, set2, ...] = make_folder_with_sets([aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();
This creates a folder with an automatically generated name and adds one or more sets of messages to it. If multiple sets are specified, their messages will be interleaved. Example:
let [folder, messageSet] = make_folder_with_sets([{count: 3}]);
This creates a new folder with a set of three messages in it and saves the folder handle into 'folder' and the message set into 'messageSet'. Another example:
let [fooBarFolder, fooSet, barSet] = make_folder_with_sets([{count: 3, subject: "foo"}, {count: 3, subject: "bar"}]);
This creates a folder with two message sets in it. Three of the messages will have the subject "foo" and three will have the subject "bar". Because the message sets are interleaved, if you read the subject lines in the order they were added (or in date order), you would read "foo", "bar", "foo", "bar", "foo", "bar". - Create multiple folders with a set of messages distributed across the folders
-
let [[folder1, folder2, ...], set1, set2, ...] = make_folders_with_sets(aFolderCount, [aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();
Likemake_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. - Add messages to an existing folder
-
let [set1, set2, ...] = make_new_sets_in_folder(aFolder, [aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();
Create one or more new sets of messages and add them to a Folder. - Add messages distributed among multiple folders
-
let [set1, set2, ...] = make_new_sets_in_folder([aFolder1, aFolder2, ...], [aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();
Create one or more new sets of messages and add them to the provided folders. Likemake_folders_with_sets
, the messages are spread across the folders.
Folders
- Create a folder
-
let folderHandle = make_empty_folder(aOptionalFolderName);
You do not have to specify a folder name - if you don't care, we'll make one up for you. If you need to get fancy about having flags set on the folder, we support that, but please check the method documentation. - Get / create the Junk folder
-
let junkFolder = get_junk_folder();
- Create a Virtual folder (a folder whose contents are the result of a saved search)
-
let virtualFolder = make_virtual_folder([aFolderToSearch1, aFolderToSearch2, ...],
{subject: "", body: "", from: "", to: "", cc: "", recipient: "", involves: ""},
aAndTermsTogether, aOptionalName);
This is a convenience function to help you create a new virtual folder. The first argument is the list of folders we should search. The second argument defines the search query; each attribute defines a "contains" string constraint. If you include the attribute, the constraint is created. So if you want no constraints, pass an empty object, but if you just want a subject constraint, pass {subject: "foo"}. The third argument decides whether to "AND" together multiple constraints (if there are multiple constraints); true for AND, false for OR. - Mark an IMAP folder as offline and bring the messages offline
-
yield make_folder_and_contents_offline(folderHandle);
Messages and folders
- Move messages to a folder
-
yield async_move_messages(aSynMessageSet, aDestFolder);
- Trash messages (move them to the trash folder)
-
yield async_trash_messages(aSynMessageSet);
- Empty the trash folder
-
yield async_empty_trash();
- Delete messages (without moving them to the trash folder)
-
yield async_delete_messages(aSynMessageSet);
Implementation details
The following files make up the framework:
- asyncTestUtils.js: Core async testing logic. Dependent on
logHelper.js
so it can log what test it is on, etc. - logHelper.js: This allows rich logging via LogSploder. If you aren't using LogSploder, then this just makes your tests fail if errors get logged to the error console (like you see if you go to the "Tools | Error Console" menu).
- messageGenerator.js: Provides the
SyntheticMessage
abstraction andMessageGenerator
class that can generate one or moreSyntheticMessages
at a time. TheMessageScenarioFactory
produces specific pre-configured message threading configurations using fresh messages (important in avoiding duplicate messages for code that cares, like gloda). - messageModifier.js: Slightly misnamed, this provides the
SyntheticMessageSet
abstraction that is a set abstraction forSyntheticMessages
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. - messageInjection.js: All the logic for injecting messages into folders via local / IMAP and then doing further folder-level manipulations.