Writing Mochitest Tests

Tips on writing Mochitest tests for Thunderbird.

This document offers some basic tips for writing Mochitest tests. (See also this Mochitest page in the Firefox docs, which is Firefox-centric but may still be useful.)

Adding a New Test

You may be writing a new test in an existing test file, or you may have set up a new test file as described in Adding Tests. Either way, add a new test with the add_task function:

add_task(async () => {
  // The code for the test goes here.
});

Helper Functions

Many essential functions live in these files.

EventUtils and BrowserTestUtils do not need to be imported as they are already available in Mochitest files. mailTestUtils requires importing:

const { mailTestUtils } = ChromeUtils.import(
  "resource://testing-common/mailnews/MailTestUtils.jsm"
);

This document is a basic introduction. To go further, explore these files (particularly the docstrings of the various functions in them) and look at existing tests.

Assertion Functions

Use the is and ok functions to make test assertions. is compares two values (using JavaScript's === equality comparison). ok asserts that a single value is truthy (in JavaScript's sense of truthy).

The last argument to each is a message printed to the console to identify the assertion. It is optional but is a good practice for more understandable test logs.

let sum = 3 * 4;
is(sum, 12, "multiplication appears to still be working");

let truthy = true;
ok(truthy, "thing is not falsy");

Mouse Clicks

Do a single click on a DOM element:

let element = document.getElementById("some-element-id");

EventUtils.synthesizeMouseAtCenter(element, { clickCount: 1 });

To double-click change the clickCount from 1 to 2. There is a shorthand for a single click:

EventUtils.synthesizeMouseAtCenter(element, {});

If the test is interacting with a window that is not the main one, pass the relevant window as the (optional) third argument:

EventUtils.synthesizeMouseAtCenter(element, {}, anotherWindow);

Keyboard Keys

Type some text with the keyboard, or type a single key, even a non-character one like the tab key:

EventUtils.sendString("some text");

EventUtils.sendKey("TAB");

Some other valid keys for sendKey include: RETURN, BACK_SPACE, DELETE, HOME, END, UP, DOWN, LEFT, RIGHT, PAGE_UP, PAGE_DOWN, SHIFT, CONTROL, ALT, ESCAPE, F1, F2, etc. (Not an exhaustive list.)

If the test is interacting with a window that is not the main one, pass the relevant window as the (optional) second argument:

EventUtils.sendString("some text", anotherWindow);

EventUtils.sendKey("TAB", anotherWindow);

Modifier Keys (Ctrl, Alt, Shift)

To press a key along with one or more modifier keys:

// Ctrl+A:
EventUtils.synthesizeKey("a", { accelKey: true });

// Ctrl+Alt+B (no shift key):
EventUtils.synthesizeKey("b", {
  accelKey: true,
  altKey: true,
  shiftKey: false
});

Some other options are altKey, shiftKey, and ctrlKey (a non-exhaustive list).

Similar to sendString and sendKey, there is an optional third window argument to use when interacting with a specific window.

Waiting for Events

Tests move faster than users. Sometimes too fast. The test may need to wait for an event to occur before doing the next thing.

let element = document.getElementById("some-element-id");

let event = await BrowserTestUtils.waitForEvent(element, "focus");

is(event.type, "focus", "element is focused");

Letting Thunderbird Respond Before Proceeding

Sometimes the test needs to let the application respond to something the test did before moving on to the next step, and there is not an event to listen for. Here is a simple way to do this:

// Do something here involving the UI.

// Let the application finish responding to what the test just did.
await new Promise(resolve => setTimeout(resolve));

// Do the next thing.

Interacting with Regular Windows

Some tests will need to interact with windows that are not the main window. For example, below is a function that opens the address book window. It returns the window (nsIDOMWindow) object for the address book window, which can then be used when calling functions like EventUtils.sendKey.

The key point is the use of BrowserTestUtils.domWindowOpened, but this example also demonstrates some of the other tips found in this document. (See below for dialog windows which are handled differently.)

async function openAddressBookWindow() {
  // Set up a watcher for "domwindowopened". When DOM windows are opened the
  // function supplied as a second argument is called. When it returns true the
  // promise is resolved as the nsIDOMWindow object for the window.
  let addressBookWindowPromise = BrowserTestUtils.domWindowOpened(
    null,
    async win => {
      // win is the nsIDOMWindow object for a window that is opening.

      // Wait until the "load" event has happened for the window.
      await BrowserTestUtils.waitForEvent(win, "load");

      // Return true when we have the right window.
      return (
        win.document.documentURI ==
        "chrome://messenger/content/addressbook/addressbook.xul"
      );
    }
  );

  // Open the address book window.
  const addressBookButton = document.getElementById("button-address");
  EventUtils.synthesizeMouseAtCenter(addressBookButton, { clickCount: 1 });

  // Wait for the promise to resolve. abWindow is a nsIDOMWindow object.
  let abWindow = await addressBookWindowPromise;

  // There is no event that fires when the JavaScript that is initially loaded
  // in the window has finished running, so let it finish by using a setTimeout.
  await new Promise(resolve => abWindow.setTimeout(resolve));

  // Assert that the window was opened successfully.
  ok(abWindow && abWindow instanceof Window, "address book window was opened");

  return abWindow;
}

Interacting with Dialog Windows

Interact with dialog windows by using BrowserTestUtils.promiseAlertDialog.

// The third argument is a function that interacts with the dialog window.
let dialogWindowPromise = BrowserTestUtils.promiseAlertDialog(
  null,
  "chrome://path/to/the/dialog.xul",
  async dialogWindow => {
    // dialogWindow is an nsIDOMWindow object.
    let doc = dialogWindow.document;
    let dialogElement = doc.querySelector("dialog");

    // More code to interact with the dialog goes here.

    // Usually click a button to close the dialog.
    dialogElement.getButton("accept").click();
  }
);

// Code to open the dialog window goes here.

// Wait for the dialog to close.
await dialogWindowPromise;

Interacting with Trees

Trees are not like other DOM elements and require special handling.

// Get the text from a given cell in a tree:

let tree = document.getElementById("some-tree");
let rowNumber = 1;
let columnNumber = 1;
let cellText = tree.view.getCellText(rowNumber, tree.columns[columnNumber]);

// Click on a given cell in a tree:

const { mailTestUtils } = ChromeUtils.import(
  "resource://testing-common/mailnews/MailTestUtils.jsm"
);

mailTestUtils.treeClick(EventUtils, window, tree, rowNumber, columnNumber, {
  clickCount: 1,
});

Last updated