9. Audit Steps
Permalink to 9. Audit StepsThis section is normative for processors that support audit.
The audit keyword delegates to a separate accessibility engine. The reference
implementation requires @afixt/afixt-engine. Other engines MAY be supported by
other processors.
9.1 Syntax
Permalink to 9.1 Syntax- audit: page # full-page audit, level AA
- audit: page level "AAA" # full-page, level AAA
- audit: region "Search Results" # element-scoped, level AA
- audit: navigation "Main Menu" level "A" # element-scoped, custom level
- audit: dialog "Confirm Delete" # element-scoped, level AA
If the first token is page, the audit is full-page. Otherwise, the first token
MUST be a role token and MUST be followed by an accessible name; the audit is
scoped to that element.
9.2 Modifier
Permalink to 9.2 ModifierThe only recognized modifier is level, which takes a quoted WCAG level
identifier: "A", "AA" (default), or "AAA". The set is closed: any other
value MUST be a parse error naming the modifier, the offending token, the
accepted set and the step, as §9A.1 requires of contrast.
Matching is case-insensitive and the value is canonicalised to upper case.
An unvalidated level is not inert. It reaches the engine as written, so the report names a conformance level that does not exist while the check runs at whatever threshold the engine falls back to — a result that reads as evidence about the page and is evidence about nothing.
9.3 Execution
Permalink to 9.3 ExecutionFor element-scoped audits, the processor MUST first resolve and wait for the target element to be visible (using §6). This both confirms the element exists in the accessibility tree and guarantees the engine has something to scan.
The processor MUST then derive a CSS context selector from the located element by evaluating the following preference order in the page:
- If the element has an
id, return'#' + id. - Else if it has both
roleandaria-label, return'[role="<role>"][aria-label="<aria-label>"]'. - Else if it has
aria-labelonly, return<tagName>[aria-label="<aria-label>"]. - Else if it has
roleonly, return[role="<role>"]. - Else return the lowercased tag name.
Every value interpolated into that selector — the id, the role, the
aria-label — comes from the page and MUST be escaped before interpolation: the
id as a CSS identifier and the attribute values as CSS strings, as in
§6.1. An aria-label containing "] would otherwise end the
selector early and scope the audit to something else.
The processor MUST then hand the engine a snapshot of the live page: the
serialised DOM as it stands after every preceding step (page.content() in the
reference implementation), the context selector (omitted for full-page audits),
the WCAG level, the current viewport size, and the page's own URL as a base
URL when that URL's scheme is http or https. The engine audits that
snapshot. It MUST NOT re-fetch the page's URL in a browser of its own:
re-navigation loses the runner's session — cookies, storage state, consent
dismissals, and every piece of client-side state earlier steps established — and
on a gated site lands on the gate rather than on the page the runner was looking
at. This is what makes C.3's second audit see the search results, and what makes
§5.6.19's statement that audits run at the current viewport
true.
The base URL is what keeps the snapshot equivalent to the page. A serialised DOM
loaded into a fresh document has no location of its own, so every relative and
root-relative href and src in it resolves against that empty document and
fetches nothing: the page's stylesheets, fonts and scripts do not load, and the
engine renders user-agent defaults. Every criterion decided from computed style
— 1.4.10 Reflow, 1.4.11 Non-text Contrast, 1.4.3 Contrast, 2.5.8 Target Size,
2.4.11 Focus Not Obscured — would then be decided against a rendering no user
has seen, reporting failures that do not exist and missing ones that do. A
document whose assets are all referenced by absolute URL is unaffected, so a
processor that omits the base URL can appear to work. Schemes other than http
and https MUST NOT be passed: they give the engine nothing it can fetch, and a
file: base would aim a fetching loader at the local filesystem (§16). A
processor MUST NOT override a <base> element the snapshot already carries.
A serialised snapshot is not the page itself. Content the serialisation cannot
carry — the contents of a shadow root, of a nested browsing context
(<iframe>), or of a <canvas> — is not audited. A report MUST NOT describe an
audit as having examined content the snapshot did not contain, and a processor
SHOULD document the limit.
The derived context selector can be broader than the located element. Step 4
yields [role="region"] for an unlabelled region and step 5 a bare tag name,
either of which may match several elements, and the engine then audits all of
them. An author who needs the audit scoped to exactly one element SHOULD give it
an id or an aria-label, which steps 1–3 resolve uniquely.
9.4 Issue mapping
Permalink to 9.4 Issue mappingThe engine returns a list of tests, each containing zero or more issues with
priority High, Medium, or Low. Issues MUST be aggregated into an
AuditResult:
interface AuditResult {
total_issues: number;
high: number; // count of High-priority issues
medium: number; // count of Medium-priority issues
low: number; // count of Low-priority issues
issues: AuditIssue[];
}
9.5 Audit step status
Permalink to 9.5 Audit step statusThe status of an audit step is determined by:
| Issue counts | Step status | Notes |
|---|---|---|
high == 0 && medium == 0 && low == 0 |
pass |
No accessibility_notes set. |
high == 0 && medium == 0 && low > 0 |
pass |
accessibility_notes set; raises overall score to pass_with_conditions. |
high > 0 || medium > 0 |
fail |
accessibility_notes set; overall score is fail. |
This rule is normative. The contribution to the overall use case score is defined in §11.
9.6 Engine absence
Permalink to 9.6 Engine absenceIf the engine module cannot be loaded, the processor MUST fail the audit step
with a descriptive error, MUST set failure_reason: "dependency_unavailable" on
the step result (§10.1), and SHOULD include installation
guidance in the error message. Other steps in the same use case MUST proceed
according to continue_on_failure.
The engine loading but failing to load the snapshot is a different failure and
MUST be reported as failure_reason: "snapshot_load_failed". It is reachable
because of the base URL of §9.3: a snapshot that resolves its
own stylesheets, fonts and scripts is a snapshot that fetches them, so a slow or
unreachable asset host fails an audit that has nothing to do with the page's
accessibility. The audit examined nothing, so the processor MUST NOT report it
as a finding, and the error SHOULD say that nothing was audited rather than
repeating the engine's own wording about a timeout. This applies to the direct
execution mode, where the processor hands over a snapshot; a generated test's
engine navigates to the URL itself (§8.3) and a failure there is an ordinary
navigation failure.
How long the engine may spend loading is governed by the run's configured
timeout, the same value the processor applies to steps it performs itself. A
processor that leaves the engine on its own default instead makes the timeout
unconfigurable exactly where it binds: the audit resolves the page's
subresources, so it is the step most likely to need longer than the default on a
slow origin.
The value binds in both directions. A project that raises timeout gives the
snapshot longer to load; a project that lowers it — a short per-step timeout is
a reasonable choice for the interaction verbs — bounds the load to that shorter
value, and an audit that would have completed within the engine's own default
can now fail as snapshot_load_failed. A processor SHOULD document this rather
than leave the narrowing implicit. Where a project configures nothing, the
processor's own default applies in direct execution and the engine's applies to
a generated test; the two are the same duration in this specification's
reference implementation, but nothing requires that of an implementation
(§8.3 governs the modes' behaviour, not their defaults).
timeout is a non-negative integer (§12.1),
and 0 is a configured value, not an absent one. When a project configures 0,
a processor MUST hand the engine 0 in both execution modes. Carrying it in
either mode as though nothing were configured would give the two modes different
limits from one configuration. What 0 means to the engine is the engine's to
define. The reference implementation's engine reads it as its own default —
neither "no limit" nor "give up at once" — while the steps a processor performs
itself read it as no limit, which is Playwright's meaning. A processor SHOULD
document that difference.