ultra11y 2.22.0 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -85
- package/package.json +6 -2
- package/scripts/build-pack-rgaa.mjs +95 -1
- package/scripts/build-standards.mjs +5 -1
- package/scripts/ultra11y.mjs +2972 -1694
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +2972 -1694
- package/skills/ultra11y/SKILL.md +38 -2
- package/skills/ultra11y/references/ci.md +165 -0
- package/skills/ultra11y/references/criteria.md +5 -5
- package/skills/ultra11y/references/devtools.md +67 -0
- package/skills/ultra11y/references/e2e.md +112 -0
- package/skills/ultra11y/references/judgment.md +25 -0
- package/skills/ultra11y/references/pages.md +205 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +2972 -1694
- package/src/adjudicate.ts +270 -45
- package/src/annotate.ts +158 -0
- package/src/audit.ts +8 -1
- package/src/capture.ts +4 -0
- package/src/cli.ts +469 -92
- package/src/criteria.ts +58 -0
- package/src/data/standards/rgaa.json +155 -30
- package/src/data/wcag.json +5 -5
- package/src/dev.ts +427 -0
- package/src/e2e.ts +303 -0
- package/src/gh.ts +72 -0
- package/src/init.ts +39 -7
- package/src/messages.ts +87 -0
- package/src/orchestrate-templates.ts +12 -4
- package/src/pages.ts +283 -0
- package/src/parse/html.ts +12 -0
- package/src/pixel.ts +217 -0
- package/src/prd.ts +37 -0
- package/src/report.ts +28 -2
- package/src/rules/registry.ts +2 -0
- package/src/rules/rendered.ts +379 -0
- package/src/rules/rule.ts +3 -0
- package/src/sarif.ts +196 -0
- package/src/scan-local.ts +4 -0
- package/src/snapshot.ts +442 -0
- package/src/standards/derive.ts +32 -1
- package/src/types.ts +161 -1
- package/src/util.ts +18 -0
- package/src/mcp/handlers.ts +0 -388
- package/src/mcp/http.ts +0 -260
- package/src/mcp/prompts.ts +0 -183
- package/src/mcp/protocol.ts +0 -187
- package/src/mcp/resources.ts +0 -136
- package/src/mcp/server.ts +0 -228
- package/src/mcp/stdio.ts +0 -119
- package/src/mcp/tools.ts +0 -347
- package/src/project-lock.ts +0 -42
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ultra11y
|
|
3
|
-
description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibility and produce a dated auditor-conformance report, OR to AUTHOR/REVIEW accessible markup (native-HTML-first, ARIA last). An install-free engine (`node scripts/ultra11y.mjs`, no keys) runs
|
|
3
|
+
description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibility and produce a dated auditor-conformance report, OR to AUTHOR/REVIEW accessible markup (native-HTML-first, ARIA last). An install-free engine (`node scripts/ultra11y.mjs`, no keys) runs 78 static checks tied to WCAG criteria — alt/lang/title, unlabeled fields, empty links/buttons, tables, headings, ARIA vocabulary, label-in-name, autocomplete — measured against the official W3C ACT corpus. The engine decides 3 of the 55 criteria; the AI agent adjudicates the 38 judgment ones from harvested evidence + a per-criterion decision protocol (`verify --manual`, gated, fannable via `orchestrate`), and the 14 rendering ones go to `scan` — never silently conforming. Library/SFC code is audited as RENDERED captures (`render --setup`); country standards are pluggable packs (`--standard rgaa`, `scan --sample`). check/verify reject invented non-conformities. Triggers: 'audit WCAG/a11y', 'make accessible', 'fix a11y', 'audit RGAA'."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: 2.
|
|
6
|
+
version: 2.23.0
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -83,6 +83,10 @@ Domain knowledge first, then the tooling. Read the one that matches the question
|
|
|
83
83
|
| `references/verify.md` | The `check`/`verify` gates |
|
|
84
84
|
| `references/false-positives.md` | Where a finding may still be wrong, and how to refute it |
|
|
85
85
|
| `references/rendered.md` · `references/automation.md` | Auditing produced HTML, captures, hooks and CI |
|
|
86
|
+
| `references/ci.md` | `--format sarif\|github`: inline PR annotations, code scanning, job summary |
|
|
87
|
+
| `references/pages.md` | Page snapshots + the per-page criterion grid (`pages`), and its honesty rules |
|
|
88
|
+
| `references/e2e.md` | `render --e2e`: auditing a page during your Playwright/Cypress run |
|
|
89
|
+
| `references/devtools.md` | `dev`: the live overlay while you build, and the per-page dashboard |
|
|
86
90
|
| `references/dynamic.md` | The `scan` tier: runtimes, probes, authenticated pages |
|
|
87
91
|
| `references/scale.md` | Focusing an audit on a large repository |
|
|
88
92
|
| `references/fix.md` · `references/correction.md` | Applying fixes, by priority, without regressions |
|
|
@@ -153,6 +157,38 @@ Domain knowledge first, then the tooling. Read the one that matches the question
|
|
|
153
157
|
"block only NEW non-conformities" variant. For library/SFC code, commit rendered
|
|
154
158
|
**captures** (`render --setup`) and stage them so the real semantic HTML is what's
|
|
155
159
|
checked (`audit --require-captures`); read **`references/automation.md`**.
|
|
160
|
+
- **"Show me the problems while I build the page"** → `dev --next` writes a one-line overlay
|
|
161
|
+
component and `dev` starts a loopback side-car: a floating panel lists the current page's
|
|
162
|
+
non-conformities, each opening its `file:line` in your editor, and `http://127.0.0.1:4111`
|
|
163
|
+
accumulates the per-page grid as you browse. Renders nothing outside development; read
|
|
164
|
+
**`references/devtools.md`**.
|
|
165
|
+
- **"Check a page during our E2E tests"** → `render --e2e` writes Playwright/Cypress fixtures
|
|
166
|
+
that audit the page **as your test left it** (logged in, form filled, modal open) — state a
|
|
167
|
+
separate `scan` run does not have — and persist each checked page as a snapshot. `failOn`
|
|
168
|
+
gates the test; `failOn: false` records without failing, which is how you adopt it on an
|
|
169
|
+
existing backlog; read **`references/e2e.md`**.
|
|
170
|
+
- **"Audit the real PAGE, not the component"** → a **page snapshot**
|
|
171
|
+
(`.ultra11y/pages/<id>/`) is the whole rendered document plus the browser-only signals
|
|
172
|
+
(computed styles, boxes, a11y tree, screenshot), ingested by `audit` automatically. Because
|
|
173
|
+
it is a FULL document, the page-scoped rules run on it — that is where RGAA 8.3 (lang),
|
|
174
|
+
8.5/8.6 (title) and 12.6 become decidable at all; read **`references/pages.md`**.
|
|
175
|
+
- **"Decide the rendering criteria without a browser in CI"** → once a page snapshot exists,
|
|
176
|
+
the **rendered tier** runs inside the ordinary `audit`: computed-style contrast
|
|
177
|
+
(RGAA 3.2/10.5), screenshot-measured contrast for text over a gradient/image (the CSSOM
|
|
178
|
+
blind spot axe-core shares), and colour-only link identification (**RGAA 10.6**, previously
|
|
179
|
+
undecidable). Each leaves the criterion `manual` when it cannot measure — never a guess;
|
|
180
|
+
read **`references/pages.md`**.
|
|
181
|
+
- **"Give me the RGAA grid, page by page"** → `pages --in audits/audit-latest.json --standard
|
|
182
|
+
rgaa` (also embedded in `report`): one row per criterion, one column per page, rebuilt from
|
|
183
|
+
the committed audit.json alone. Two rules hold: a finding is attributed to a page only when
|
|
184
|
+
something SAYS so (else it is reported as unattributed, never spread), and `C` by silence is
|
|
185
|
+
earned only by a page whose real DOM was audited — a source-only page keeps its undecided
|
|
186
|
+
criteria « à évaluer »; read **`references/pages.md`**.
|
|
187
|
+
- **"Show the findings ON the pull request, not just a red job"** → `--format sarif` (upload
|
|
188
|
+
to code scanning → inline annotations at the right file:line) or `--format github`
|
|
189
|
+
(`::error::` workflow commands + a `$GITHUB_STEP_SUMMARY` table) — from `report
|
|
190
|
+
--standard rgaa` when you want the pack's criteria rather than WCAG's; read
|
|
191
|
+
**`references/ci.md`**.
|
|
156
192
|
- **"Make this code accessible / review it"** → audit the snippet
|
|
157
193
|
(`audit - < component.html`) native-first; read **`references/authoring.md`** and
|
|
158
194
|
**`references/forbidden-patterns.md`**.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# CI — the shipped action, SARIF, annotations, job summary
|
|
2
|
+
|
|
3
|
+
## The action
|
|
4
|
+
|
|
5
|
+
The whole surface in one step. The engine ships **inside** the action (one bundled `.mjs`,
|
|
6
|
+
zero dependencies), so there is nothing to install, no `setup-node`, and no version that can
|
|
7
|
+
drift from the action's own.
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
security-events: write # SARIF upload; without it the annotations still report
|
|
13
|
+
pull-requests: write # only if you set `comment: 'true'`
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
with: { fetch-depth: 0 } # the diff gate needs the base ref
|
|
18
|
+
- uses: maxgfr/ultra11y@v2 # or pin the exact version, as `init --ci` does
|
|
19
|
+
with:
|
|
20
|
+
since: auto # the PR's base branch
|
|
21
|
+
standard: rgaa
|
|
22
|
+
fail-on: blocking
|
|
23
|
+
baseline: audits/baseline.json
|
|
24
|
+
comment: 'true'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Page by page, with a real browser, in the same step:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
- uses: maxgfr/ultra11y@v2
|
|
31
|
+
with:
|
|
32
|
+
standard: rgaa
|
|
33
|
+
start: npm run start
|
|
34
|
+
wait-on: http://localhost:3000
|
|
35
|
+
urls: http://localhost:3000 http://localhost:3000/contact
|
|
36
|
+
# …or `sample: 'true'` to scan the sample declared in .ultra11yrc.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`ultra11y init --ci` writes a workflow using it, **pinned to the exact engine version that
|
|
40
|
+
generated the file** (`@v<that version>`) so a CI run stays reproducible. `@v2` is a moving major alias
|
|
41
|
+
the release workflow keeps pointing at the latest release, for teams who would rather take
|
|
42
|
+
fixes automatically. Never `@main`: it would change under you without a version to blame.
|
|
43
|
+
|
|
44
|
+
**Order matters, and it is deliberate**: the audit runs first, then SARIF, annotations, the
|
|
45
|
+
summary, the comment and the report — and the **gate runs last**. A failing audit has
|
|
46
|
+
therefore already produced every surface, so a red job is never a dead end. `fail-on: ''`
|
|
47
|
+
turns the gate off entirely (report-only), which is how you adopt this on an existing backlog;
|
|
48
|
+
`baseline` is the other way — only NEW non-conformities can fail.
|
|
49
|
+
|
|
50
|
+
The SARIF upload is `continue-on-error`: a repository without Advanced Security keeps its
|
|
51
|
+
annotations instead of failing. Each upload carries `category: ultra11y-<standard>`, so a WCAG
|
|
52
|
+
run and an RGAA run coexist rather than overwriting each other. The Markdown report goes
|
|
53
|
+
through `check` before being uploaded, so CI cannot publish a report citing an invented
|
|
54
|
+
criterion.
|
|
55
|
+
|
|
56
|
+
# The `--format` renderings
|
|
57
|
+
|
|
58
|
+
A gate tells a developer *that* the build failed; it does not put the non-conformity **on the
|
|
59
|
+
line of code that caused it**. These two renderings do — they are what the action posts, and
|
|
60
|
+
you can run them yourself outside it.
|
|
61
|
+
|
|
62
|
+
Nothing new is measured here. Both are projections of the same `AuditResult`.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
node scripts/ultra11y.mjs audit "src/**/*.tsx" --jsx --format sarif > a11y.sarif
|
|
66
|
+
node scripts/ultra11y.mjs audit "src/**/*.tsx" --jsx --format github
|
|
67
|
+
node scripts/ultra11y.mjs report --in audits/audit-latest.json --standard rgaa --format sarif > rgaa.sarif
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## `--format sarif` — inline PR annotations via code scanning
|
|
71
|
+
|
|
72
|
+
SARIF 2.1.0, the format GitHub code scanning ingests. Upload it and every finding becomes an
|
|
73
|
+
annotation on the right file and line of the pull request, plus an entry in the Security tab.
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
- run: node scripts/ultra11y.mjs audit "src/**/*.tsx" --jsx --format sarif > a11y.sarif
|
|
77
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
78
|
+
with: { sarif_file: a11y.sarif }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
What the projection guarantees:
|
|
82
|
+
|
|
83
|
+
- **Levels.** `bloquant → error`, `majeur → warning`, `mineur → note`. An **advisory**
|
|
84
|
+
(non-normative recommendation) is **never** an `error`, whatever its severity rank — it is
|
|
85
|
+
always a `note`, so a recommendation cannot fail a reviewer's build on principle.
|
|
86
|
+
- **Stable alert identity.** `partialFingerprints` reuses `findingId()` — the *same* identity
|
|
87
|
+
the baseline gate uses — so an alert survives line drift instead of being closed and
|
|
88
|
+
reopened on every edit above it.
|
|
89
|
+
- **Rules table.** One entry per `ruleId`, tagged `accessibility`, `wcag:<sc>` and, under
|
|
90
|
+
`--standard`, `<pack>:<criterion>` (e.g. `rgaa:1.1`), with the WCAG *Understanding* page as
|
|
91
|
+
`helpUri`.
|
|
92
|
+
- **Repo-relative URIs.** An absolute path is relativised against the working directory;
|
|
93
|
+
GitHub matches alerts against the checkout and would silently drop an absolute one.
|
|
94
|
+
- **No invented locations.** A finding keyed to a **URL** (a `scan` result the host-anchor
|
|
95
|
+
resolver could not map back to source) is emitted with **no** physical location and its URL
|
|
96
|
+
in `properties.url`. Pinning it to a guessed `file:line` is exactly the class of error the
|
|
97
|
+
engine exists to refuse.
|
|
98
|
+
|
|
99
|
+
## `--format github` — annotations without code scanning
|
|
100
|
+
|
|
101
|
+
Advanced Security is not available on every plan. This mode writes
|
|
102
|
+
`::error file=…,line=…,col=…,title=…::` workflow commands to **stdout**, which every GitHub
|
|
103
|
+
plan renders inline on the diff, and appends a Markdown job summary to
|
|
104
|
+
`$GITHUB_STEP_SUMMARY` (severity table, and the per-page synthesis once a page sample has
|
|
105
|
+
been scanned and merged). With no `$GITHUB_STEP_SUMMARY` set, the summary goes to stderr so a
|
|
106
|
+
local run still shows it.
|
|
107
|
+
|
|
108
|
+
URL-keyed findings are **skipped** for annotations — there is no repo line to point at — and
|
|
109
|
+
counted in the summary instead, so they are never silently dropped.
|
|
110
|
+
|
|
111
|
+
## Which command to run it from
|
|
112
|
+
|
|
113
|
+
`audit` is always WCAG-keyed: it takes **no** `--standard`, by design (a pack is a derived
|
|
114
|
+
view, never a second source of truth). So:
|
|
115
|
+
|
|
116
|
+
| You want | Run |
|
|
117
|
+
|---|---|
|
|
118
|
+
| WCAG-keyed CI output, one shot | `audit … --format sarif` |
|
|
119
|
+
| **RGAA**-keyed CI output | `audit … --out audits` then `report --in audits/audit-latest.json --standard rgaa --format sarif` |
|
|
120
|
+
| Only what the PR introduced | `audit --since origin/main --baseline audits/baseline.json --format sarif` |
|
|
121
|
+
|
|
122
|
+
In `--baseline` gate mode the CI rendering covers **exactly the new findings** — the subject
|
|
123
|
+
of the gate is the regression, not the backlog, so a PR is annotated with what it introduced
|
|
124
|
+
and the existing backlog stays out of the diff.
|
|
125
|
+
|
|
126
|
+
Uploading two SARIF files in one workflow (a source audit and a page scan) is supported:
|
|
127
|
+
each run carries a distinct `automationDetails.id` (`ultra11y/<standard>/`), so GitHub keeps
|
|
128
|
+
them as separate analyses instead of one overwriting the other.
|
|
129
|
+
|
|
130
|
+
## The sticky PR comment
|
|
131
|
+
|
|
132
|
+
`ULTRA11Y_PR_COMMENT=1` (what the action's `comment: 'true'` sets) posts the job summary as a
|
|
133
|
+
pull-request comment and **edits it in place** on every subsequent run — a CI job that appends
|
|
134
|
+
a fresh comment on each push turns a busy PR into a wall of stale audits. The comment is keyed
|
|
135
|
+
by an invisible marker that includes the standard, so a WCAG run and an RGAA run keep separate
|
|
136
|
+
comments instead of overwriting each other, and a human's comment is never adopted (an edit is
|
|
137
|
+
destructive, so the match has to be exact).
|
|
138
|
+
|
|
139
|
+
It is best-effort, like issue creation: off a pull request, or with no `gh`/no auth, it
|
|
140
|
+
reports `skipped` and the run carries on. A comment is never worth failing a build over. If
|
|
141
|
+
listing the existing comments fails (permissions, rate limit) it creates a new one — a
|
|
142
|
+
duplicate comment is a far smaller harm than dropping the report.
|
|
143
|
+
|
|
144
|
+
## How this repo publishes itself (npm trusted publishing)
|
|
145
|
+
|
|
146
|
+
ultra11y's own release workflow holds **no npm token**. It publishes by OIDC: the job asks
|
|
147
|
+
GitHub for a short-lived id-token (`permissions: id-token: write`) and exchanges it with
|
|
148
|
+
registry.npmjs.org for a credential scoped to this package. Nothing long-lived is stored, so
|
|
149
|
+
there is no secret to leak or rotate.
|
|
150
|
+
|
|
151
|
+
Two things it depends on, both easy to break silently:
|
|
152
|
+
|
|
153
|
+
- **A trusted publisher** configured on npmjs.com for the package, naming this repository and
|
|
154
|
+
**this workflow file**. Rename `release.yml` and the exchange stops matching.
|
|
155
|
+
- **npm >= 11.5.1** on the runner. `@semantic-release/npm` only *pre-flights* the exchange;
|
|
156
|
+
the publish itself is `npm publish`, and the CLI does its own OIDC handshake. Node 24 still
|
|
157
|
+
ships an older 11.x, so the workflow upgrades the CLI explicitly.
|
|
158
|
+
|
|
159
|
+
The package must exist before a trusted publisher can be attached to it, so the **first**
|
|
160
|
+
publish is manual — `npm publish` from a logged-in shell, after `pnpm run build`. The name is
|
|
161
|
+
unscoped and `publishConfig.access` is set, so nothing else is needed. Every release after
|
|
162
|
+
that is tokenless.
|
|
163
|
+
|
|
164
|
+
If the exchange fails, `@semantic-release/npm` logs why and falls back to demanding a token —
|
|
165
|
+
it does not publish unauthenticated.
|
|
@@ -31,20 +31,20 @@ criteria that map to it. SC ids, titles and levels are derived from the W3C sour
|
|
|
31
31
|
| 1.3.1 | Info and Relationships | A | judgment | fieldset-legend-missing, data-table-no-headers, table-caption-missing, layout-table-data-markup, heading-order-skip, h1-missing, h1-multiple, list-structure, dl-structure, empty-heading, label-for-dangling, missing-main-landmark, multiple-main-landmark, sortable-header-no-aria-sort, nav-landmark-missing, nav-landmark-unnamed, aria-required-parent, headers-attr-dangling, th-no-data-cells, radio-checkbox-group-ungrouped, table-empty-data-cell, css-generated-content-informative | 3.1, 5.1, 5.2, 5.4, 5.5, 5.6, 5.7, 5.8, 7.3, 8.9, 9.1, 9.2, 9.3, 9.4, 10.1, 10.2, 11.1, 11.5, 11.6, 11.7, 11.8, 12.6, 13.3, 13.4 |
|
|
32
32
|
| 1.3.2 | Meaningful Sequence | A | judgment | — | 5.3, 8.10, 10.1, 10.3, 10.8, 13.3, 13.4 |
|
|
33
33
|
| 1.3.3 | Sensory Characteristics | A | judgment | — | 10.9, 10.10 |
|
|
34
|
-
| 1.3.4 | Orientation | AA | needs-rendering |
|
|
34
|
+
| 1.3.4 | Orientation | AA | needs-rendering | rendered-orientation-lock | 13.9 |
|
|
35
35
|
| 1.3.5 | Identify Input Purpose | AA | judgment | field-purpose-incomplete, autocomplete-token-invalid | 11.13 |
|
|
36
36
|
|
|
37
37
|
## 1.4 Distinguishable
|
|
38
38
|
|
|
39
39
|
| SC | Title | Level | Automatability | Rules | RGAA |
|
|
40
40
|
|---|---|---|---|---|---|
|
|
41
|
-
| 1.4.1 | Use of Color | A | needs-rendering |
|
|
41
|
+
| 1.4.1 | Use of Color | A | needs-rendering | rendered-link-colour-only | 3.1, 10.6, 10.7, 10.9, 10.10 |
|
|
42
42
|
| 1.4.2 | Audio Control | A | static | autoplay-media | 4.10 |
|
|
43
|
-
| 1.4.3 | Contrast (Minimum) | AA | needs-rendering | contrast-literal | 3.2, 10.5 |
|
|
43
|
+
| 1.4.3 | Contrast (Minimum) | AA | needs-rendering | contrast-literal, rendered-contrast, rendered-contrast-pixel | 3.2, 10.5 |
|
|
44
44
|
| 1.4.4 | Resize Text | AA | needs-rendering | meta-viewport-zoom-block | 10.4 |
|
|
45
45
|
| 1.4.5 | Images of Text | AA | needs-rendering | — | 1.8 |
|
|
46
46
|
| 1.4.10 | Reflow | AA | needs-rendering | — | 10.11 |
|
|
47
|
-
| 1.4.11 | Non-text Contrast | AA | needs-rendering |
|
|
47
|
+
| 1.4.11 | Non-text Contrast | AA | needs-rendering | rendered-nontext-contrast | 3.3 |
|
|
48
48
|
| 1.4.12 | Text Spacing | AA | needs-rendering | — | 10.12 |
|
|
49
49
|
| 1.4.13 | Content on Hover or Focus | AA | needs-rendering | — | 10.13 |
|
|
50
50
|
|
|
@@ -79,7 +79,7 @@ criteria that map to it. SC ids, titles and levels are derived from the W3C sour
|
|
|
79
79
|
| 2.4.4 | Link Purpose (In Context) | A | judgment | link-empty-name, icon-only-control-unnamed | 6.1, 6.2 |
|
|
80
80
|
| 2.4.5 | Multiple Ways | AA | judgment | — | 12.1, 12.3, 12.4 |
|
|
81
81
|
| 2.4.6 | Headings and Labels | AA | judgment | — | 9.1, 11.1, 11.2 |
|
|
82
|
-
| 2.4.7 | Focus Visible | AA | needs-rendering |
|
|
82
|
+
| 2.4.7 | Focus Visible | AA | needs-rendering | rendered-focus-not-visible | 7.3, 10.7 |
|
|
83
83
|
| 2.4.11 | Focus Not Obscured (Minimum) | AA | needs-rendering | — | — |
|
|
84
84
|
|
|
85
85
|
## 2.5 Input Modalities
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# See the defects while you build — `dev`
|
|
2
|
+
|
|
3
|
+
An audit at the end of a sprint finds the same defect fifty times. The cheapest moment to fix
|
|
4
|
+
an accessibility problem is the moment you write it, in the page you are looking at.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
node scripts/ultra11y.mjs dev --next # write the overlay component (once)
|
|
8
|
+
node scripts/ultra11y.mjs dev # start the side-car, then run your app as usual
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Two halves, deliberately separate:
|
|
12
|
+
|
|
13
|
+
- **the side-car** — a `node:http` server (a builtin: the zero-dependency promise holds) on
|
|
14
|
+
`http://127.0.0.1:4111`. It receives collected pages, writes them as snapshots, audits them,
|
|
15
|
+
and serves the dashboard.
|
|
16
|
+
- **the overlay** — framework-agnostic vanilla JS, served by the side-car and loaded by the
|
|
17
|
+
app. A floating panel lists the current page's non-conformities; each links to its
|
|
18
|
+
`file:line` and opens it in your editor through Next's own `/__nextjs_launch-editor`
|
|
19
|
+
endpoint. It re-checks on every client-side navigation, not just the first paint.
|
|
20
|
+
|
|
21
|
+
## Wiring it into Next
|
|
22
|
+
|
|
23
|
+
`dev --next` writes `.ultra11y/next/overlay.jsx`. You add one line:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// app/layout.tsx
|
|
27
|
+
import { Ultra11yOverlay } from "../.ultra11y/next/overlay";
|
|
28
|
+
|
|
29
|
+
<body>{children}<Ultra11yOverlay /></body>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A component you import, not a bundler hack — deliberately. Custom webpack configuration does
|
|
33
|
+
not exist under Turbopack (the Next default), and entry-point patching breaks across Next
|
|
34
|
+
versions; a client component does not. It **renders nothing outside development**: the whole
|
|
35
|
+
body short-circuits on `NODE_ENV`, so shipping it is inert.
|
|
36
|
+
|
|
37
|
+
The same overlay serves any framework — point a `<script src="http://127.0.0.1:4111/overlay.js">`
|
|
38
|
+
at it in dev and set `window.__ULTRA11Y_ENDPOINT__` if you moved the port.
|
|
39
|
+
|
|
40
|
+
## The dashboard
|
|
41
|
+
|
|
42
|
+
`http://127.0.0.1:4111` renders the **per-page criterion grid** (`references/pages.md`) for
|
|
43
|
+
every page you have visited: one row per criterion, one column per page, in the standard you
|
|
44
|
+
chose (`--standard rgaa`). It is a self-contained page — no external stylesheet, no script, no
|
|
45
|
+
network — and it accumulates as you browse. Visit five pages of your app and you have the
|
|
46
|
+
beginnings of a real échantillon, snapshotted and re-auditable offline.
|
|
47
|
+
|
|
48
|
+
## Two things worth knowing
|
|
49
|
+
|
|
50
|
+
**The overlay removes itself from the page before collecting.** It is DOM, and the collector
|
|
51
|
+
serializes DOM: left in place it would be captured, audited, and would report non-conformities
|
|
52
|
+
about *itself* — and, worse, its element would shift every document-order index by one and
|
|
53
|
+
break the join between the styles digest and the DOM. So the host is detached for the duration
|
|
54
|
+
of the collection and re-attached immediately after. It also lives in a shadow root, so the
|
|
55
|
+
app's CSS cannot reach it and its own cannot leak.
|
|
56
|
+
|
|
57
|
+
**The server binds to loopback only.** It writes files and returns audit results, so it is
|
|
58
|
+
never reachable from the network — `127.0.0.1`, never `0.0.0.0`. CORS is permissive because
|
|
59
|
+
the app runs on a different port; the loopback bind is what actually contains it. This is a
|
|
60
|
+
development tool, not something to expose.
|
|
61
|
+
|
|
62
|
+
## What it does NOT do
|
|
63
|
+
|
|
64
|
+
It runs the same static engine as `audit`, plus the rendered tier when the page carries
|
|
65
|
+
signals. It does not adjudicate the judgment criteria — alt relevance, link purpose, reading
|
|
66
|
+
order remain yours (`verify --manual`) — and a clean panel means "the engine found nothing
|
|
67
|
+
here", never "this page is accessible".
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Run the audit during your E2E tests (Playwright / Cypress)
|
|
2
|
+
|
|
3
|
+
`scan` launches its **own** browser after the fact. That is a second run of your app, with
|
|
4
|
+
none of the state your tests just built: no login, no filled form, no opened modal. The E2E
|
|
5
|
+
integration inverts it — the audit runs **inside the run you already have**, on the page as
|
|
6
|
+
your test left it.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
node scripts/ultra11y.mjs render --e2e # detects Playwright / Cypress, writes the fixtures
|
|
10
|
+
node scripts/ultra11y.mjs render --e2e --runner cypress # or force one
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Fixtures land in `.ultra11y/e2e/`. They are **generated files**, not a published library: no
|
|
14
|
+
install, and they target the engine build that produced them (`ULTRA11Y=<path>` overrides).
|
|
15
|
+
|
|
16
|
+
## Playwright
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { test, checkA11y } from "../.ultra11y/e2e/playwright.mjs";
|
|
20
|
+
|
|
21
|
+
test("home page is accessible", async ({ page, ultra11y }) => {
|
|
22
|
+
await page.goto("/");
|
|
23
|
+
await ultra11y({ as: "accueil" }); // via the fixture
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// …or with no fixture, on any page object:
|
|
27
|
+
await checkA11y(page, { as: "contact", failOn: "major" });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Cypress
|
|
31
|
+
|
|
32
|
+
Cypress test code runs in the **browser**, which cannot write to disk, so the collected page
|
|
33
|
+
round-trips through a Node task. Two files, two wiring points:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
// cypress.config.js
|
|
37
|
+
import ultra11y from "./.ultra11y/e2e/cypress-plugin.mjs";
|
|
38
|
+
export default defineConfig({ e2e: { setupNodeEvents(on, config) { ultra11y(on); return config; } } });
|
|
39
|
+
|
|
40
|
+
// cypress/support/e2e.js (the supportFile)
|
|
41
|
+
import "../../.ultra11y/e2e/cypress-commands.mjs";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
cy.visit("/");
|
|
46
|
+
cy.ultra11y({ as: "accueil" });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Options
|
|
50
|
+
|
|
51
|
+
| Option | Effect |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `as` | the page **id** (directory name). Defaults to the slugified URL path |
|
|
54
|
+
| `name` | human page name for the report. Defaults to the document title |
|
|
55
|
+
| `failOn` | `"blocking"` (default), `"major"`, `"minor"`, or `false` to record without ever failing |
|
|
56
|
+
| `auth` | mark the page as sitting behind authentication |
|
|
57
|
+
| `sources` | source files that rendered it — findings are then reported against **your code** |
|
|
58
|
+
| `notes` | reproduction notes carried into the auditor ticket |
|
|
59
|
+
|
|
60
|
+
`failOn: false` is the useful one for adoption: record every page first, look at the real
|
|
61
|
+
backlog, then turn the gate on.
|
|
62
|
+
|
|
63
|
+
## What it does, exactly
|
|
64
|
+
|
|
65
|
+
Two steps per checked page, and the fixture owns neither:
|
|
66
|
+
|
|
67
|
+
1. the page is collected **in the browser** with the engine's own collector — DOM,
|
|
68
|
+
computed-style digest, bounding boxes;
|
|
69
|
+
2. the payload is piped to `ultra11y snapshot write`, which persists
|
|
70
|
+
`.ultra11y/pages/<id>/` **and** audits it, returning the `AuditResult`.
|
|
71
|
+
|
|
72
|
+
So the fixture knows nothing about the snapshot format, the provenance comment or the audit —
|
|
73
|
+
it is a pipe. There is no second implementation to drift out of sync, and any other producer
|
|
74
|
+
can use the same command.
|
|
75
|
+
|
|
76
|
+
## One difference between the two runners
|
|
77
|
+
|
|
78
|
+
The Playwright fixture also captures a **viewport screenshot** and sends it with the payload,
|
|
79
|
+
which is what feeds the pixel tier (contrast over a gradient or a background image — the case
|
|
80
|
+
computed styles cannot express). Pass `screenshot: false` to skip it.
|
|
81
|
+
|
|
82
|
+
The Cypress command does **not**: `cy.screenshot()` writes to a path Cypress chooses and
|
|
83
|
+
names, so wiring it back into the payload reliably is more machinery than it is worth. A
|
|
84
|
+
Cypress-collected page therefore gets every rule except `rendered-contrast-pixel`, and that
|
|
85
|
+
criterion simply stays undecided for it rather than being guessed. If you need the pixel tier,
|
|
86
|
+
scan those pages separately (`scan <url>`), or capture them with Playwright.
|
|
87
|
+
|
|
88
|
+
Everything else — the DOM, the computed styles, the boxes and the stylesheets — is identical
|
|
89
|
+
between the two.
|
|
90
|
+
|
|
91
|
+
## Why the artefact matters more than the assertion
|
|
92
|
+
|
|
93
|
+
A failing assertion tells one developer, once. The **snapshot** is the durable part:
|
|
94
|
+
|
|
95
|
+
- It records the page as the browser actually built it, so the same page can be re-audited
|
|
96
|
+
**offline, with no browser** — which is how CI decides rendering criteria without booting
|
|
97
|
+
the app, and how the report speaks page by page.
|
|
98
|
+
- Because it is a full document (a component capture is a fragment), the page-scoped rules run
|
|
99
|
+
on it: **RGAA 8.3** (`lang`), **8.5/8.6** (`title`), 12.6 (`main`). Those are not decidable
|
|
100
|
+
from a component render, nor from source once a framework injects the document shell.
|
|
101
|
+
- Every finding carries `page` **and** `origin.sourceFile`, so a defect found at render time
|
|
102
|
+
is reported against the code that caused it.
|
|
103
|
+
|
|
104
|
+
Commit `.ultra11y/pages/` to gate on it (`audit` ingests the directory automatically). Add
|
|
105
|
+
`sources` to your options and the failure message names your component instead of `dom.html`.
|
|
106
|
+
|
|
107
|
+
## Scoping
|
|
108
|
+
|
|
109
|
+
`snapshot write` audits **only the page it was handed** — never the whole pages tree — so a
|
|
110
|
+
test checking the contact page is never failed by the home page's backlog. Findings are
|
|
111
|
+
filtered to non-advisory ones before the gate: a non-normative recommendation never fails a
|
|
112
|
+
test.
|
|
@@ -88,3 +88,28 @@ Then `node scripts/ultra11y.mjs verify --apply VERIFY.todo.json` is green again
|
|
|
88
88
|
> For a country standard's own test grid, `criteria --standard rgaa <id>` shows the RGAA tests
|
|
89
89
|
> behind a WCAG SC. The pre-completion checklist stops you concluding too early. The rule that
|
|
90
90
|
> never bends: no residual criterion reaches `C` without a recorded, gated justification.
|
|
91
|
+
|
|
92
|
+
## Under a country standard, the worklist speaks that standard
|
|
93
|
+
|
|
94
|
+
`verify --manual --standard rgaa` keys the worklist by **RGAA criteria**, not WCAG success
|
|
95
|
+
criteria — which is the granularity that matters, since 99 of RGAA's 106 criteria can only
|
|
96
|
+
ever derive `manual`. Each item carries, inline:
|
|
97
|
+
|
|
98
|
+
- the criterion's **numbered tests, in full** (`11.2.1` … `11.2.6`) — what actually has to be
|
|
99
|
+
ruled on;
|
|
100
|
+
- its **technical note** and **particular cases**;
|
|
101
|
+
- its **implementation guidance** (before/after), previously reserved for criteria that
|
|
102
|
+
already had a finding;
|
|
103
|
+
- the **definitions** of the terms its own tests cite, from the standard's glossary.
|
|
104
|
+
|
|
105
|
+
**`normativeRef` must cite one of the item's OWN tests.** This is not pedantry: an RGAA test
|
|
106
|
+
id has the same `N.N.N` shape as a WCAG success criterion, so a laxer check accepted the WCAG
|
|
107
|
+
id an agent naturally reaches for and read it as an unrelated test — citing `1.4.3` (Contrast
|
|
108
|
+
Minimum) validated as RGAA test 1.4.3, which is about CAPTCHA images. The worklist now lists
|
|
109
|
+
the acceptable references per item, and anything else is rejected.
|
|
110
|
+
|
|
111
|
+
Verdicts fold into `packAdjudication`, not onto the WCAG criteria: a pack decision must not
|
|
112
|
+
rewrite the core verdict, and since WCAG 1.1.1 alone fans out to 19 RGAA criteria, folding by
|
|
113
|
+
success criterion would let those criteria overwrite one another.
|
|
114
|
+
|
|
115
|
+
Look a defined term up on its own with `criteria --standard rgaa --glossary <term>`.
|