ultra11y 5.6.0 → 5.6.1

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.6.0",
13
+ "version": "5.6.1",
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.6.0",
4
+ "version": "5.6.1",
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.6.0",
4
+ "version": "5.6.1",
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.6.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.6.1";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
@@ -320,37 +320,80 @@ async function probeHover(page) {
320
320
  async function runLiveProbes(page, opts = {}) {
321
321
  const only = opts.only?.length ? new Set(opts.only) : null;
322
322
  const want = (id) => only === null || only.has(id);
323
- const size = page.viewportSize?.() ?? null;
323
+ const canResize = typeof page.setViewportSize === "function" && typeof page.viewportSize === "function";
324
+ const canType = !!page.keyboard && typeof page.keyboard.press === "function";
325
+ const canHover = typeof page.hover === "function" && typeof page.waitForTimeout === "function";
326
+ const canStyle = typeof page.addStyleTag === "function";
327
+ const size = canResize ? page.viewportSize() ?? null : null;
324
328
  const restore = size ?? { width: 1280, height: 900 };
325
- const out = { focusVisible: [], hover: [], reflowZoom: [], textSpacing: [], reflow: { horizontalScroll: false }, probed: [] };
326
- if (want("2.4.7")) {
327
- out.focusVisible = await probeFocusVisible(page).catch(() => []);
328
- out.probed.push("2.4.7");
329
+ const out = { focusVisible: [], hover: [], reflowZoom: [], textSpacing: [], reflow: { horizontalScroll: false }, probed: [], skipped: [] };
330
+ const skip = (sc, why) => {
331
+ out.skipped?.push({ sc, why });
332
+ };
333
+ if (!canResize) skip("1.4.10", "the page object cannot resize its viewport");
334
+ if (!canType) skip("2.4.7", "the page object exposes no keyboard");
335
+ if (!canHover) skip("1.4.13", "the page object cannot hover");
336
+ if (!canStyle) skip("1.4.12", "the page object cannot inject a stylesheet");
337
+ if (want("2.4.7") && canType) {
338
+ const r = await probeFocusVisible(page).catch((e) => {
339
+ skip("2.4.7", String(e?.message ?? e).slice(0, 160));
340
+ return null;
341
+ });
342
+ if (r) {
343
+ out.focusVisible = r;
344
+ out.probed.push("2.4.7");
345
+ }
329
346
  }
330
- if (want("1.4.13")) {
331
- out.hover = await probeHover(page).catch(() => []);
332
- out.probed.push("1.4.13");
347
+ if (want("1.4.13") && canHover && canType) {
348
+ const r = await probeHover(page).catch((e) => {
349
+ skip("1.4.13", String(e?.message ?? e).slice(0, 160));
350
+ return null;
351
+ });
352
+ if (r) {
353
+ out.hover = r;
354
+ out.probed.push("1.4.13");
355
+ }
333
356
  }
334
357
  if (want("1.4.4")) {
335
358
  out.reflowZoom = await page.evaluate(REFLOW_ZOOM_PROBE).catch(() => []) ?? [];
336
359
  out.probed.push("1.4.4");
337
360
  }
338
- if (want("1.4.10")) {
339
- await page.setViewportSize({ width: 320, height: restore.height }).catch(() => {
361
+ if (want("1.4.10") && canResize) {
362
+ let narrowed = true;
363
+ await page.setViewportSize({ width: 320, height: restore.height }).catch((e) => {
364
+ narrowed = false;
365
+ skip("1.4.10", String(e?.message ?? e).slice(0, 160));
340
366
  });
341
- out.reflow = await page.evaluate(REFLOW_PROBE).catch(() => ({ horizontalScroll: false })) ?? {
342
- horizontalScroll: false
343
- };
344
- await page.setViewportSize(restore).catch(() => {
345
- });
346
- out.probed.push("1.4.10");
367
+ if (narrowed) {
368
+ const r = await page.evaluate(REFLOW_PROBE).catch((e) => {
369
+ skip("1.4.10", String(e?.message ?? e).slice(0, 160));
370
+ return null;
371
+ });
372
+ await page.setViewportSize(restore).catch(() => {
373
+ });
374
+ if (r) {
375
+ out.reflow = r;
376
+ out.probed.push("1.4.10");
377
+ }
378
+ }
347
379
  }
348
- if (want("1.4.12")) {
349
- const handle = await page.addStyleTag({ content: TEXT_SPACING_CSS }).catch(() => null);
350
- out.textSpacing = await page.evaluate(TEXT_SPACING_PROBE).catch(() => []) ?? [];
351
- if (handle) await page.evaluate(REMOVE_TEXT_SPACING_STEP).catch(() => {
380
+ if (want("1.4.12") && canStyle) {
381
+ const handle = await page.addStyleTag({ content: TEXT_SPACING_CSS }).catch((e) => {
382
+ skip("1.4.12", String(e?.message ?? e).slice(0, 160));
383
+ return null;
352
384
  });
353
- out.probed.push("1.4.12");
385
+ if (handle) {
386
+ const r = await page.evaluate(TEXT_SPACING_PROBE).catch((e) => {
387
+ skip("1.4.12", String(e?.message ?? e).slice(0, 160));
388
+ return null;
389
+ });
390
+ await page.evaluate(REMOVE_TEXT_SPACING_STEP).catch(() => {
391
+ });
392
+ if (r) {
393
+ out.textSpacing = r;
394
+ out.probed.push("1.4.12");
395
+ }
396
+ }
354
397
  }
355
398
  return out;
356
399
  }
@@ -501,7 +544,10 @@ async function checkA11y(page, opts = {}) {
501
544
  if (opts.probes) {
502
545
  try {
503
546
  probes = await runLiveProbes(page, Array.isArray(opts.probes) ? { only: opts.probes } : {});
504
- } catch {
547
+ } catch (e) {
548
+ console.warn(
549
+ `ultra11y: the live probes failed on this page \u2014 ${e instanceof Error ? e.message : String(e)}. The snapshot was still recorded; the criteria they decide stay to assess.`
550
+ );
505
551
  }
506
552
  }
507
553
  const payload = buildPayload(collected, url, "playwright", opts, shot, probes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.6.0",
3
+ "version": "5.6.1",
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",
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.6.0";
9
+ var VERSION = "5.6.1";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -5,7 +5,7 @@ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should
5
5
  allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
6
6
  license: MIT
7
7
  metadata:
8
- version: 5.6.0
8
+ version: 5.6.1
9
9
  ---
10
10
 
11
11
  # review-a11y — accessibility review of changed code
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.6.0";
9
+ var VERSION = "5.6.1";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -4,7 +4,7 @@ description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibili
4
4
  when_to_use: "Invoke when the user asks for an accessibility AUDIT or a formal deliverable: audit a repo, site or page against WCAG 2.2 AA or a country standard (RGAA, Section 508, EN 301 549); produce a dated conformance report, a criterion grid, a PRD backlog, or tickets (GitHub/GitLab/Jira); author accessible markup from scratch; wire the repo gate (init --hook/--ci) or a rendered-DOM capture pipeline. For a review of the code UNDER CHANGE (diff, branch, PR), use the `review-a11y` skill instead."
5
5
  license: MIT
6
6
  metadata:
7
- version: 5.6.0
7
+ version: 5.6.1
8
8
  ---
9
9
 
10
10
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.6.0";
9
+ var VERSION = "5.6.1";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -85,8 +85,15 @@ export async function checkA11y(page: PlaywrightPage, opts: PlaywrightCheckOptio
85
85
  if (opts.probes) {
86
86
  try {
87
87
  probes = await runLiveProbes(page, Array.isArray(opts.probes) ? { only: opts.probes } : {});
88
- } catch {
89
- /* the page is still recorded; the criteria these decide simply stay to assess */
88
+ } catch (e) {
89
+ // LOUD, not silent. A swallowed probe failure is indistinguishable from a page with
90
+ // nothing wrong: the criteria it would have decided simply stay « to assess », run after
91
+ // run, with nobody able to tell whether they were measured and clean or never measured
92
+ // at all. That confusion is the exact failure mode this tool exists to prevent, so it
93
+ // must not be the one it ships. The page is still recorded either way.
94
+ console.warn(
95
+ `ultra11y: the live probes failed on this page — ${e instanceof Error ? e.message : String(e)}. The snapshot was still recorded; the criteria they decide stay to assess.`,
96
+ );
90
97
  }
91
98
  }
92
99
 
package/src/probes.ts CHANGED
@@ -260,38 +260,87 @@ export async function probeHover(page: Any): Promise<ProbeHit[]> {
260
260
  export async function runLiveProbes(page: Any, opts: { only?: string[] } = {}): Promise<LiveProbeResult> {
261
261
  const only = opts.only?.length ? new Set(opts.only) : null;
262
262
  const want = (id: string): boolean => only === null || only.has(id);
263
- const size = (page.viewportSize?.() ?? null) as { width: number; height: number } | null;
263
+ // Capability checks, not assumptions. A page object that cannot resize or press a key is a
264
+ // page whose reflow and focus-visibility simply were not measured — which is a fact to
265
+ // record, not a crash to propagate into somebody's test run.
266
+ const canResize = typeof page.setViewportSize === "function" && typeof page.viewportSize === "function";
267
+ const canType = !!page.keyboard && typeof page.keyboard.press === "function";
268
+ const canHover = typeof page.hover === "function" && typeof page.waitForTimeout === "function";
269
+ const canStyle = typeof page.addStyleTag === "function";
270
+ const size = (canResize ? (page.viewportSize() ?? null) : null) as { width: number; height: number } | null;
264
271
  const restore = size ?? { width: 1280, height: 900 };
265
- const out: LiveProbeResult = { focusVisible: [], hover: [], reflowZoom: [], textSpacing: [], reflow: { horizontalScroll: false }, probed: [] };
272
+ const out: LiveProbeResult = { focusVisible: [], hover: [], reflowZoom: [], textSpacing: [], reflow: { horizontalScroll: false }, probed: [], skipped: [] };
273
+ const skip = (sc: string, why: string): void => {
274
+ out.skipped?.push({ sc, why });
275
+ };
276
+ if (!canResize) skip("1.4.10", "the page object cannot resize its viewport");
277
+ if (!canType) skip("2.4.7", "the page object exposes no keyboard");
278
+ if (!canHover) skip("1.4.13", "the page object cannot hover");
279
+ if (!canStyle) skip("1.4.12", "the page object cannot inject a stylesheet");
266
280
  // Each probe is guarded on its own: one that throws costs its criterion, never the others
267
281
  // and never the caller's test.
268
- if (want("2.4.7")) {
269
- out.focusVisible = await probeFocusVisible(page).catch(() => [] as ProbeHit[]);
270
- out.probed.push("2.4.7");
282
+ if (want("2.4.7") && canType) {
283
+ const r = await probeFocusVisible(page).catch((e: unknown) => {
284
+ skip("2.4.7", String((e as Error)?.message ?? e).slice(0, 160));
285
+ return null;
286
+ });
287
+ if (r) {
288
+ out.focusVisible = r;
289
+ out.probed.push("2.4.7");
290
+ }
271
291
  }
272
- if (want("1.4.13")) {
273
- out.hover = await probeHover(page).catch(() => [] as ProbeHit[]);
274
- out.probed.push("1.4.13");
292
+ if (want("1.4.13") && canHover && canType) {
293
+ const r = await probeHover(page).catch((e: unknown) => {
294
+ skip("1.4.13", String((e as Error)?.message ?? e).slice(0, 160));
295
+ return null;
296
+ });
297
+ if (r) {
298
+ out.hover = r;
299
+ out.probed.push("1.4.13");
300
+ }
275
301
  }
276
302
  if (want("1.4.4")) {
277
303
  out.reflowZoom = ((await page.evaluate(REFLOW_ZOOM_PROBE).catch(() => [])) as ProbeHit[]) ?? [];
278
304
  out.probed.push("1.4.4");
279
305
  }
280
- if (want("1.4.10")) {
281
- await page.setViewportSize({ width: 320, height: restore.height }).catch(() => {});
282
- out.reflow = ((await page.evaluate(REFLOW_PROBE).catch(() => ({ horizontalScroll: false }))) as { horizontalScroll: boolean }) ?? {
283
- horizontalScroll: false,
284
- };
285
- await page.setViewportSize(restore).catch(() => {});
286
- out.probed.push("1.4.10");
306
+ if (want("1.4.10") && canResize) {
307
+ let narrowed = true;
308
+ await page.setViewportSize({ width: 320, height: restore.height }).catch((e: unknown) => {
309
+ narrowed = false;
310
+ skip("1.4.10", String((e as Error)?.message ?? e).slice(0, 160));
311
+ });
312
+ if (narrowed) {
313
+ const r = (await page.evaluate(REFLOW_PROBE).catch((e: unknown) => {
314
+ skip("1.4.10", String((e as Error)?.message ?? e).slice(0, 160));
315
+ return null;
316
+ })) as { horizontalScroll: boolean } | null;
317
+ // The viewport goes back whatever the probe did — the caller's next assertion must not
318
+ // be measuring a 320px page.
319
+ await page.setViewportSize(restore).catch(() => {});
320
+ if (r) {
321
+ out.reflow = r;
322
+ out.probed.push("1.4.10");
323
+ }
324
+ }
287
325
  }
288
- if (want("1.4.12")) {
326
+ if (want("1.4.12") && canStyle) {
289
327
  // Tagged so it can be removed again — an injected stylesheet left behind would change
290
328
  // every measurement the caller makes after this call.
291
- const handle = await page.addStyleTag({ content: TEXT_SPACING_CSS }).catch(() => null);
292
- out.textSpacing = ((await page.evaluate(TEXT_SPACING_PROBE).catch(() => [])) as ProbeHit[]) ?? [];
293
- if (handle) await page.evaluate(REMOVE_TEXT_SPACING_STEP).catch(() => {});
294
- out.probed.push("1.4.12");
329
+ const handle = await page.addStyleTag({ content: TEXT_SPACING_CSS }).catch((e: unknown) => {
330
+ skip("1.4.12", String((e as Error)?.message ?? e).slice(0, 160));
331
+ return null;
332
+ });
333
+ if (handle) {
334
+ const r = (await page.evaluate(TEXT_SPACING_PROBE).catch((e: unknown) => {
335
+ skip("1.4.12", String((e as Error)?.message ?? e).slice(0, 160));
336
+ return null;
337
+ })) as ProbeHit[] | null;
338
+ await page.evaluate(REMOVE_TEXT_SPACING_STEP).catch(() => {});
339
+ if (r) {
340
+ out.textSpacing = r;
341
+ out.probed.push("1.4.12");
342
+ }
343
+ }
295
344
  }
296
345
  return out;
297
346
  }
@@ -317,4 +366,9 @@ export interface LiveProbeResult {
317
366
  /** The success criteria actually probed on THIS page. Absence of a hit only means
318
367
  * something when the probe ran, which is why this travels with the hits. */
319
368
  probed: string[];
369
+ /** Why a probe did not run, when one did not. Recorded rather than dropped: a measurement
370
+ * that vanishes silently is indistinguishable from a page with nothing wrong, and the
371
+ * criteria it would have decided then sit at « to assess » with nobody able to tell which
372
+ * of the two happened. */
373
+ skipped?: { sc: string; why: string }[];
320
374
  }
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Single source of truth for shared types + the embedded version. sync-version.mjs
2
2
  // rewrites VERSION at release time (kept in lockstep with package.json + SKILL.md).
3
- export const VERSION = "5.6.0";
3
+ export const VERSION = "5.6.1";
4
4
  export const SCHEMA_VERSION = 2;
5
5
 
6
6
  // Lang = the UI FRAME's language — the `L` localization tables each module (report,