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-mainregion
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.