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.
@@ -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.7.0",
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.7.0",
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.7.0",
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.7.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.9.0";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
@@ -1,4 +1,4 @@
1
- import { S as SnapshotPayload, F as FindingLike } from './payload-D8dvqLcc.js';
1
+ import { S as SnapshotPayload, F as FindingLike } from './payload-DzQb84Kw.js';
2
2
 
3
3
  type On = (event: string, handler: any) => void;
4
4
  interface TaskPayload extends SnapshotPayload {
package/dist/cypress.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CheckOptions } from './payload-D8dvqLcc.js';
2
- export { s as slugify } from './payload-D8dvqLcc.js';
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
- only?: string[];
71
- reflowWidth?: number;
72
- maxFocusables?: number;
73
- maxHits?: number;
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 };
@@ -1,4 +1,4 @@
1
- import { C as CheckOptions, A as AuditLike } from './payload-D8dvqLcc.js';
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 };
@@ -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 = { reflowWidth: 320, maxFocusables: 120, maxHits: 20 };
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("2.4.7") && canType) {
340
- const r = await probeFocusVisible(page, "", limits).catch((e) => {
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.focusVisible = r;
346
- out.probed.push("2.4.7");
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
- let narrowed = true;
365
- await page.setViewportSize({ width: limits.reflowWidth, height: restore.height }).catch((e) => {
366
- narrowed = false;
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).catch((e) => {
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 }).catch((e) => {
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).catch((e) => {
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
- failOn: false,
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.7.0",
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",