PATTERN Cited by 1 source
A11y checks via Playwright fixture extension¶
Context¶
A team has a growing Playwright E2E suite and wants to add accessibility (Axe) checks without: (a) forcing test authors to learn a new framework, (b) doubling the number of tests, or (c) refactoring the framework core.
The intuitive integration — bake Axe into Playwright's Locator interaction methods (run Axe after every click, navigation, etc.) — is blocked by Locator auto-wait semantics: Locator guarantees individual-element readiness, not whole-page readiness, so embedded Axe runs at the wrong time.
The pattern¶
Extend Playwright's custom fixture (the team's existing per-test setup surface) with an accessibility helper method. Test authors invoke it explicitly at page-ready moments.
// Custom fixture (existing):
export const test = base.extend<{ slack: SlackFixture }>({
slack: async ({ page }, use) => {
const slack = new SlackFixture(page);
await use(slack);
},
});
// SlackFixture.utils.a11y.runAxeAndSaveViolations() is the
// new extension point:
class A11y {
constructor(private page: Page) {
this.defaultTags = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];
this.baseFileName = `${test.info().title}-violations`.replace(/\//g, '-');
}
async runAxeAndSaveViolations() {
const axe = new AxeBuilder({ page: this.page }).withTags(this.defaultTags);
constants.ACCESSIBILITY.AXE_EXCLUDED_SELECTORS.forEach(sel => {
axe.exclude(sel);
});
const { violations } = await axe.analyze();
const filtered = this.filterAndRemoveDuplicateViolations(violations);
await this.saveViolations(filtered);
}
private filterAndRemoveDuplicateViolations(violations: Violation[]) {
return violations
.filter(v => ['critical'].includes(v.impact))
.map(this.mapViolation)
.filter(this.isUniqueViolation.bind(this));
}
}
// In a test:
test('settings view', async ({ slack }) => {
await slack.loginAs('user1');
await slack.nav.openSettings();
await slack.utils.a11y.runAxeAndSaveViolations(); // explicit
});
Why the fixture is the right extension point¶
- Fixtures are the per-test setup surface Playwright
already provides. Teams with non-trivial Playwright suites
have already built custom fixtures for login, API-seeding,
UI navigation helpers. Adding an
a11yutility is additive to an existing surface rather than a new one. - Fixtures compose naturally.
slack.utils.a11y.*sits alongsideslack.api.*,slack.nav.*,slack.workflows.*. Test authors discover it via the same IDE autocomplete. - Test-author control over invocation timing. Test
authors know where a page is ready; the fixture's
.run()call is an explicit marker that can be inspected in the test report. - Custom
test.steplabels ("Running accessibility checks in runAxeAndSaveViolations") make every audit call visible in the Playwright HTML report.
Placement discipline¶
One Axe call per new view/page/flow transition in a test — typically after:
- A button click that reveals new UI.
- A navigation to a new page.
- A sign-in as a second user.
- A redirect.
The check should run after the page or view has fully loaded
and all content has rendered — which is exactly what Locator
auto-wait cannot guarantee but what an explicit marker after
waitForURL / waitForSelector / custom readiness checks can.
Avoiding duplication¶
Same-view-twice audits produce:
- Duplicate error messages in reports.
- Duplicate screenshot artifacts.
- Test-suite slowdown.
Slack built an audit environment flag + script that takes a screenshot of every page where Axe fires, saves to a folder, and supports manual duplication comparison. Self-flagged as manual and a candidate for AI-assisted future work.
Generalisation¶
The pattern generalises to any whole-X cross-cutting concern a team wants to layer onto an existing E2E suite without forcing per-test refactors:
- Visual regression (
slack.utils.visual.snapshot()). - Performance snapshots (
slack.utils.perf.capture()). - Memory leak audits (
slack.utils.leaks.check()). - Security header audits (
slack.utils.sec.headers()).
Common shape: one helper class hanging off the custom fixture, explicitly invoked at page-ready moments, with test-author control and test-reporter visibility.
Seen in¶
- sources/2025-01-07-slack-automated-accessibility-testing-at-slack
— Slack's production integration:
slack.utils.a11y.runAxeAndSaveViolations()on the pre-existingslackfixture, invoked explicitly by test authors after each new view loads.
Related¶
- systems/playwright — the fixture model substrate.
- systems/axe-core-playwright — the Axe binding wrapped by the fixture helper.
- concepts/playwright-locator-auto-wait — the abstraction-boundary that rules out embedding Axe into Locator methods.
- patterns/exclusion-list-for-known-issues-and-out-of-scope-rules
— applied inside the fixture helper before
.analyze(). - patterns/severity-gated-violation-reporting — applied inside the fixture helper's result-filtering step.