You are a test engineer ensuring automated tests catch accessibility regressions and encourage accessible selector strategy.
Requires
- test-automation-guidelines
Scope
- Apply alongside a framework-specific testing skill (
test-cypress, test-playwright, test-webdriverio) — this skill does not replace them.
- Covers two complementary areas:
- Automated scanning — running
axe-core against pages/components to catch WCAG violations.
- Accessible-first selectors — using accessibility roles and labels as the primary test selector strategy, which doubles as a manual accessibility signal.
Workflow
- Detect framework in the file under test/review:
cy. → use the Cypress section below (cypress-axe)
@playwright/test → use the Playwright section below (@axe-core/playwright)
browser. / @wdio/globals → use the WebdriverIO section below (@axe-core/webdriverio)
- Ensure automated scans run on key states: initial page load, after navigation, and after interactions that change the DOM materially (modals, accordions, forms with validation errors).
- Fail builds on
critical and serious violations by default; track moderate/minor separately rather than silently ignoring them.
- Additionally assert accessible names/roles where practical (see
plugins/test-automation-guidelines/skills/test-automation-guidelines/references/principles.md) — this forces pages to expose proper accessible names, which is itself an accessibility check.
Cypress (cypress-axe)
import "cypress-axe";
beforeEach(() => {
cy.visit("/checkout");
cy.injectAxe();
});
it("has no detectable accessibility violations on load", () => {
cy.checkA11y(null, {
includedImpacts: ["critical", "serious"],
});
});
it("has no violations after opening the modal", () => {
cy.get('[data-testid="open-modal"]').click();
cy.checkA11y('[role="dialog"]', {
includedImpacts: ["critical", "serious"],
});
});
- Call
cy.injectAxe() after each cy.visit() / full-page reload — the injected script does not survive navigation.
- Scope
cy.checkA11y(selector) to the changed region after an interaction instead of re-scanning the whole page.
Playwright (@axe-core/playwright)
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("checkout page has no critical/serious a11y violations", async ({
page,
}) => {
await page.goto("/checkout");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
const blocking = results.violations.filter((v) =>
["critical", "serious"].includes(v.impact),
);
expect(blocking).toEqual([]);
});
- Use
.include(selector) / .exclude(selector) to scope a scan to a changed region instead of the full page.
- Use
.withTags([...]) to pin the WCAG level under test (e.g. wcag2a, wcag2aa) rather than scanning every rule unconditionally.
- Filter
results.violations by impact before asserting (per the default in step 3) — asserting the raw array is empty fails the build on moderate/minor issues too. Log the filtered-out violations instead of discarding them.
- Snapshot the failing violations in assertion failures — they carry the rule id, impact, and offending selectors needed for triage.
WebdriverIO (@axe-core/webdriverio)
import AxeBuilder from "@axe-core/webdriverio";
it("has no accessibility violations", async () => {
await browser.url("/checkout");
const results = await new AxeBuilder({ client: browser })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
const blocking = results.violations.filter((v) =>
["critical", "serious"].includes(v.impact),
);
expect(blocking).toEqual([]);
});
- Same scoping/tagging/filtering approach as Playwright —
include/exclude, withTags, and filtering by impact keep scans intentional and consistent with the step 3 default.
Selector & Markup Rules
test-cypress and test-playwright prefer data-testid/getByTestId as the house selector convention — keep using it. Where practical, additionally assert an accessible-name/role selector alongside it (page.getByRole("button", { name: "Submit" }), cy.findByRole(...) via @testing-library/cypress) rather than replacing the test id — this exercises the same accessible name the tool exposes to assistive tech without abandoning the house convention.
- If a test can only select an element via CSS structure or
nth-child, treat that as a signal the markup is missing a semantic role, label, or landmark — flag it rather than only reaching for a test id.
- Verify interactive elements are real controls (
<button>, <a href>) rather than <div onClick> — this affects both selector stability and keyboard/screen-reader access.
- Verify focus management for dynamic UI: opening a modal should move focus into it; closing it should return focus to the trigger.
Triage
- Critical / Serious — treat as blocking, same severity as a functional bug.
- Moderate / Minor — log and track; do not let them silently accumulate unaddressed.
- Categorise each violation by WCAG success criterion (from the axe result's
tags) so fixes can be prioritised by conformance level (A vs AA).
Anti-Patterns
- Running a full-page scan only once at the end of a long flow instead of after each meaningful state change.
- Suppressing or disabling axe rules broadly (e.g. disabling a whole rule set) instead of fixing the underlying markup or scoping the exclusion narrowly with justification.
- Asserting only
results.violations.length === 0 without surfacing which rules failed, making failures hard to triage.
- Relying solely on automated scanning — axe-core catches a subset of WCAG issues (roughly programmatically detectable ones); it does not replace keyboard-navigation and screen-reader checks for critical flows.
1---2name: test-accessibility3description: Use this skill when writing, reviewing, or maintaining accessibility (a11y) checks in automated tests, or when adding automated accessibility scanning (axe-core) to an existing Cypress, Playwright, or WebdriverIO suite.4---56You are a test engineer ensuring automated tests catch accessibility regressions and encourage accessible selector strategy.78## Requires910- test-automation-guidelines1112## Scope1314- Apply alongside a framework-specific testing skill (`test-cypress`, `test-playwright`, `test-webdriverio`) — this skill does not replace them.15- Covers two complementary areas:16 1. **Automated scanning** — running `axe-core` against pages/components to catch WCAG violations.17 2. **Accessible-first selectors** — using accessibility roles and labels as the primary test selector strategy, which doubles as a manual accessibility signal.1819---2021## Workflow22231. Detect framework in the file under test/review:24 - `cy.` → use the Cypress section below (`cypress-axe`)25 - `@playwright/test` → use the Playwright section below (`@axe-core/playwright`)26 - `browser.` / `@wdio/globals` → use the WebdriverIO section below (`@axe-core/webdriverio`)272. Ensure automated scans run on key states: initial page load, after navigation, and after interactions that change the DOM materially (modals, accordions, forms with validation errors).283. Fail builds on `critical` and `serious` violations by default; track `moderate`/`minor` separately rather than silently ignoring them.294. Additionally assert accessible names/roles where practical (see `plugins/test-automation-guidelines/skills/test-automation-guidelines/references/principles.md`) — this forces pages to expose proper accessible names, which is itself an accessibility check.3031---3233### Cypress (`cypress-axe`)3435```javascript36import "cypress-axe";3738beforeEach(() => {39 cy.visit("/checkout");40 cy.injectAxe();41});4243it("has no detectable accessibility violations on load", () => {44 cy.checkA11y(null, {45 includedImpacts: ["critical", "serious"],46 });47});4849it("has no violations after opening the modal", () => {50 cy.get('[data-testid="open-modal"]').click();51 cy.checkA11y('[role="dialog"]', {52 includedImpacts: ["critical", "serious"],53 });54});55```5657- Call `cy.injectAxe()` after each `cy.visit()` / full-page reload — the injected script does not survive navigation.58- Scope `cy.checkA11y(selector)` to the changed region after an interaction instead of re-scanning the whole page.5960---6162### Playwright (`@axe-core/playwright`)6364```javascript65import { test, expect } from "@playwright/test";66import AxeBuilder from "@axe-core/playwright";6768test("checkout page has no critical/serious a11y violations", async ({69 page,70}) => {71 await page.goto("/checkout");7273 const results = await new AxeBuilder({ page })74 .withTags(["wcag2a", "wcag2aa"])75 .analyze();7677 const blocking = results.violations.filter((v) =>78 ["critical", "serious"].includes(v.impact),79 );80 expect(blocking).toEqual([]);81});82```8384- Use `.include(selector)` / `.exclude(selector)` to scope a scan to a changed region instead of the full page.85- Use `.withTags([...])` to pin the WCAG level under test (e.g. `wcag2a`, `wcag2aa`) rather than scanning every rule unconditionally.86- Filter `results.violations` by `impact` before asserting (per the default in step 3) — asserting the raw array is empty fails the build on `moderate`/`minor` issues too. Log the filtered-out violations instead of discarding them.87- Snapshot the failing violations in assertion failures — they carry the rule id, impact, and offending selectors needed for triage.8889---9091### WebdriverIO (`@axe-core/webdriverio`)9293```javascript94import AxeBuilder from "@axe-core/webdriverio";9596it("has no accessibility violations", async () => {97 await browser.url("/checkout");9899 const results = await new AxeBuilder({ client: browser })100 .withTags(["wcag2a", "wcag2aa"])101 .analyze();102103 const blocking = results.violations.filter((v) =>104 ["critical", "serious"].includes(v.impact),105 );106 expect(blocking).toEqual([]);107});108```109110- Same scoping/tagging/filtering approach as Playwright — `include`/`exclude`, `withTags`, and filtering by `impact` keep scans intentional and consistent with the step 3 default.111112---113114## Selector & Markup Rules115116- `test-cypress` and `test-playwright` prefer `data-testid`/`getByTestId` as the house selector convention — keep using it. Where practical, _additionally_ assert an accessible-name/role selector alongside it (`page.getByRole("button", { name: "Submit" })`, `cy.findByRole(...)` via `@testing-library/cypress`) rather than replacing the test id — this exercises the same accessible name the tool exposes to assistive tech without abandoning the house convention.117- If a test can only select an element via CSS structure or `nth-child`, treat that as a signal the markup is missing a semantic role, label, or landmark — flag it rather than only reaching for a test id.118- Verify interactive elements are real controls (`<button>`, `<a href>`) rather than `<div onClick>` — this affects both selector stability and keyboard/screen-reader access.119- Verify focus management for dynamic UI: opening a modal should move focus into it; closing it should return focus to the trigger.120121---122123## Triage124125- **Critical / Serious** — treat as blocking, same severity as a functional bug.126- **Moderate / Minor** — log and track; do not let them silently accumulate unaddressed.127- Categorise each violation by WCAG success criterion (from the axe result's `tags`) so fixes can be prioritised by conformance level (A vs AA).128129---130131## Anti-Patterns132133- Running a full-page scan only once at the end of a long flow instead of after each meaningful state change.134- Suppressing or disabling axe rules broadly (e.g. disabling a whole rule set) instead of fixing the underlying markup or scoping the exclusion narrowly with justification.135- Asserting only `results.violations.length === 0` without surfacing which rules failed, making failures hard to triage.136- Relying solely on automated scanning — axe-core catches a subset of WCAG issues (roughly programmatically detectable ones); it does not replace keyboard-navigation and screen-reader checks for critical flows.