ultra11y 5.7.0 → 5.9.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/dist/cypress-plugin.d.ts +1 -1
- package/dist/cypress.d.ts +2 -2
- package/dist/{payload-D8dvqLcc.d.ts → payload-DzQb84Kw.d.ts} +16 -6
- package/dist/playwright.d.ts +16 -2
- package/dist/playwright.mjs +81 -44
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +1492 -1308
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +1492 -1308
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/references/e2e.md +25 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +1492 -1308
- package/src/adjudicate-subjects.ts +87 -0
- package/src/adjudicate.ts +162 -2
- package/src/annotate.ts +40 -2
- package/src/audit.ts +89 -0
- package/src/config.ts +10 -0
- package/src/integrations/payload.ts +17 -1
- package/src/integrations/playwright.ts +20 -8
- package/src/probes.ts +130 -45
- package/src/standards/derive.ts +24 -0
- package/src/types.ts +6 -1
- package/dist/payload-B6NDkLYJ.d.ts +0 -51
- package/dist/payload-BshGXnOO.d.ts +0 -68
- package/dist/payload-D0VQYlGS.d.ts +0 -44
|
@@ -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": "5.
|
|
13
|
+
"version": "5.9.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": "5.
|
|
4
|
+
"version": "5.9.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": "5.
|
|
4
|
+
"version": "5.9.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 = "5.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.9.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/dist/cypress-plugin.d.ts
CHANGED
package/dist/cypress.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CheckOptions } from './payload-
|
|
2
|
-
export { s as slugify } from './payload-
|
|
1
|
+
import { C as CheckOptions } from './payload-DzQb84Kw.js';
|
|
2
|
+
export { s as slugify } from './payload-DzQb84Kw.js';
|
|
3
3
|
|
|
4
4
|
interface CypressCheckOptions extends CheckOptions {
|
|
5
5
|
/** Capture a viewport screenshot so the pixel tier can run. On by default — the plugin
|
|
@@ -66,12 +66,22 @@ interface CheckOptions {
|
|
|
66
66
|
* recording. 320px is normative and stays the default; the other two are judgements about
|
|
67
67
|
* YOUR pages, and a screen with 400 focusables should be able to say so rather than being
|
|
68
68
|
* quietly cut off at 120. */
|
|
69
|
-
probes?: boolean | string[] |
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
probes?: boolean | string[] | ProbeTuning;
|
|
70
|
+
}
|
|
71
|
+
/** The bounds a repository may put on the live probes. Stated once, here, because the same
|
|
72
|
+
* list is read from `.ultra11yrc.json`, accepted at the call site and handed to the engine —
|
|
73
|
+
* three copies of a field list is three chances for one of them to fall behind.
|
|
74
|
+
*
|
|
75
|
+
* `budgetMs` and `actionTimeoutMs` are not tuning in the same sense as the others: they are
|
|
76
|
+
* what stops a probe from spending SOMEBODY ELSE'S test timeout. See ProbeLimits. */
|
|
77
|
+
interface ProbeTuning {
|
|
78
|
+
only?: string[];
|
|
79
|
+
reflowWidth?: number;
|
|
80
|
+
maxFocusables?: number;
|
|
81
|
+
maxHits?: number;
|
|
82
|
+
maxTriggers?: number;
|
|
83
|
+
actionTimeoutMs?: number;
|
|
84
|
+
budgetMs?: number;
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
export { type AuditLike as A, type CheckOptions as C, type FindingLike as F, type SnapshotPayload as S, slugify as s };
|
package/dist/playwright.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as CheckOptions, A as AuditLike } from './payload-
|
|
1
|
+
import { C as CheckOptions, A as AuditLike } from './payload-DzQb84Kw.js';
|
|
2
2
|
|
|
3
3
|
interface PlaywrightPage {
|
|
4
4
|
evaluate(script: string): Promise<unknown>;
|
|
@@ -106,6 +106,20 @@ declare function samplePagesFor(opts?: Pick<SweepOptions, "cwd" | "only">): Samp
|
|
|
106
106
|
* `scan`, which needs a real address); a Playwright test wants the path, so its `baseURL`
|
|
107
107
|
* applies. Anything unparseable is already a path. */
|
|
108
108
|
declare function sweepTarget(url: string): string;
|
|
109
|
+
/** The defaults every swept page is checked with, and the one seam a test can reach without
|
|
110
|
+
* a Playwright runtime in the room.
|
|
111
|
+
*
|
|
112
|
+
* `probes: true` is the default HERE and opt-in on `checkA11y`, and the asymmetry is the
|
|
113
|
+
* point. `checkA11y` is called from a test that owns its page and may be asserting on focus
|
|
114
|
+
* or hover a line later; a sweep exists for nothing but recording the sample, and the probes
|
|
115
|
+
* are the only thing that can decide zoom, reflow, text spacing and content-on-hover on
|
|
116
|
+
* screens a cold scanner never reaches. Measured on egapro: 15 of 20 pages came from a sweep
|
|
117
|
+
* that never probed, and because conformity is an AND across every page in scope, those four
|
|
118
|
+
* criteria stayed « à évaluer » for the whole audit — including on the five pages that HAD
|
|
119
|
+
* been probed. Opt out with `check: { probes: false }`. */
|
|
120
|
+
declare function sweepCheckOptions(check?: SweepOptions["check"]): SweepOptions["check"] & {
|
|
121
|
+
failOn: false | undefined;
|
|
122
|
+
};
|
|
109
123
|
declare function sweepSample(opts?: SweepOptions): void;
|
|
110
124
|
|
|
111
|
-
export { AuditLike, CheckOptions, type PlaywrightCheckOptions, type SamplePageLike, type SweepOptions, checkA11y, samplePagesFor, sweepSample, sweepTarget, test };
|
|
125
|
+
export { AuditLike, CheckOptions, type PlaywrightCheckOptions, type SamplePageLike, type SweepOptions, checkA11y, samplePagesFor, sweepCheckOptions, sweepSample, sweepTarget, test };
|
package/dist/playwright.mjs
CHANGED
|
@@ -154,7 +154,22 @@ const __vis = (e) => {
|
|
|
154
154
|
};
|
|
155
155
|
const __html = (e) => (e.outerHTML || '').slice(0, 160);
|
|
156
156
|
`;
|
|
157
|
-
var PROBE_DEFAULTS = {
|
|
157
|
+
var PROBE_DEFAULTS = {
|
|
158
|
+
reflowWidth: 320,
|
|
159
|
+
maxFocusables: 120,
|
|
160
|
+
maxHits: 20,
|
|
161
|
+
maxTriggers: 60,
|
|
162
|
+
actionTimeoutMs: 1e3,
|
|
163
|
+
budgetMs: 2e4
|
|
164
|
+
};
|
|
165
|
+
function probeDeadline(budgetMs, now = Date.now) {
|
|
166
|
+
const end = now() + budgetMs;
|
|
167
|
+
return { out: () => now() >= end, left: () => Math.max(0, end - now()) };
|
|
168
|
+
}
|
|
169
|
+
function actionTimeout(limits, deadline) {
|
|
170
|
+
const left = deadline ? deadline.left() : limits.actionTimeoutMs;
|
|
171
|
+
return Math.max(1, Math.min(limits.actionTimeoutMs, left || limits.actionTimeoutMs));
|
|
172
|
+
}
|
|
158
173
|
var REFLOW_PROBE = `(() => {
|
|
159
174
|
const el = document.scrollingElement || document.documentElement;
|
|
160
175
|
return { horizontalScroll: el.scrollWidth > el.clientWidth + 2 };
|
|
@@ -267,13 +282,14 @@ function hoverVisibleExpr(id, wantHidden = false) {
|
|
|
267
282
|
const j = JSON.stringify(id);
|
|
268
283
|
return `(() => { const t = document.getElementById(${j}); if (!t) return ${wantHidden ? "true" : "false"}; const s = getComputedStyle(t); const shown = s.display !== 'none' && s.visibility !== 'hidden' && t.getBoundingClientRect().height > 0; return ${wantHidden ? "!shown" : "shown"}; })()`;
|
|
269
284
|
}
|
|
270
|
-
async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS) {
|
|
285
|
+
async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, deadline) {
|
|
271
286
|
const count = await page.evaluate(focusSetupExpr(scope, limits.maxFocusables));
|
|
272
287
|
if (!count) return [];
|
|
273
288
|
const hits = [];
|
|
274
289
|
const seen = /* @__PURE__ */ new Set();
|
|
275
290
|
const limit = Math.min(count + 2, limits.maxFocusables + 10);
|
|
276
291
|
for (let i = 0; i < limit; i++) {
|
|
292
|
+
if (deadline?.out()) break;
|
|
277
293
|
await page.keyboard.press("Tab");
|
|
278
294
|
const r = await page.evaluate(FOCUS_CHECK_PROBE);
|
|
279
295
|
if (!r) continue;
|
|
@@ -290,12 +306,13 @@ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS) {
|
|
|
290
306
|
}
|
|
291
307
|
return hits;
|
|
292
308
|
}
|
|
293
|
-
async function probeHover(page, limits = PROBE_DEFAULTS) {
|
|
309
|
+
async function probeHover(page, limits = PROBE_DEFAULTS, deadline) {
|
|
294
310
|
const triggers = await page.evaluate(HOVER_SETUP_PROBE);
|
|
295
311
|
const hits = [];
|
|
296
|
-
for (const tr of triggers) {
|
|
312
|
+
for (const tr of triggers.slice(0, Math.max(1, limits.maxTriggers))) {
|
|
313
|
+
if (deadline?.out()) break;
|
|
297
314
|
try {
|
|
298
|
-
await page.hover(`[data-u11y-h="${tr.key}"]
|
|
315
|
+
await page.hover(`[data-u11y-h="${tr.key}"]`, { timeout: actionTimeout(limits, deadline) });
|
|
299
316
|
} catch {
|
|
300
317
|
continue;
|
|
301
318
|
}
|
|
@@ -322,6 +339,7 @@ async function runLiveProbes(page, opts = {}) {
|
|
|
322
339
|
const limits = { ...PROBE_DEFAULTS, ...opts.limits };
|
|
323
340
|
const only = opts.only?.length ? new Set(opts.only) : null;
|
|
324
341
|
const want = (id) => only === null || only.has(id);
|
|
342
|
+
const deadline = probeDeadline(limits.budgetMs);
|
|
325
343
|
const canResize = typeof page.setViewportSize === "function" && typeof page.viewportSize === "function";
|
|
326
344
|
const canType = !!page.keyboard && typeof page.keyboard.press === "function";
|
|
327
345
|
const canHover = typeof page.hover === "function" && typeof page.waitForTimeout === "function";
|
|
@@ -332,45 +350,53 @@ async function runLiveProbes(page, opts = {}) {
|
|
|
332
350
|
const skip = (sc, why) => {
|
|
333
351
|
out.skipped?.push({ sc, why });
|
|
334
352
|
};
|
|
353
|
+
const bounded = async (sc, run) => {
|
|
354
|
+
if (deadline.out()) {
|
|
355
|
+
skip(sc, `the probe budget of ${limits.budgetMs}ms was already spent when this criterion came up`);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
let timer;
|
|
359
|
+
const CUT = /* @__PURE__ */ Symbol("cut");
|
|
360
|
+
const started = Date.now();
|
|
361
|
+
try {
|
|
362
|
+
const raced = await Promise.race([
|
|
363
|
+
run().catch((e) => {
|
|
364
|
+
throw e;
|
|
365
|
+
}),
|
|
366
|
+
new Promise((resolve) => {
|
|
367
|
+
timer = setTimeout(() => resolve(CUT), Math.max(1, deadline.left()));
|
|
368
|
+
})
|
|
369
|
+
]);
|
|
370
|
+
if (raced === CUT) {
|
|
371
|
+
skip(sc, `the probe budget of ${limits.budgetMs}ms ran out after ${Date.now() - started}ms \u2014 this measurement did not complete`);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
return raced;
|
|
375
|
+
} catch (e) {
|
|
376
|
+
skip(sc, String(e?.message ?? e).slice(0, 160));
|
|
377
|
+
return null;
|
|
378
|
+
} finally {
|
|
379
|
+
if (timer) clearTimeout(timer);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
335
382
|
if (!canResize) skip("1.4.10", "the page object cannot resize its viewport");
|
|
336
383
|
if (!canType) skip("2.4.7", "the page object exposes no keyboard");
|
|
337
384
|
if (!canHover) skip("1.4.13", "the page object cannot hover");
|
|
338
385
|
if (!canStyle) skip("1.4.12", "the page object cannot inject a stylesheet");
|
|
339
|
-
if (want("
|
|
340
|
-
const r = await
|
|
341
|
-
skip("2.4.7", String(e?.message ?? e).slice(0, 160));
|
|
342
|
-
return null;
|
|
343
|
-
});
|
|
386
|
+
if (want("1.4.4")) {
|
|
387
|
+
const r = await bounded("1.4.4", async () => await page.evaluate(REFLOW_ZOOM_PROBE));
|
|
344
388
|
if (r) {
|
|
345
|
-
out.
|
|
346
|
-
out.probed.push("
|
|
389
|
+
out.reflowZoom = r ?? [];
|
|
390
|
+
out.probed.push("1.4.4");
|
|
347
391
|
}
|
|
348
392
|
}
|
|
349
|
-
if (want("1.4.13") && canHover && canType) {
|
|
350
|
-
const r = await probeHover(page, limits).catch((e) => {
|
|
351
|
-
skip("1.4.13", String(e?.message ?? e).slice(0, 160));
|
|
352
|
-
return null;
|
|
353
|
-
});
|
|
354
|
-
if (r) {
|
|
355
|
-
out.hover = r;
|
|
356
|
-
out.probed.push("1.4.13");
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
if (want("1.4.4")) {
|
|
360
|
-
out.reflowZoom = await page.evaluate(REFLOW_ZOOM_PROBE).catch(() => []) ?? [];
|
|
361
|
-
out.probed.push("1.4.4");
|
|
362
|
-
}
|
|
363
393
|
if (want("1.4.10") && canResize) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
skip("1.4.10", String(e?.message ?? e).slice(0, 160));
|
|
394
|
+
const narrowed = await bounded("1.4.10", async () => {
|
|
395
|
+
await page.setViewportSize({ width: limits.reflowWidth, height: restore.height });
|
|
396
|
+
return true;
|
|
368
397
|
});
|
|
369
398
|
if (narrowed) {
|
|
370
|
-
const r = await page.evaluate(REFLOW_PROBE)
|
|
371
|
-
skip("1.4.10", String(e?.message ?? e).slice(0, 160));
|
|
372
|
-
return null;
|
|
373
|
-
});
|
|
399
|
+
const r = await bounded("1.4.10", async () => await page.evaluate(REFLOW_PROBE));
|
|
374
400
|
await page.setViewportSize(restore).catch(() => {
|
|
375
401
|
});
|
|
376
402
|
if (r) {
|
|
@@ -380,15 +406,9 @@ async function runLiveProbes(page, opts = {}) {
|
|
|
380
406
|
}
|
|
381
407
|
}
|
|
382
408
|
if (want("1.4.12") && canStyle) {
|
|
383
|
-
const handle = await page.addStyleTag({ content: TEXT_SPACING_CSS })
|
|
384
|
-
skip("1.4.12", String(e?.message ?? e).slice(0, 160));
|
|
385
|
-
return null;
|
|
386
|
-
});
|
|
409
|
+
const handle = await bounded("1.4.12", () => page.addStyleTag({ content: TEXT_SPACING_CSS }));
|
|
387
410
|
if (handle) {
|
|
388
|
-
const r = await page.evaluate(TEXT_SPACING_PROBE)
|
|
389
|
-
skip("1.4.12", String(e?.message ?? e).slice(0, 160));
|
|
390
|
-
return null;
|
|
391
|
-
});
|
|
411
|
+
const r = await bounded("1.4.12", async () => await page.evaluate(TEXT_SPACING_PROBE));
|
|
392
412
|
await page.evaluate(REMOVE_TEXT_SPACING_STEP).catch(() => {
|
|
393
413
|
});
|
|
394
414
|
if (r) {
|
|
@@ -397,6 +417,20 @@ async function runLiveProbes(page, opts = {}) {
|
|
|
397
417
|
}
|
|
398
418
|
}
|
|
399
419
|
}
|
|
420
|
+
if (want("2.4.7") && canType) {
|
|
421
|
+
const r = await bounded("2.4.7", () => probeFocusVisible(page, "", limits, deadline));
|
|
422
|
+
if (r) {
|
|
423
|
+
out.focusVisible = r;
|
|
424
|
+
out.probed.push("2.4.7");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (want("1.4.13") && canHover && canType) {
|
|
428
|
+
const r = await bounded("1.4.13", () => probeHover(page, limits, deadline));
|
|
429
|
+
if (r) {
|
|
430
|
+
out.hover = r;
|
|
431
|
+
out.probed.push("1.4.13");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
400
434
|
return out;
|
|
401
435
|
}
|
|
402
436
|
var REMOVE_TEXT_SPACING_STEP = `(() => {
|
|
@@ -610,6 +644,9 @@ function sweepTarget(url) {
|
|
|
610
644
|
return url;
|
|
611
645
|
}
|
|
612
646
|
}
|
|
647
|
+
function sweepCheckOptions(check) {
|
|
648
|
+
return { failOn: false, probes: true, ...check };
|
|
649
|
+
}
|
|
613
650
|
function sweepSample(opts = {}) {
|
|
614
651
|
const pages = samplePagesFor(opts);
|
|
615
652
|
const t = test;
|
|
@@ -628,8 +665,7 @@ function sweepSample(opts = {}) {
|
|
|
628
665
|
`${target} answered HTTP ${status} \u2014 an error page at the requested address; nothing to record as "${p.name}"`
|
|
629
666
|
);
|
|
630
667
|
await checkA11y(page, {
|
|
631
|
-
|
|
632
|
-
...opts.check,
|
|
668
|
+
...sweepCheckOptions(opts.check),
|
|
633
669
|
as: p.id,
|
|
634
670
|
name: p.name,
|
|
635
671
|
...p.auth !== void 0 ? { auth: p.auth } : {},
|
|
@@ -643,6 +679,7 @@ function sweepSample(opts = {}) {
|
|
|
643
679
|
export {
|
|
644
680
|
checkA11y,
|
|
645
681
|
samplePagesFor,
|
|
682
|
+
sweepCheckOptions,
|
|
646
683
|
sweepSample,
|
|
647
684
|
sweepTarget,
|
|
648
685
|
test
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|