ultra11y 5.5.4 → 5.6.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.5.4",
13
+ "version": "5.6.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.5.4",
4
+ "version": "5.6.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.5.4",
4
+ "version": "5.6.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.5.4";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.6.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-B6NDkLYJ.js';
1
+ import { S as SnapshotPayload, F as FindingLike } from './payload-BshGXnOO.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-B6NDkLYJ.js';
2
- export { s as slugify } from './payload-B6NDkLYJ.js';
1
+ import { C as CheckOptions } from './payload-BshGXnOO.js';
2
+ export { s as slugify } from './payload-BshGXnOO.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
package/dist/cypress.mjs CHANGED
@@ -155,7 +155,7 @@ function stayedOnPage(expected, actual) {
155
155
  if (a === void 0 || b === void 0) return true;
156
156
  return a === b;
157
157
  }
158
- function buildPayload(collected, url, runner, opts, screenshot) {
158
+ function buildPayload(collected, url, runner, opts, screenshot, probes) {
159
159
  const id = opts.as || slugify(url);
160
160
  return {
161
161
  meta: {
@@ -174,7 +174,11 @@ function buildPayload(collected, url, runner, opts, screenshot) {
174
174
  styles: collected.styles,
175
175
  boxes: collected.boxes,
176
176
  css: collected.css,
177
- ...screenshot ? { screenshot } : {}
177
+ ...screenshot ? { screenshot } : {},
178
+ // What the live probes measured, when the caller asked for them. It rides in the payload
179
+ // because `snapshot write` persists it beside the DOM — the audit that folds it runs later
180
+ // and in another process, so a measurement kept in memory decides nothing.
181
+ ...probes ? { probes } : {}
178
182
  };
179
183
  }
180
184
 
@@ -0,0 +1,68 @@
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
+ /** What the live probes measured, when the caller ran them. */
25
+ probes?: unknown;
26
+ }
27
+ /** Same slug rule as the engine (src/snapshot.ts slugifyPageId): accent-folded URL path. */
28
+ declare function slugify(url: string): string;
29
+ interface CheckOptions {
30
+ /** The page id (directory name). Defaults to the slugified URL path. */
31
+ as?: string;
32
+ /** Human page name for the report. Defaults to the document title. */
33
+ name?: string;
34
+ /** Severity at which the check fails, or `false` to record without ever failing. */
35
+ failOn?: "blocking" | "major" | "minor" | false;
36
+ /** Mark the page as sitting behind authentication. */
37
+ auth?: boolean;
38
+ /** Source files that rendered it — findings are then reported against YOUR code. */
39
+ sources?: string[];
40
+ /** Reproduction notes carried into the auditor ticket. */
41
+ notes?: string;
42
+ /** Capture a viewport screenshot for the pixel tier (Playwright: on by default). */
43
+ screenshot?: boolean;
44
+ /** The path you NAVIGATED to. Given it, the fixture refuses to record a page the browser
45
+ * did not stay on — a guarded route that redirected, a session that expired — instead of
46
+ * filing that other screen under this page's `as`/`name`. Nothing about such a snapshot
47
+ * looks wrong, which is what makes it the worst thing an accessibility report can carry.
48
+ * Compared by path (a query or fragment the app appends to its own route is the same
49
+ * page); a full URL works too. `scan --sample` applies the same rule. */
50
+ expectPath?: string;
51
+ /** Run the LIVE PROBES on this page after collecting it: 200% zoom, 320px reflow, the
52
+ * text-spacing override, focus visibility and content-on-hover.
53
+ *
54
+ * These are the measurements a recorded snapshot can never settle, because they are
55
+ * properties of a page being acted on rather than of a page as it stands. `scan` runs them
56
+ * in its own browser, which is exactly why they were undecidable on any screen behind a
57
+ * login or a state machine — the scanner arrives cold and gets a redirect. Your suite has
58
+ * the page in the right state; this is how it says so.
59
+ *
60
+ * Off by default: the probes press Tab, hover, resize the viewport and inject a stylesheet,
61
+ * and a suite should opt into that. Everything is restored before it returns, and the
62
+ * snapshot has already been collected, so what they measure cannot alter what was recorded.
63
+ * Pass an array to run only some of them (by criterion: "1.4.4", "1.4.10", "1.4.12",
64
+ * "1.4.13", "2.4.7"). */
65
+ probes?: boolean | string[];
66
+ }
67
+
68
+ 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-B6NDkLYJ.js';
1
+ import { C as CheckOptions, A as AuditLike } from './payload-BshGXnOO.js';
2
2
 
3
3
  interface PlaywrightPage {
4
4
  evaluate(script: string): Promise<unknown>;
@@ -6,6 +6,25 @@ interface PlaywrightPage {
6
6
  fullPage: boolean;
7
7
  }): Promise<Buffer>;
8
8
  url(): string;
9
+ viewportSize?(): {
10
+ width: number;
11
+ height: number;
12
+ } | null;
13
+ setViewportSize?(size: {
14
+ width: number;
15
+ height: number;
16
+ }): Promise<void>;
17
+ addStyleTag?(opts: {
18
+ content: string;
19
+ }): Promise<unknown>;
20
+ hover?(selector: string): Promise<void>;
21
+ waitForTimeout?(ms: number): Promise<void>;
22
+ keyboard?: {
23
+ press(key: string): Promise<void>;
24
+ };
25
+ mouse?: {
26
+ move(x: number, y: number): Promise<void>;
27
+ };
9
28
  }
10
29
  interface PlaywrightCheckOptions extends CheckOptions {
11
30
  /** Also write the per-page report once this page is recorded. Off by default: a report
@@ -132,6 +132,236 @@ var COLLECT_SNAPSHOT = `(() => {
132
132
  };
133
133
  })()`;
134
134
 
135
+ // src/probes.ts
136
+ var PRELUDE = `
137
+ const __sel = (e) => {
138
+ if (!e || !e.tagName) return '\u2014';
139
+ const t = e.tagName.toLowerCase();
140
+ if (e.id) return t + '#' + e.id;
141
+ const c = typeof e.className === 'string' ? e.className.trim().split(/\\s+/)[0] : '';
142
+ return c ? t + '.' + c : t;
143
+ };
144
+ const __vis = (e) => {
145
+ const r = e.getBoundingClientRect();
146
+ if (r.width <= 4 || r.height <= 4) return false; // tiny / 1px sr-only boxes
147
+ const s = getComputedStyle(e);
148
+ if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return false;
149
+ // visually-hidden "screen-reader-only" pattern (clip rect / clip-path inset) \u2014 present in
150
+ // the a11y tree but not painted; must not be measured for clipping/target-size.
151
+ if (s.clip && s.clip !== 'auto' && s.clip !== 'rect(auto, auto, auto, auto)') return false;
152
+ if (s.clipPath && (s.clipPath.indexOf('inset(100%') >= 0 || s.clipPath.indexOf('inset(50%') >= 0)) return false;
153
+ return true;
154
+ };
155
+ const __html = (e) => (e.outerHTML || '').slice(0, 160);
156
+ `;
157
+ var REFLOW_PROBE = `(() => {
158
+ const el = document.scrollingElement || document.documentElement;
159
+ return { horizontalScroll: el.scrollWidth > el.clientWidth + 2 };
160
+ })()`;
161
+ var REFLOW_ZOOM_PROBE = `(() => { ${PRELUDE}
162
+ const root = document.documentElement;
163
+ const prev = root.style.fontSize;
164
+ root.style.fontSize = '200%';
165
+ const hits = [];
166
+ for (const e of Array.from(document.querySelectorAll('p,li,h1,h2,h3,h4,h5,h6,td,th,button,a,label,span'))) {
167
+ if (!__vis(e)) continue;
168
+ if ((e.textContent || '').trim().length < 8) continue;
169
+ const s = getComputedStyle(e);
170
+ const clip = s.overflow === 'hidden' || s.overflowY === 'hidden' || s.overflowX === 'hidden';
171
+ const noWrap = s.whiteSpace === 'nowrap' || s.textOverflow === 'ellipsis';
172
+ if ((clip || noWrap) && (e.scrollHeight > e.clientHeight + 6 || e.scrollWidth > e.clientWidth + 6)) {
173
+ hits.push({ selector: __sel(e), html: __html(e), detail: 'Texte tronqu\xE9/masqu\xE9 \xE0 200% (conteneur overflow:hidden / nowrap) \u2014 perte de contenu au zoom (1.4.4).' });
174
+ }
175
+ if (hits.length >= 12) break;
176
+ }
177
+ root.style.fontSize = prev;
178
+ return hits;
179
+ })()`;
180
+ var TEXT_SPACING_CSS = "* { line-height: 1.5 !important; letter-spacing: 0.12em !important; word-spacing: 0.16em !important; } p { margin-bottom: 2em !important; }";
181
+ var TEXT_SPACING_PROBE = `(() => { ${PRELUDE}
182
+ const hits = [];
183
+ for (const e of Array.from(document.querySelectorAll('p,li,span,a,button,h1,h2,h3,h4,h5,h6,td,th,label,div'))) {
184
+ if (!__vis(e)) continue;
185
+ if ((e.textContent || '').trim().length < 8) continue;
186
+ const s = getComputedStyle(e);
187
+ const clipped = (s.overflowX === 'hidden' || s.overflowY === 'hidden' || s.overflow === 'hidden') && (e.scrollHeight > e.clientHeight + 2 || e.scrollWidth > e.clientWidth + 2);
188
+ const ellipsis = s.textOverflow === 'ellipsis' && e.scrollWidth > e.clientWidth + 2;
189
+ if (clipped || ellipsis) {
190
+ hits.push({ selector: __sel(e), html: __html(e), detail: 'Texte tronqu\xE9/masqu\xE9 sous l\\'espacement de texte WCAG 1.4.12 \u2014 perte de contenu.' });
191
+ }
192
+ if (hits.length >= 20) break;
193
+ }
194
+ return hits;
195
+ })()`;
196
+ function focusSetupExpr(scope = "") {
197
+ const rootExpr = scope ? `document.querySelectorAll(${JSON.stringify(scope)})` : `[document.documentElement]`;
198
+ return `(() => { ${PRELUDE}
199
+ const sel = 'a[href],button:not([disabled]),input:not([type=hidden]):not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"]),[role=button]:not([disabled])';
200
+ const snap = (e) => { const s = getComputedStyle(e); return [s.outlineStyle, s.outlineWidth, s.outlineColor, s.boxShadow, s.borderColor, s.borderTopWidth, s.borderBottomWidth, s.backgroundColor, s.color, s.textDecorationLine].join('|'); };
201
+ // Visually-hidden radio/checkbox \u2192 measure its visible label/proxy, not the input.
202
+ const proxyFor = (e) => {
203
+ const type = (e.getAttribute('type') || '').toLowerCase();
204
+ const custom = e.tagName === 'INPUT' && (type === 'radio' || type === 'checkbox') && !__vis(e);
205
+ if (!custom) return __vis(e) ? e : null;
206
+ let p = null;
207
+ if (e.id) { try { p = document.querySelector('label[for="' + (window.CSS && CSS.escape ? CSS.escape(e.id) : e.id) + '"]'); } catch (_) {} }
208
+ if (!p) p = e.closest('label');
209
+ if (!p) { const lb = (e.getAttribute('aria-labelledby') || '').split(/\\s+/)[0]; if (lb) p = document.getElementById(lb); }
210
+ return (p && __vis(p)) ? p : null;
211
+ };
212
+ // Fresh authoritative pass: drop any tags a previous (whole-document or dialog) pass left.
213
+ for (const el of Array.from(document.querySelectorAll('[data-u11y-f],[data-u11y-fp]'))) { el.removeAttribute('data-u11y-f'); el.removeAttribute('data-u11y-fp'); }
214
+ const roots = ${rootExpr};
215
+ const focusables = [];
216
+ for (const root of Array.from(roots)) {
217
+ if (root.matches && root.matches(sel)) focusables.push(root);
218
+ for (const e of Array.from(root.querySelectorAll(sel))) focusables.push(e);
219
+ }
220
+ window.__u11yF = {};
221
+ let n = 0;
222
+ for (const e of focusables) {
223
+ const proxy = proxyFor(e);
224
+ if (!proxy) continue;
225
+ const key = 'k' + n;
226
+ e.setAttribute('data-u11y-f', key);
227
+ proxy.setAttribute('data-u11y-fp', key);
228
+ window.__u11yF[key] = { rest: snap(proxy), sel: __sel(proxy), html: __html(proxy) };
229
+ n++;
230
+ if (n >= 120) break;
231
+ }
232
+ return n;
233
+ })()`;
234
+ }
235
+ var FOCUS_CHECK_PROBE = `(() => {
236
+ const e = document.activeElement;
237
+ if (!e || e === document.body || e === document.documentElement) return null;
238
+ const key = e.getAttribute && e.getAttribute('data-u11y-f');
239
+ if (!key || !window.__u11yF || !window.__u11yF[key]) return null;
240
+ const rec = window.__u11yF[key];
241
+ const proxy = document.querySelector('[data-u11y-fp="' + key + '"]') || e;
242
+ const s = getComputedStyle(proxy);
243
+ const now = [s.outlineStyle, s.outlineWidth, s.outlineColor, s.boxShadow, s.borderColor, s.borderTopWidth, s.borderBottomWidth, s.backgroundColor, s.color, s.textDecorationLine].join('|');
244
+ return { key: key, changed: now !== rec.rest, selector: rec.sel, html: rec.html };
245
+ })()`;
246
+ var HOVER_SETUP_PROBE = `(() => { ${PRELUDE}
247
+ const out = [];
248
+ let n = 0;
249
+ for (const e of Array.from(document.querySelectorAll('[aria-describedby]'))) {
250
+ const id = (e.getAttribute('aria-describedby') || '').split(/\\s+/)[0];
251
+ if (!id) continue;
252
+ const t = document.getElementById(id);
253
+ if (!t) continue;
254
+ const s = getComputedStyle(t);
255
+ const hidden = s.display === 'none' || s.visibility === 'hidden' || t.getBoundingClientRect().height === 0;
256
+ if (!hidden) continue;
257
+ const key = 'h' + n;
258
+ e.setAttribute('data-u11y-h', key);
259
+ out.push({ key: key, target: id, selector: __sel(e) });
260
+ n++;
261
+ if (n >= 10) break;
262
+ }
263
+ return out;
264
+ })()`;
265
+ function hoverVisibleExpr(id, wantHidden = false) {
266
+ const j = JSON.stringify(id);
267
+ 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"}; })()`;
268
+ }
269
+ async function probeFocusVisible(page, scope = "") {
270
+ const count = await page.evaluate(focusSetupExpr(scope));
271
+ if (!count) return [];
272
+ const hits = [];
273
+ const seen = /* @__PURE__ */ new Set();
274
+ const limit = Math.min(count + 2, 130);
275
+ for (let i = 0; i < limit; i++) {
276
+ await page.keyboard.press("Tab");
277
+ const r = await page.evaluate(FOCUS_CHECK_PROBE);
278
+ if (!r) continue;
279
+ if (seen.has(r.key)) break;
280
+ seen.add(r.key);
281
+ if (!r.changed) {
282
+ hits.push({
283
+ selector: r.selector,
284
+ html: r.html,
285
+ detail: "Le focus clavier ne produit aucun changement visible (outline/box-shadow/bordure/fond) \u2014 focus non visible (2.4.7)."
286
+ });
287
+ }
288
+ if (hits.length >= 20) break;
289
+ }
290
+ return hits;
291
+ }
292
+ async function probeHover(page) {
293
+ const triggers = await page.evaluate(HOVER_SETUP_PROBE);
294
+ const hits = [];
295
+ for (const tr of triggers) {
296
+ try {
297
+ await page.hover(`[data-u11y-h="${tr.key}"]`);
298
+ } catch {
299
+ continue;
300
+ }
301
+ await page.waitForTimeout(150);
302
+ const shown = await page.evaluate(hoverVisibleExpr(tr.target));
303
+ if (!shown) continue;
304
+ await page.keyboard.press("Escape");
305
+ await page.waitForTimeout(100);
306
+ const dismissed = await page.evaluate(hoverVisibleExpr(tr.target, true));
307
+ await page.mouse.move(2, 2).catch(() => {
308
+ });
309
+ if (!dismissed) {
310
+ hits.push({
311
+ selector: tr.selector,
312
+ html: "",
313
+ detail: `Le contenu r\xE9v\xE9l\xE9 au survol (aria-describedby #${tr.target}) ne se masque pas avec \xC9chap \u2014 Contenu au survol ou au focus (1.4.13).`
314
+ });
315
+ }
316
+ if (hits.length >= 8) break;
317
+ }
318
+ return hits;
319
+ }
320
+ async function runLiveProbes(page, opts = {}) {
321
+ const only = opts.only?.length ? new Set(opts.only) : null;
322
+ const want = (id) => only === null || only.has(id);
323
+ const size = page.viewportSize?.() ?? null;
324
+ 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
+ }
330
+ if (want("1.4.13")) {
331
+ out.hover = await probeHover(page).catch(() => []);
332
+ out.probed.push("1.4.13");
333
+ }
334
+ if (want("1.4.4")) {
335
+ out.reflowZoom = await page.evaluate(REFLOW_ZOOM_PROBE).catch(() => []) ?? [];
336
+ out.probed.push("1.4.4");
337
+ }
338
+ if (want("1.4.10")) {
339
+ await page.setViewportSize({ width: 320, height: restore.height }).catch(() => {
340
+ });
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");
347
+ }
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(() => {
352
+ });
353
+ out.probed.push("1.4.12");
354
+ }
355
+ return out;
356
+ }
357
+ var REMOVE_TEXT_SPACING_STEP = `(() => {
358
+ const sheets = Array.from(document.querySelectorAll('style'));
359
+ for (const s of sheets) {
360
+ if (s.textContent && s.textContent.indexOf('letter-spacing: 0.12em') >= 0) s.remove();
361
+ }
362
+ return true;
363
+ })()`;
364
+
135
365
  // src/integrations/core.ts
136
366
  import { spawnSync } from "child_process";
137
367
  import { createRequire } from "module";
@@ -180,7 +410,7 @@ function stayedOnPage(expected, actual) {
180
410
  if (a === void 0 || b === void 0) return true;
181
411
  return a === b;
182
412
  }
183
- function buildPayload(collected, url, runner, opts, screenshot) {
413
+ function buildPayload(collected, url, runner, opts, screenshot, probes) {
184
414
  const id = opts.as || slugify(url);
185
415
  return {
186
416
  meta: {
@@ -199,7 +429,11 @@ function buildPayload(collected, url, runner, opts, screenshot) {
199
429
  styles: collected.styles,
200
430
  boxes: collected.boxes,
201
431
  css: collected.css,
202
- ...screenshot ? { screenshot } : {}
432
+ ...screenshot ? { screenshot } : {},
433
+ // What the live probes measured, when the caller asked for them. It rides in the payload
434
+ // because `snapshot write` persists it beside the DOM — the audit that folds it runs later
435
+ // and in another process, so a measurement kept in memory decides nothing.
436
+ ...probes ? { probes } : {}
203
437
  };
204
438
  }
205
439
  function gate(result, pageName, failOn) {
@@ -263,7 +497,14 @@ async function checkA11y(page, opts = {}) {
263
497
  `ultra11y: ${opts.expectPath} landed on ${url} \u2014 not recording it as "${opts.as ?? opts.name ?? "this page"}". The state that opens this route is not the one the test built; seed it first, or drop the page from the sample.`
264
498
  );
265
499
  }
266
- const payload = buildPayload(collected, url, "playwright", opts, shot);
500
+ let probes;
501
+ if (opts.probes) {
502
+ try {
503
+ probes = await runLiveProbes(page, Array.isArray(opts.probes) ? { only: opts.probes } : {});
504
+ } catch {
505
+ }
506
+ }
507
+ const payload = buildPayload(collected, url, "playwright", opts, shot, probes);
267
508
  const result = auditSnapshot(payload);
268
509
  if (opts.report) writePagesReport(typeof opts.report === "object" ? opts.report : {});
269
510
  gate(result, String(payload.meta.name), opts.failOn);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.5.4",
3
+ "version": "5.6.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",