Axe Accessibility Checks

Thunderbird browser-chrome tests can run axe-core against content browsers and chrome windows through AxeHelpers.sys.mjs. Use these helpers to add automated accessibility checks to normal functional tests.

const { checkAxe, startAxeMutationObserver } = ChromeUtils.importESModule(
  "resource://testing-common/mail/AxeHelpers.sys.mjs"
);

Axe checks are ordinary test assertions. A failure is reported in the normal test output with the failing rule id, impact, axe help text, help URL, and target selector. There is no separate report to collect.

One-shot checks

Use checkAxe when the test reaches a stable UI state and you want to scan a content browser once.

await checkAxe(tab.browser, {
  context: "#settingsPane",
  message: "Settings pane has no axe violations",
  specialPowers: SpecialPowers,
});

Use checkAxeInWindow for chrome windows.

const { checkAxeInWindow } = ChromeUtils.importESModule(
  "resource://testing-common/mail/AxeHelpers.sys.mjs"
);

await checkAxeInWindow(window, {
  context: document.querySelector("#toolbar-context-menu"),
  message: "Toolbar context menu has no axe violations",
});

The context option is passed to axe.run. Keep it as small as the UI under test allows, such as a dialog, pane, notification, or fixture root.

Watching DOM Mutations

Use a mutation observer when a functional test drives UI changes over time and you want axe to check the states produced by those changes. The watcher runs once when it starts, then runs again after observed DOM mutations while the test continues.

For content tabs, use startAxeMutationObserver.

const { startAxeMutationObserver } = ChromeUtils.importESModule(
  "resource://testing-common/mail/AxeHelpers.sys.mjs"
);

let browser;
let tab;

add_setup(async function () {
  tab = tabmail.openTab("contentTab", {
    url: "chrome://mochitests/content/browser/comm/path/to/test.xhtml",
  });
  await BrowserTestUtils.browserLoaded(tab.browser);
  browser = tab.browser;

  await startAxeMutationObserver(tab.browser, {
    container: "#fixtureRoot",
    message: "Content tab stayed axe-clean while the test mutated the DOM",
    specialPowers: SpecialPowers,
  });

  registerCleanupFunction(() => {
    tabmail.closeTab(tab);
  });
});

add_task(async function test_content_tab_ui() {
  // Run the functional test steps that mutate #fixtureRoot.
  EventUtils.synthesizeMouseAtCenter(
    browser.contentWindow.document.querySelector("button"),
    {},
    browser.contentWindow
  );
});

For chrome windows, use startAxeMutationObserverInWindow.

const { startAxeMutationObserverInWindow } = ChromeUtils.importESModule(
  "resource://testing-common/mail/AxeHelpers.sys.mjs"
);

let axeWatcher;
let dialog;

add_setup(async function () {
  dialog = await openTestDialog();

  axeWatcher = await startAxeMutationObserverInWindow(window, {
    autoFinish: false,
    container: dialog,
    message: "Dialog stayed axe-clean while the test mutated it",
  });

  registerCleanupFunction(async () => {
    await axeWatcher.finish();
    await closeTestDialog(dialog);
  });
});

add_task(async function test_chrome_window_ui() {
  // Run the functional test steps that mutate the dialog.
  EventUtils.synthesizeMouseAtCenter(
    dialog.querySelector("button"),
    {},
    window
  );
});

By default the watcher registers a cleanup function and finishes automatically. This is what the content-tab example uses. In browser-chrome tests that need to tear down chrome UI in the same cleanup path, set autoFinish to false and finish the watcher before closing or navigating the window or dialog under observation, as shown in the chrome-window example. finish() stops observing, runs a final check, and asserts that no watcher errors or axe violations were collected. Use flush() to force any pending check to run and inspect the current report before the end of the test.

The watcher uses a throttle, not a debounce. While mutations continue, checks are scheduled at most once per throttleMs interval. The default is 10 ms.

Context and Container

context controls what axe scans. container controls what the mutation observer watches. If context is omitted for a mutation watcher, the helper uses the watched container as the axe context.

For content browsers, pass selector strings because the observer runs in the content process. If no content-browser container or string context is provided, the observer watches document.body, falling back to documentElement.

For chrome windows, container may be either an element or a selector string. If no chrome-window container or string context is provided, the observer watches the document root.

Content Browser Defaults

Content tabs in browser tests are often fixtures or partial documents. They are not expected to have full page-level landmarks, so content-browser helpers disable these axe rules by default:

  • landmark-one-main

  • region

Chrome-window helpers do not apply those defaults. A test can still override any axe rule through axeOptions.

await checkAxe(tab.browser, {
  axeOptions: {
    rules: {
      "landmark-one-main": { enabled: true },
    },
  },
  specialPowers: SpecialPowers,
});

Fluent and Build Skips

The helpers wait for Fluent localization before every axe run. When a document has document.l10n, the helper waits for l10n.ready, calls translateRoots(), and waits for a frame so translated strings are present in the DOM before axe scans it.

Axe checks are skipped on debug, ASan, TSan, and code coverage builds. The helpers return empty skipped results in those configurations so the same test can still run.

SpecialPowers

Content-browser helpers use SpecialPowers.spawn to inject and run axe in the content process. If the helper cannot find SpecialPowers from the test global or browser owner, pass it explicitly:

await checkAxe(tab.browser, {
  specialPowers: SpecialPowers,
});

Chrome-window helpers run in the current process and do not need SpecialPowers.spawn.

Vendored axe-core

The axe browser bundle is vendored under third_party/axe-core. The build exposes third_party/axe-core/axe.min.js as resource://testing-common/mail/axe.min.js, which AxeHelpers.sys.mjs injects into the target document. Tests should import and use the helper module instead of loading axe.min.js directly.

The vendoring metadata is in third_party/axe-core/moz.yaml. The custom vendor script downloads the published npm package, verifies the npm integrity hash, and copies the files Thunderbird keeps.

To update axe-core, run mach vendor with the desired npm package version:

../mach vendor -r 4.12.1 third_party/axe-core/moz.yaml

Use the new version number in place of 4.12.1.