ultra11y 2.30.0 → 2.31.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.
Files changed (54) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.opencode/plugins/ultra11y.js +1 -1
  5. package/README.md +59 -7
  6. package/dist/cypress-plugin.d.ts +17 -0
  7. package/dist/cypress-plugin.mjs +82 -0
  8. package/dist/cypress.d.ts +11 -0
  9. package/dist/cypress.mjs +170 -0
  10. package/dist/payload-D0VQYlGS.d.ts +44 -0
  11. package/dist/playwright.d.ts +35 -0
  12. package/dist/playwright.mjs +250 -0
  13. package/docker/runner.mjs +12 -1
  14. package/package.json +25 -3
  15. package/scripts/build-extension-icons.mjs +137 -0
  16. package/scripts/check-artifact-recall.mjs +199 -0
  17. package/scripts/sync-engine.mjs +126 -52
  18. package/scripts/ultra11y.mjs +1368 -348
  19. package/scripts/vendor/wcag-2.2-fr.json +1 -1
  20. package/scripts/vendor/wcag-2.2-sc-universe.json +1 -1
  21. package/skills/review-a11y/SKILL.md +1 -1
  22. package/skills/review-a11y/scripts/ultra11y.mjs +1368 -348
  23. package/skills/ultra11y/SKILL.md +2 -1
  24. package/skills/ultra11y/references/ci.md +33 -0
  25. package/skills/ultra11y/references/devtools.md +7 -0
  26. package/skills/ultra11y/references/dynamic.md +54 -1
  27. package/skills/ultra11y/references/e2e.md +42 -11
  28. package/skills/ultra11y/references/extension.md +52 -0
  29. package/skills/ultra11y/references/judgment.md +38 -0
  30. package/skills/ultra11y/references/pages.md +66 -3
  31. package/skills/ultra11y/scripts/ultra11y.mjs +1368 -348
  32. package/src/annotate.ts +53 -13
  33. package/src/check.ts +22 -5
  34. package/src/cli.ts +426 -32
  35. package/src/collector.ts +125 -0
  36. package/src/crawl.ts +43 -0
  37. package/src/data/wcag-universe.json +1 -1
  38. package/src/dev.ts +136 -5
  39. package/src/e2e.ts +13 -2
  40. package/src/init.ts +11 -2
  41. package/src/integrations/core.ts +75 -0
  42. package/src/integrations/cypress-plugin.ts +75 -0
  43. package/src/integrations/cypress.ts +56 -0
  44. package/src/integrations/payload.ts +141 -0
  45. package/src/integrations/playwright.ts +95 -0
  46. package/src/llm.ts +259 -0
  47. package/src/pages-report.ts +302 -0
  48. package/src/pages.ts +25 -4
  49. package/src/report.ts +48 -18
  50. package/src/sample.ts +57 -0
  51. package/src/scan-local.ts +57 -5
  52. package/src/scan.ts +162 -12
  53. package/src/snapshot.ts +44 -116
  54. package/src/types.ts +7 -1
@@ -10,7 +10,7 @@
10
10
  "name": "ultra11y",
11
11
  "source": "./",
12
12
  "description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
13
- "version": "2.30.0",
13
+ "version": "2.31.0",
14
14
  "author": { "name": "maxgfr" },
15
15
  "homepage": "https://github.com/maxgfr/ultra11y",
16
16
  "repository": "https://github.com/maxgfr/ultra11y",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ultra11y",
3
3
  "description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
4
- "version": "2.30.0",
4
+ "version": "2.31.0",
5
5
  "author": { "name": "maxgfr" },
6
6
  "homepage": "https://github.com/maxgfr/ultra11y",
7
7
  "repository": "https://github.com/maxgfr/ultra11y",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ultra11y",
3
3
  "description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
4
- "version": "2.30.0",
4
+ "version": "2.31.0",
5
5
  "author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
6
6
  "homepage": "https://github.com/maxgfr/ultra11y",
7
7
  "repository": "https://github.com/maxgfr/ultra11y",
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
27
27
  // Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
28
28
  // does not carry it, so a user's own ultra11y.js is never clobbered.
29
29
  const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
30
- const ULTRA11Y_PLUGIN_VERSION = "2.30.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "2.31.0";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
package/README.md CHANGED
@@ -32,7 +32,7 @@ ultra11y is built around an honest **division of labour**. Automated tools only
32
32
  - **Needs rendering (scan tier):** computed contrast, visible focus, zoom/reflow, content on hover — decided by `scan` (axe-core in a real browser); until then they stay **residual risks**, never silently "conforming".
33
33
  - **Advisory vs non-conformity:** a good practice with no failing normative test (an `advisory` pack rule, a best-practice-only axe violation, an agent recommendation) renders as « Recommandation (non normative) » — it never flips a criterion to `NC` nor enters the conformance rate. An NC always cites a `normativeRef`.
34
34
  - **Stateful scan probes:** the local runtime drives the page with bounded, non-navigating interactions (fill inputs then re-measure overflow; a live-region probe for status messages 4.1.3) — `--no-interact` opts out; `--interact-clicks` re-enables button clicks on authenticated scans (destructive-named buttons are never clicked).
35
- - **Normative page sample (échantillon):** a country-standard audit runs over a declared `sample.pages` set — `sample check` lints its coverage against the standard's required page kinds, `scan --sample` scans it, and an un-scanned `--standard rgaa` report is flagged **partial**.
35
+ - **Normative page sample (échantillon):** a country-standard audit runs over a declared `sample.pages` set — **`pages discover --crawl|--sitemap --write` builds it for you** (names read from each page's `<title>`, existing entries never overwritten), `sample check` lints its coverage against the standard's required page kinds, `scan --sample` scans it, and an un-scanned `--standard rgaa` report is flagged **partial**.
36
36
  - **Pack-only detection (declarative):** a standards pack can ship its own `rules` (a bounded, ReDoS-guarded matcher DSL — no code) and normativity/severity `overrides`, projecting onto its criteria without forking the engine.
37
37
 
38
38
  ## Measured against a corpus it did not write
@@ -58,6 +58,14 @@ npx ultra11y --help # no install
58
58
  npm i -D ultra11y # or as a dev dependency
59
59
  ```
60
60
 
61
+ **As a test-runner plugin** — audit a page inside your own E2E run, in the state your test
62
+ built it:
63
+
64
+ ```js
65
+ import { checkA11y } from "ultra11y/playwright"; // or "ultra11y/cypress" (+ "/plugin")
66
+ await checkA11y(page, { as: "accueil" });
67
+ ```
68
+
61
69
  **As an agent skill** — the repository ships **two** (pick at the prompt, or pass the name):
62
70
  `ultra11y` for full audits, compliance reports, PRD backlogs and standards packs, and
63
71
  `review-a11y` for the fast loop that audits **exactly the code under change** (staged files,
@@ -140,8 +148,11 @@ ultra11y render [<dir>] [--scaffold | --setup | --e2e | --coverage | --storybo
140
148
  ultra11y snapshot write [--root <dir>] [--fail-on blocking|major|minor] [--json] # payload on stdin → .ultra11y/pages/<id>/ + audit
141
149
  ultra11y snapshot list [--root <dir>] [--json]
142
150
  ultra11y pages --in <audit.json> [--standard <pack>] [--json] [--lang auto|en|fr] # the per-page criterion grid
151
+ ultra11y pages --in <audit.json> --format report [--split page] [--out <dir>] # the per-page report, with screenshots
152
+ ultra11y pages discover --sitemap <url> | --crawl <url> [--depth <n>] [--max <n>] [--write] # build the page sample
143
153
  ultra11y dev [--port <n>] [--standard <pack>] | dev --next # live overlay while you build + dashboard
144
154
  ultra11y criteria [<sc>] [--list] [--standard <pack> [--theme <N>]] [--generate] [--json] [--lang auto|en|fr]
155
+ ultra11y judge --in <audit.json> [--standard <pack>] [--max <n>] [--model <id>] [--apply] # adjudicate with a model (ANTHROPIC_API_KEY)
145
156
  ultra11y check --report <md> [--standard <pack>] [--quiet] [--json]
146
157
  ultra11y verify --report <md> [--standard <pack>] [--semantic] [--apply <verdicts.json>] [--max-verify <n>] [--json]
147
158
  ultra11y fix <globs… | -> [--write] [--iterate] [--changed | --since <ref> | --staged] [--safe] [--only <ids>] [--jsx] [--json]
@@ -260,8 +271,13 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
260
271
  auto-codemods, fill-in `TODO` placeholders for the agent to complete, and judgment-only
261
272
  proposals. `--dry-run` is the default; `--write` applies but only after a re-audit proves
262
273
  no new non-conformity, and never on lossy JSX/TSX. See `references/fix.md`.
263
- - **During your E2E run** — `render --e2e` writes Playwright/Cypress fixtures that audit a
264
- targeted page **inside the test run you already have**, so the page is checked in the state
274
+ - **During your E2E run** — `import { checkA11y } from "ultra11y/playwright"` (or
275
+ `ultra11y/cypress` + `ultra11y/cypress/plugin`) audits a targeted page **inside the test run
276
+ you already have**; `render --e2e` still writes install-free fixtures for a repo that does
277
+ not depend on ultra11y, and the two cannot drift (the fixtures interpolate the published
278
+ module's tables, and a test gates both over the same findings). Cypress now feeds the pixel
279
+ tier too, via `after:screenshot`. `checkA11y(page, { report: true })` emits the per-page
280
+ report straight out of the run. The audit runs, so the page is checked in the state
265
281
  your test built (logged in, form filled, modal open) — state a separate `scan` run does not
266
282
  have. Each checked page is persisted as a **snapshot** (`.ultra11y/pages/<id>/`: the whole
267
283
  rendered document + computed styles + boxes), which is the durable half: the same page
@@ -269,6 +285,11 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
269
285
  a snapshot is a full document, the page-scoped rules finally run — RGAA **8.3** (`lang`),
270
286
  **8.5/8.6** (`title`) — and every finding carries both its page and the source file that
271
287
  rendered it. See `references/e2e.md` and `references/pages.md`.
288
+ - **In your browser** — a Chrome extension (`extension/`) audits the page you are *looking at*:
289
+ a staging build, a page behind a login you walked through by hand. It is a client of the
290
+ side-car, not a second engine — it fetches the engine's own collector rather than copying it,
291
+ its manifest can reach **loopback and nothing else**, and the popup decides nothing the engine
292
+ did not. See `references/extension.md`.
272
293
  - **While you build** — `dev --next` writes a one-line Next overlay component and `dev` starts
273
294
  a loopback side-car: a floating panel lists the current page's non-conformities as you
274
295
  browse, each linking to its `file:line` in your editor, and `http://127.0.0.1:4111` shows
@@ -298,6 +319,15 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
298
319
  rather than guessed, and without signals the rules do not fire, so no pre-existing verdict
299
320
  changes. `tests/rgaa-coverage.test.ts` ratchets the result: **48 of 106** RGAA criteria now
300
321
  map onto an engine rule, up from 43, and the number can only go up.
322
+ - **Every scanned page is a snapshot** — `scan` no longer keeps only findings: each page it
323
+ visits is persisted to `.ultra11y/pages/<id>/` (`--no-snapshot` opts out). That is what turns
324
+ a URL into a real per-page verdict — a page known only by its URL can never be conforming by
325
+ silence, so a sitemap-driven audit used to produce an almost empty grid. With a snapshot the
326
+ page-scoped rules run (**RGAA 8.3 / 8.5 / 8.6**), the DOM **JavaScript built at runtime** is
327
+ audited like any other markup, and the page re-audits **offline, with no browser**. The
328
+ collection happens on the *pristine* page — before axe injects its source and before any probe
329
+ fills an input or resizes the viewport — otherwise the snapshot would record our own
330
+ instrumentation instead of the site.
301
331
  - **Page by page** — RGAA is a per-page norm; the engine's verdict is scope-wide. `pages`
302
332
  bridges the two: one row per criterion, one column per page, embedded in `report` and
303
333
  rebuilt from a committed `audit.json` alone. The per-page status is not recomputed — a
@@ -305,7 +335,12 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
305
335
  and report agree by construction. Two rules hold the line: a finding is attributed to a page
306
336
  only when something **says** so (else it is counted as unattributed, never spread across
307
337
  pages), and a criterion is conforming *by silence* only on a page whose real rendered DOM
308
- was audited. See `references/pages.md`.
338
+ was audited. And `pages --format report [--split page]` turns that into the **deliverable an
339
+ auditor hands over**: one dossier per page — identity, **screenshot**, rate, *every* criterion
340
+ of the standard with its status on that page, then each non-conformity as the ordinary auditor
341
+ block. It re-decides nothing and invents no format, so a page sheet and the compliance report
342
+ cannot disagree; `check` gates it against invented criteria like any other report.
343
+ See `references/pages.md`.
309
344
  - **A shipped GitHub Action** — `uses: maxgfr/ultra11y@main` audits the **code** (PR diff) and,
310
345
  optionally, the **pages** (it can start your app, wait for it, then scan real URLs or your
311
346
  declared sample). The engine ships inside the action, so there is nothing to install and no
@@ -325,6 +360,18 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
325
360
  regression variant (hook + committed baseline / GitHub Actions job) that fails only on **new**
326
361
  non-conformities, not the existing backlog. See `references/automation.md`.
327
362
 
363
+ ### Optional model tier (`judge`) — for the runs with no agent in the loop
364
+
365
+ Inside a coding agent the judgment criteria are the agent's to rule on. Outside one — CI, a
366
+ browser extension, an E2E run — nobody does, so they stay « à évaluer ». `judge` adjudicates
367
+ them with a model, and is a **caller, not a second judge**: the worklist, the evidence and the
368
+ prompt are `verify --manual`'s own, and the verdicts pass through the **same fail-closed gate**
369
+ an agent's do. An unjustified `C`, an `NC` citing a line that does not resolve, a verdict for a
370
+ criterion nobody asked about, or an incomplete run are all refused — and a rejected
371
+ adjudication leaves the audit untouched. All-`manual` with reasons is accepted, because that is
372
+ a correct answer. It is the **only** part of the tool that takes an API key; without one the
373
+ command explains itself and exits, and nothing else changes.
374
+
328
375
  ### Optional dynamic tier (axe-core)
329
376
 
330
377
  `scan` runs **axe-core in a headless browser** to decide the *needs-rendering* criteria the static engine leaves as residual risks — chiefly **computed colour contrast (1.4.3)** plus a 320px **reflow** check (1.4.10) and a render cross-check of the structural rules. Two runtimes (default `--runtime auto`): **`--runtime local`** uses a Playwright that resolves from your project (`--cwd`) — **no Docker** — and additionally probes focus visibility (2.4.7), 200% zoom (1.4.4), text spacing (1.4.12) and content-on-hover (1.4.13), plus **stateful** probes (fill inputs then re-measure overflow, and a live-region probe for status messages 4.1.3 — opt out with `--no-interact`, `--interact-clicks` for authed-scan button clicks), and takes `--storage-state` for authenticated pages; **`--runtime docker`** falls back to a self-contained image built on first use. `--merge` folds the findings into a static `AuditResult`, upgrading `manual` criteria to `C`/`NC`. A country-standard audit scans its declared page sample with `scan --sample` (lint it first with `sample check`):
@@ -353,14 +400,19 @@ The skill (`skills/ultra11y/SKILL.md` + `references/`) teaches the agent when an
353
400
  pnpm install
354
401
  pnpm test # vitest
355
402
  pnpm run typecheck
356
- pnpm run build # tsup → scripts/ultra11y.mjs, mirrored into skills/ultra11y/scripts/
357
- pnpm run check:build # asserts the committed bundle is reproducible
403
+ pnpm run build # tsup → scripts/ultra11y.mjs (engine) + dist/ (the e2e plugins)
404
+ pnpm run check:build # asserts every committed artefact is reproducible from its source
358
405
  pnpm run build:wcag # re-derive src/data/wcag.json from the vendored W3C source
359
406
  pnpm run build:pack:rgaa # re-build the RGAA pack from the vendored DINUM source
360
407
  pnpm run build:criteria # regenerate skills/ultra11y/references/criteria.md
408
+ pnpm run build:icons # re-generate the browser extension's icons from their source
361
409
  ```
362
410
 
363
- Releases are cut automatically by semantic-release on push to `main` (GitHub release + tarball, no npm publish).
411
+ Releases are cut automatically by **semantic-release** on push to `main` (GitHub release +
412
+ npm publish via OIDC trusted publishing, plus the moving `v2` major tag). The version bump is
413
+ decided by **Conventional Commits**: a commit whose subject carries no `feat:`/`fix:` prefix
414
+ is analysed as *no release*, so a change that must ship needs at least one conventional commit
415
+ in the range — otherwise `@v2` and npm keep serving the previous version while `main` moves on.
364
416
 
365
417
  ## Data & licensing
366
418
 
@@ -0,0 +1,17 @@
1
+ import { S as SnapshotPayload, F as FindingLike } from './payload-D0VQYlGS.js';
2
+
3
+ type On = (event: string, handler: any) => void;
4
+ interface TaskPayload extends SnapshotPayload {
5
+ failOn?: string | false;
6
+ /** The name `cy.screenshot()` was called with, when the browser half took one. */
7
+ screenshotName?: string;
8
+ }
9
+ interface TaskResult {
10
+ findings: FindingLike[];
11
+ failing: FindingLike[];
12
+ message: string;
13
+ }
14
+ /** Register the ultra11y task (and the screenshot capture) on Cypress's node events. */
15
+ declare function register(on: On): void;
16
+
17
+ export { type TaskPayload, type TaskResult, register as default };
@@ -0,0 +1,82 @@
1
+ // src/integrations/cypress-plugin.ts
2
+ import { readFileSync } from "fs";
3
+
4
+ // src/integrations/core.ts
5
+ import { spawnSync } from "child_process";
6
+ import { createRequire } from "module";
7
+
8
+ // src/integrations/payload.ts
9
+ var RANK = { bloquant: 0, majeur: 1, mineur: 2 };
10
+ var THRESHOLD = { blocking: 0, bloquant: 0, major: 1, majeur: 1, minor: 2, mineur: 2 };
11
+ function failingFindings(result, failOn) {
12
+ const max = THRESHOLD[failOn];
13
+ if (max === void 0) throw new Error(`ultra11y: failOn must be blocking|major|minor (got "${failOn}")`);
14
+ return (result.findings ?? []).filter((f) => !f.advisory && (RANK[f.severity] ?? 99) <= max);
15
+ }
16
+ function formatFailure(pageName, failing) {
17
+ const lines = [`ultra11y: ${failing.length} accessibility non-conformity(ies) on "${pageName}":`];
18
+ for (const f of failing.slice(0, 20)) {
19
+ lines.push(` [${f.severity}] ${f.ruleId} (WCAG ${f.criteriaId}) \u2014 ${f.origin?.sourceFile ?? f.file} \u2014 ${f.message}`);
20
+ }
21
+ if (failing.length > 20) lines.push(` \u2026 and ${failing.length - 20} more.`);
22
+ lines.push("Full detail: .ultra11y/pages/ \u2014 re-audit offline with `ultra11y audit`.");
23
+ return lines.join("\n");
24
+ }
25
+
26
+ // src/integrations/core.ts
27
+ function enginePath() {
28
+ if (process.env.ULTRA11Y) return process.env.ULTRA11Y;
29
+ try {
30
+ return createRequire(import.meta.url).resolve("ultra11y/scripts/ultra11y.mjs");
31
+ } catch {
32
+ return new URL("../scripts/ultra11y.mjs", import.meta.url).pathname;
33
+ }
34
+ }
35
+ function auditSnapshot(payload, engine = enginePath()) {
36
+ const res = spawnSync(process.execPath, [engine, "snapshot", "write", "--json"], {
37
+ input: JSON.stringify(payload),
38
+ encoding: "utf8",
39
+ maxBuffer: 192 * 1024 * 1024
40
+ });
41
+ if (res.error) throw new Error(`ultra11y: could not run the engine at ${engine} \u2014 ${res.error.message}`);
42
+ if (!res.stdout) throw new Error(`ultra11y: the engine produced no output (exit ${res.status})
43
+ ${res.stderr || ""}`);
44
+ try {
45
+ return JSON.parse(res.stdout);
46
+ } catch {
47
+ throw new Error(`ultra11y: could not parse the engine output
48
+ ${res.stdout.slice(0, 500)}`);
49
+ }
50
+ }
51
+
52
+ // src/integrations/cypress-plugin.ts
53
+ function register(on) {
54
+ const shots = /* @__PURE__ */ new Map();
55
+ on("after:screenshot", (details) => {
56
+ if (details?.name && details.path) shots.set(details.name, details.path);
57
+ return details;
58
+ });
59
+ on("task", {
60
+ ultra11ySnapshot(payload) {
61
+ const withShot = { ...payload };
62
+ const path = payload.screenshotName ? shots.get(payload.screenshotName) : void 0;
63
+ if (path) {
64
+ try {
65
+ withShot.screenshot = readFileSync(path).toString("base64");
66
+ } catch {
67
+ }
68
+ }
69
+ const result = auditSnapshot(withShot);
70
+ const failOn = payload.failOn === void 0 ? "blocking" : payload.failOn;
71
+ const failing = failOn === false ? [] : failingFindings(result, failOn);
72
+ return {
73
+ findings: result.findings ?? [],
74
+ failing,
75
+ message: failing.length ? formatFailure(String(payload.meta.name), failing) : ""
76
+ };
77
+ }
78
+ });
79
+ }
80
+ export {
81
+ register as default
82
+ };
@@ -0,0 +1,11 @@
1
+ import { C as CheckOptions } from './payload-D0VQYlGS.js';
2
+ export { s as slugify } from './payload-D0VQYlGS.js';
3
+
4
+ interface CypressCheckOptions extends CheckOptions {
5
+ /** Capture a viewport screenshot so the pixel tier can run. On by default — the plugin
6
+ * reads it back through Cypress's `after:screenshot` event. */
7
+ screenshot?: boolean;
8
+ }
9
+ declare function registerUltra11yCommand(): void;
10
+
11
+ export { type CypressCheckOptions, registerUltra11yCommand };
@@ -0,0 +1,170 @@
1
+ // src/collector.ts
2
+ var COLLECTED_CSS = [
3
+ "color",
4
+ "backgroundColor",
5
+ "backgroundImage",
6
+ "fontSize",
7
+ "fontWeight",
8
+ "fontStyle",
9
+ "textDecorationLine",
10
+ "textTransform",
11
+ "lineHeight",
12
+ "letterSpacing",
13
+ "wordSpacing",
14
+ "whiteSpace",
15
+ "display",
16
+ "visibility",
17
+ "opacity",
18
+ "position",
19
+ "overflowX",
20
+ "overflowY",
21
+ "outlineStyle",
22
+ "outlineWidth",
23
+ "outlineColor",
24
+ // All four borders + the box shadow: WCAG 1.4.11 asks whether a control's BOUNDARY is
25
+ // perceivable, and a boundary can be drawn by any side, by the fill, by an outline or by a
26
+ // shadow. Collecting only one side would manufacture non-conformities on the other three.
27
+ "borderTopStyle",
28
+ "borderRightStyle",
29
+ "borderBottomStyle",
30
+ "borderLeftStyle",
31
+ "borderTopWidth",
32
+ "borderRightWidth",
33
+ "borderBottomWidth",
34
+ "borderLeftWidth",
35
+ "borderTopColor",
36
+ "borderRightColor",
37
+ "borderBottomColor",
38
+ "borderLeftColor",
39
+ "boxShadow",
40
+ "cursor"
41
+ ];
42
+ var COLLECT_MAX_ELEMENTS = 5e3;
43
+ var COLLECT_SNAPSHOT = `(() => {
44
+ const PROPS = ${JSON.stringify(COLLECTED_CSS)};
45
+ const MAX = ${COLLECT_MAX_ELEMENTS};
46
+ const els = document.querySelectorAll('*');
47
+ const styles = [];
48
+ const boxes = [];
49
+ const n = Math.min(els.length, MAX);
50
+ for (let i = 0; i < n; i++) {
51
+ const el = els[i];
52
+ const tag = el.tagName.toLowerCase();
53
+ const cs = getComputedStyle(el);
54
+ const css = {};
55
+ for (const p of PROPS) {
56
+ const v = cs[p];
57
+ if (v !== undefined && v !== null && v !== '') css[p] = String(v);
58
+ }
59
+ styles.push({ i: i, tag: tag, css: css });
60
+ const r = el.getBoundingClientRect();
61
+ boxes.push({ i: i, tag: tag, x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) });
62
+ }
63
+ // The page's own stylesheets. Some criteria are properties of the STYLESHEET rather than of
64
+ // any element's computed style \u2014 focus styling removed with no replacement (2.4.7), a media
65
+ // query that locks the orientation (1.3.4) \u2014 and this is the only place they exist.
66
+ // A cross-origin sheet throws on .cssRules: COUNT those instead of ignoring them, so a rule
67
+ // reading this digest can tell "nothing there" apart from "I could not look".
68
+ const cssRules = [];
69
+ let unreadable = 0;
70
+ const MAXR = 4000;
71
+ const camel = (p) => p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
72
+ const walk = (list, media) => {
73
+ for (const r of list) {
74
+ if (cssRules.length >= MAXR) return;
75
+ if (typeof r.selectorText === 'string' && r.style) {
76
+ const decls = {};
77
+ for (let i = 0; i < r.style.length; i++) {
78
+ const prop = r.style[i];
79
+ decls[camel(prop)] = r.style.getPropertyValue(prop);
80
+ }
81
+ cssRules.push(media ? { selector: r.selectorText, media: media, decls: decls } : { selector: r.selectorText, decls: decls });
82
+ } else if (r.cssRules) {
83
+ const cond = r.conditionText || (r.media && r.media.mediaText) || media;
84
+ walk(r.cssRules, cond);
85
+ }
86
+ }
87
+ };
88
+ for (const sheet of document.styleSheets) {
89
+ try {
90
+ walk(sheet.cssRules, undefined);
91
+ } catch (e) {
92
+ unreadable++;
93
+ }
94
+ }
95
+
96
+ const truncated = els.length > MAX;
97
+ return {
98
+ dom: document.documentElement.outerHTML,
99
+ css: { v: 1, rules: cssRules, unreadable: unreadable, truncated: cssRules.length >= MAXR },
100
+ title: document.title,
101
+ lang: document.documentElement.getAttribute('lang') || '',
102
+ url: location.href,
103
+ viewport: { width: window.innerWidth, height: window.innerHeight },
104
+ styles: { v: 1, entries: styles, truncated: truncated },
105
+ boxes: { v: 1, entries: boxes, truncated: truncated }
106
+ };
107
+ })()`;
108
+
109
+ // src/integrations/payload.ts
110
+ function slugify(url) {
111
+ let path = url;
112
+ try {
113
+ path = new URL(url).pathname;
114
+ try {
115
+ path = decodeURIComponent(path);
116
+ } catch {
117
+ }
118
+ } catch {
119
+ }
120
+ const slug = path.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
121
+ return slug || (path === "/" || path === "" ? "accueil" : "page");
122
+ }
123
+ function buildPayload(collected, url, runner, opts, screenshot) {
124
+ const id = opts.as || slugify(url);
125
+ return {
126
+ meta: {
127
+ v: 1,
128
+ id,
129
+ name: opts.name || collected.title || id,
130
+ url,
131
+ runner,
132
+ viewport: collected.viewport,
133
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
134
+ ...opts.auth !== void 0 ? { auth: opts.auth } : {},
135
+ ...opts.sources ? { sources: opts.sources } : {},
136
+ ...opts.notes ? { notes: opts.notes } : {}
137
+ },
138
+ dom: collected.dom,
139
+ styles: collected.styles,
140
+ boxes: collected.boxes,
141
+ css: collected.css,
142
+ ...screenshot ? { screenshot } : {}
143
+ };
144
+ }
145
+
146
+ // src/integrations/cypress.ts
147
+ function registerUltra11yCommand() {
148
+ Cypress.Commands.add("ultra11y", (opts = {}) => {
149
+ const shotName = opts.screenshot === false ? void 0 : `ultra11y-${opts.as || "page"}-${Date.now()}`;
150
+ if (shotName) cy.screenshot(shotName, { capture: "viewport", log: false });
151
+ return cy.window({ log: false }).then((win) => {
152
+ const collected = win.eval(COLLECT_SNAPSHOT);
153
+ const url = collected.url || win.location.href;
154
+ const payload = {
155
+ ...buildPayload(collected, url, "cypress", opts),
156
+ failOn: opts.failOn,
157
+ ...shotName ? { screenshotName: shotName } : {}
158
+ };
159
+ return cy.task("ultra11ySnapshot", payload, { log: false }).then((res) => {
160
+ if (res?.failing?.length) throw new Error(res.message);
161
+ return res;
162
+ });
163
+ });
164
+ });
165
+ }
166
+ registerUltra11yCommand();
167
+ export {
168
+ registerUltra11yCommand,
169
+ slugify
170
+ };
@@ -0,0 +1,44 @@
1
+ interface FindingLike {
2
+ ruleId: string;
3
+ criteriaId: string;
4
+ file: string;
5
+ line: number;
6
+ severity: string;
7
+ message: string;
8
+ advisory?: boolean;
9
+ origin?: {
10
+ sourceFile?: string;
11
+ sourceLine?: number;
12
+ };
13
+ }
14
+ interface AuditLike {
15
+ findings?: FindingLike[];
16
+ }
17
+ interface SnapshotPayload {
18
+ meta: Record<string, unknown>;
19
+ dom: string;
20
+ styles?: unknown;
21
+ boxes?: unknown;
22
+ css?: unknown;
23
+ screenshot?: string;
24
+ }
25
+ /** Same slug rule as the engine (src/snapshot.ts slugifyPageId): accent-folded URL path. */
26
+ declare function slugify(url: string): string;
27
+ interface CheckOptions {
28
+ /** The page id (directory name). Defaults to the slugified URL path. */
29
+ as?: string;
30
+ /** Human page name for the report. Defaults to the document title. */
31
+ name?: string;
32
+ /** Severity at which the check fails, or `false` to record without ever failing. */
33
+ failOn?: "blocking" | "major" | "minor" | false;
34
+ /** Mark the page as sitting behind authentication. */
35
+ auth?: boolean;
36
+ /** Source files that rendered it — findings are then reported against YOUR code. */
37
+ sources?: string[];
38
+ /** Reproduction notes carried into the auditor ticket. */
39
+ notes?: string;
40
+ /** Capture a viewport screenshot for the pixel tier (Playwright: on by default). */
41
+ screenshot?: boolean;
42
+ }
43
+
44
+ export { type AuditLike as A, type CheckOptions as C, type FindingLike as F, type SnapshotPayload as S, slugify as s };
@@ -0,0 +1,35 @@
1
+ import { C as CheckOptions, A as AuditLike } from './payload-D0VQYlGS.js';
2
+
3
+ interface PlaywrightPage {
4
+ evaluate(script: string): Promise<unknown>;
5
+ screenshot(opts: {
6
+ fullPage: boolean;
7
+ }): Promise<Buffer>;
8
+ url(): string;
9
+ }
10
+ interface PlaywrightCheckOptions extends CheckOptions {
11
+ /** Also write the per-page report once this page is recorded. Off by default: a report
12
+ * per checked page would be wasteful in a suite; turn it on in a final test. */
13
+ report?: boolean | {
14
+ out?: string;
15
+ standard?: string;
16
+ lang?: string;
17
+ };
18
+ }
19
+ /** Collect the current page, persist it as a snapshot, audit it, and fail on the threshold. */
20
+ declare function checkA11y(page: PlaywrightPage, opts?: PlaywrightCheckOptions): Promise<AuditLike>;
21
+ /** Optional fixture form. Import `test` from here instead of `@playwright/test`.
22
+ *
23
+ * Playwright belongs to YOUR project, not to this package, so it is resolved from the
24
+ * PROJECT FIRST (cwd) and only then relative to this module — the same order
25
+ * `scan --runtime local` uses (src/scan-local.ts resolveLocalDeps). Module-relative alone
26
+ * works for an ordinary `node_modules/ultra11y` install, where Node walks up into the
27
+ * project's own tree, but silently yields `undefined` whenever the package is linked,
28
+ * pnpm-hoisted elsewhere, or in a monorepo — and a fixture that is `undefined` for
29
+ * layout reasons is a confusing failure.
30
+ *
31
+ * Resolution is lazy and guarded either way, so importing this in a Cypress-only repo
32
+ * gives `undefined` rather than a crash. */
33
+ declare const test: any;
34
+
35
+ export { AuditLike, CheckOptions, type PlaywrightCheckOptions, checkA11y, test };