Accessibility checks the team can drop into any scenario. This group ships two layers of coverage. The first is a set of custom JS probes - fast, dependency-free assertions for POUR fundamentals (Perceivable, Operable, Understandable, Robust): alt text, label association, landmarks, focus, heading order, skip links, ARIA validity, tabindex hygiene, page title, and zoom. Each probe maps to a specific WCAG 2.1 / 2.2 success criterion and its failure message lists the offending elements, so QA gains compliance signal without a separate a11y tool.
The second layer is a full axe-core integration via the official Deque engine (@axe-core/playwright + axe-core): roughly 100 rules covering WCAG 2.0 / 2.1 / 2.2 levels A, AA, and AAA. Every violation carries an impact field (minor / moderate / serious / critical) used by the impact-gating step for triage. The audit steps default to WCAG AA - the level required by the EU Web Accessibility Directive, US Section 508, and UK PSBAR.
All steps run against the current page, so navigate first with the usual Given I am on "..." steps - BBR smart waits ensure the page has settled before any probe fires. Combine the focus assertions with the keyboard steps (When I press the "Tab" key) to test tab order end to end.
Quick reference
| Step | What it does |
|---|---|
Then every image should have an alt attribute | Every <img> has an alt attribute (empty allowed). |
Then every form field should have an accessible label | Every input/select/textarea has an accessible name. |
Then the page should have a main landmark | A <main> or [role="main"] landmark exists. |
Then the page should have a navigation landmark | At least one <nav> or [role="navigation"] exists. |
Then the page should have exactly one h1 | Exactly one <h1> on the page. |
Then the focused element should match "#email" | The focused element matches a CSS selector. |
Then the focused element should be labeled "Email" | The focused element's accessible name contains text. |
Then the page should declare a language | <html> has a non-empty lang attribute (WCAG 3.1.1). |
Then the page language should be "en" | lang equals an exact value. |
Then the heading hierarchy should be valid | Headings never skip a level (WCAG 1.3.1 / 2.4.6). |
Then the page should have a skip link | A working skip-to-content link exists (WCAG 2.4.1). |
Then every button should have an accessible name | All buttons expose a name (WCAG 4.1.2). |
Then every link should have an accessible name | All links expose a name (WCAG 2.4.4 / 4.1.2). |
Then no element should have a positive tabindex | No positive tabindex values (WCAG 2.4.3). |
Then every ARIA reference should resolve | All ARIA id references point at real elements. |
Then every ARIA role should be valid | No invalid ARIA roles (WAI-ARIA 1.2). |
Then required fields should be consistently marked | required and aria-required always agree. |
Then the page should have a title | Non-empty <title> (WCAG 2.4.2). |
Then user zoom should be allowed | Viewport meta does not block user scaling (WCAG 1.4.4). |
Then the page should pass an accessibility audit | Full axe audit, zero violations (default level AA). |
Then the page should have no critical accessibility violations | Zero axe violations of a given impact severity. |
Then the element "main" should pass an accessibility audit | Audit only one subtree. |
Then the page should pass an accessibility audit excluding "iframe.payment" | Audit the page minus one subtree. |
Then the page should not violate the accessibility rule "color-contrast" | A single named axe rule does not fire. |
Then I print accessibility violations | Dump every axe violation to stdout (debug). |
Then the page should pass the accessibility rules "image-alt, label" | Zero violations of an exact rule list. |
Steps in detail
Then every image should have an alt attribute
Iterates every <img> and fails if any lacks an alt attribute. Empty alt="" is allowed (decorative images per WCAG) and role="presentation" is exempt - only a genuinely missing attribute fails. The error lists the markup of each offender.
Given I am on "/a11y.html"
Then every image should have an alt attribute
Then every form field should have an accessible label
Every input, select, and textarea passes when at least one of these is true: an aria-label, an aria-labelledby, a matching <label for="...">, or a wrapping <label>. Submit/reset/button/image inputs and type="hidden" are exempt - they get their name from value or alt.
Given I am on "/a11y.html"
Then every form field should have an accessible label
Then the page should have a main landmark / a navigation landmark
The main-landmark step asserts a primary <main> (or [role="main"]) exists - missing it forces screen-reader users to navigate linearly (WCAG 2.4.1). The navigation variant asserts at least one <nav> or [role="navigation"].
Then the page should have a main landmark
And the page should have a navigation landmark
Then the page should have exactly one h1
Multiple h1s are valid HTML5, but most assistive tech still treats the outline as flat - one <h1> per page is the safe default. Fails on any count other than 1.
When I follow "About"
Then the page should have exactly one h1
Then the focused element should match "#email"
Asserts document.activeElement matches the given CSS selector. The workhorse for tab-order and skip-link tests when paired with the keyboard steps.
When I press the "Tab" key
Then the focused element should match "#email"
When I press the "Tab" key 2 times
Then the focused element should match "#password"
Then the focused element should be labeled "Email"
Asserts the focused element's accessible name contains the given text. Resolution order: aria-label → aria-labelledby target → <label for="..."> → wrapping <label> → value / placeholder / title / textContent.
When I focus on the element "#email"
Then the focused element should be labeled "Email"
Then the page should declare a language / the page language should be "en"
The first asserts <html> has a non-empty lang attribute - WCAG 3.1.1; screen readers use it to pick the pronunciation engine. The second asserts lang equals an exact value, handy on multilingual sites.
Then the page should declare a language
And the page language should be "en"
Then the heading hierarchy should be valid
Walks h1-h6 in document order and fails if any heading skips a level (h1 to h3 with no h2 in between), naming the offending heading and the level it skipped from. WCAG 1.3.1 / 2.4.6.
Given I am on "/a11y.html"
Then the heading hierarchy should be valid
Then the page should have a skip link
Asserts the page exposes a skip link: an anchor with href="#..." that resolves to an existing target and whose text matches "skip", "main content", or "to content". WCAG 2.4.1 requires a mechanism to bypass repeated blocks.
Then the page should have a skip link
And the page should have a main landmark
Then every button should have an accessible name / every link should have an accessible name
The button step covers <button>, role="button", and submit/button inputs; a name can come from visible text, value, aria-label, or aria-labelledby. The link step covers every <a href> and also accepts an image link whose <img alt> has text. WCAG 4.1.2.
Then every button should have an accessible name
And every link should have an accessible name
Then no element should have a positive tabindex
Asserts no element uses tabindex greater than 0. Positive values break natural focus order - a WCAG 2.4.3 violation in practice. tabindex="0" and tabindex="-1" are fine.
When I am on "/form"
Then no element should have a positive tabindex
Then every ARIA reference should resolve / every ARIA role should be valid
The reference step checks every aria-labelledby, aria-describedby, aria-controls, aria-owns, and aria-flowto - each space-separated id must exist in the document. The role step validates every role attribute against the WAI-ARIA 1.2 role list.
Then every ARIA reference should resolve
And every ARIA role should be valid
Then required fields should be consistently marked
Asserts every field with aria-required="true" also has the native required attribute, and vice versa. A mismatch means sighted users and assistive-tech users get different signals about which fields are mandatory.
Given I am on "/signup"
Then required fields should be consistently marked
Then the page should have a title / Then user zoom should be allowed
The title step asserts a non-empty <title> (WCAG 2.4.2). The zoom step fails when the viewport meta tag contains user-scalable=no or maximum-scale=1, both of which prevent pinch-zoom (WCAG 1.4.4 / 1.4.10). A page with no viewport meta passes.
Then the page should have a title
And user zoom should be allowed
Then the page should pass an accessibility audit
Runs a full axe-core audit and asserts zero violations. Defaults to WCAG AA; append at level "A", "AA", or "AAA" to change the rule set. Failure output summarises up to five violations with impact, rule id, help text, and node count. Requires @axe-core/playwright - the step prints a friendly install hint if the package is missing.
Then the page should pass an accessibility audit
And the page should pass an accessibility audit at level "AAA"
Then the page should have no critical accessibility violations
Runs the audit but only fails on violations of the named impact: critical, serious, moderate, or minor. Use it to gate merges on critical/serious issues while triaging minor regressions on legacy pages.
Then the page should have no critical accessibility violations
And the page should have no serious accessibility violations
Then the element "main" should pass an accessibility audit
Audits only the subtree under a CSS selector at level AA. Use when third-party widgets are known-bad and you only want to gate your own components.
Then the element "#checkout-form" should pass an accessibility audit
Then the page should pass an accessibility audit excluding "iframe.payment"
The inverse: audits the whole page at level AA but excludes the subtree under a CSS selector - skip known-bad embedded content such as third-party iframes, legacy widgets, or ads.
Then the page should pass an accessibility audit excluding "#chat-widget"
Then the page should not violate the accessibility rule "color-contrast"
Asserts one specific axe rule does not fire - a fine-grained gate to lock in a fix (say, color contrast) even while other rules are still red. Common rule ids: color-contrast, image-alt, label, link-name, button-name, heading-order, landmark-one-main, region, aria-valid-attr, tabindex, bypass, frame-title, meta-viewport, valid-lang.
Then the page should not violate the accessibility rule "image-alt"
And the page should not violate the accessibility rule "label"
Then the page should pass the accessibility rules "image-alt, label"
Asserts axe finds zero violations of an exact comma-separated list of rules. Ideal for smoke-test gating: pin the small list of rules you absolutely require and run them on every page.
Then the page should pass the accessibility rules "image-alt, label, button-name"
Then I print accessibility violations
Prints every axe violation to stdout with impact, rule id, help URL, target selectors, and a failure summary per node (up to three nodes per rule). A debug aid while fixing a page - strip it from CI runs.
When I am on "/checkout"
Then I print accessibility violations
And the page should have no critical accessibility violations
Complete example
From the shipped suite (tests/features/a11y.feature), run against the examples/a11y.html fixture:
Feature: Accessibility (a11y) smoke checks
Scenario: Page-level landmarks and language
Given I am on "/a11y.html"
Then the page should declare a language
And the page language should be "en"
And the page should have a main landmark
And the page should have a navigation landmark
And the page should have exactly one h1
Scenario: axe-core full WCAG audit
Given I am on "/a11y.html"
Then the page should have no critical accessibility violations
And the page should not violate the accessibility rule "image-alt"
And the page should pass the accessibility rules "image-alt, label, button-name"
Configuration
The custom probes have no dependencies. The five audit steps require the axe packages:
npm i @axe-core/playwright axe-core
Audit levels map to axe tags as follows:
| Level | axe tags |
|---|---|
A | wcag2a, wcag21a |
AA (default) | wcag2a, wcag2aa, wcag21a, wcag21aa, wcag22aa |
AAA | wcag2a, wcag2aa, wcag2aaa, wcag21a, wcag21aa, wcag21aaa, wcag22aaa |
Tips
- Public site under WCAG 2.1 AA obligations: gate every page on the AA audit plus
the page should have a title,a main landmark,the heading hierarchy should be valid, anduser zoom should be allowed. - Admin UI with known-bad third-party widgets: gate on the impact steps (
no critical/no serious) or use theexcluding "..."audit variant. - Tag a11y scenarios
@a11yand run them in isolation:npx cucumber-js --tags "@a11y".