ultra11y 5.0.1 → 5.1.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.0.1",
13
+ "version": "5.1.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.0.1",
4
+ "version": "5.1.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.0.1",
4
+ "version": "5.1.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.0.1";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.1.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-D0VQYlGS.js';
1
+ import { S as SnapshotPayload, F as FindingLike } from './payload-B6NDkLYJ.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-D0VQYlGS.js';
2
- export { s as slugify } from './payload-D0VQYlGS.js';
1
+ import { C as CheckOptions } from './payload-B6NDkLYJ.js';
2
+ export { s as slugify } from './payload-B6NDkLYJ.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
@@ -120,6 +120,20 @@ function slugify(url) {
120
120
  const slug = path.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
121
121
  return slug || (path === "/" || path === "" ? "accueil" : "page");
122
122
  }
123
+ function stayedOnPage(expected, actual) {
124
+ if (!expected || !actual) return true;
125
+ const path = (u) => {
126
+ try {
127
+ return new URL(u, "http://x.invalid").pathname.replace(/\/+$/, "");
128
+ } catch {
129
+ return void 0;
130
+ }
131
+ };
132
+ const a = path(expected);
133
+ const b = path(actual);
134
+ if (a === void 0 || b === void 0) return true;
135
+ return a === b;
136
+ }
123
137
  function buildPayload(collected, url, runner, opts, screenshot) {
124
138
  const id = opts.as || slugify(url);
125
139
  return {
@@ -151,6 +165,11 @@ function registerUltra11yCommand() {
151
165
  return cy.window({ log: false }).then((win) => {
152
166
  const collected = win.eval(COLLECT_SNAPSHOT);
153
167
  const url = collected.url || win.location.href;
168
+ if (opts.expectPath && !stayedOnPage(opts.expectPath, url)) {
169
+ throw new Error(
170
+ `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.`
171
+ );
172
+ }
154
173
  const payload = {
155
174
  ...buildPayload(collected, url, "cypress", opts),
156
175
  failOn: opts.failOn,
@@ -0,0 +1,51 @@
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
+ /** The path you NAVIGATED to. Given it, the fixture refuses to record a page the browser
43
+ * did not stay on — a guarded route that redirected, a session that expired — instead of
44
+ * filing that other screen under this page's `as`/`name`. Nothing about such a snapshot
45
+ * looks wrong, which is what makes it the worst thing an accessibility report can carry.
46
+ * Compared by path (a query or fragment the app appends to its own route is the same
47
+ * page); a full URL works too. `scan --sample` applies the same rule. */
48
+ expectPath?: string;
49
+ }
50
+
51
+ 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-D0VQYlGS.js';
1
+ import { C as CheckOptions, A as AuditLike } from './payload-B6NDkLYJ.js';
2
2
 
3
3
  interface PlaywrightPage {
4
4
  evaluate(script: string): Promise<unknown>;
@@ -143,6 +143,20 @@ function slugify(url) {
143
143
  const slug = path.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
144
144
  return slug || (path === "/" || path === "" ? "accueil" : "page");
145
145
  }
146
+ function stayedOnPage(expected, actual) {
147
+ if (!expected || !actual) return true;
148
+ const path = (u) => {
149
+ try {
150
+ return new URL(u, "http://x.invalid").pathname.replace(/\/+$/, "");
151
+ } catch {
152
+ return void 0;
153
+ }
154
+ };
155
+ const a = path(expected);
156
+ const b = path(actual);
157
+ if (a === void 0 || b === void 0) return true;
158
+ return a === b;
159
+ }
146
160
  function buildPayload(collected, url, runner, opts, screenshot) {
147
161
  const id = opts.as || slugify(url);
148
162
  return {
@@ -221,6 +235,11 @@ async function checkA11y(page, opts = {}) {
221
235
  }
222
236
  }
223
237
  const url = collected.url || page.url();
238
+ if (opts.expectPath && !stayedOnPage(opts.expectPath, url)) {
239
+ throw new Error(
240
+ `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.`
241
+ );
242
+ }
224
243
  const payload = buildPayload(collected, url, "playwright", opts, shot);
225
244
  const result = auditSnapshot(payload);
226
245
  if (opts.report) writePagesReport(typeof opts.report === "object" ? opts.report : {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.0.1",
3
+ "version": "5.1.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",
@@ -6,7 +6,7 @@ import { join as join50, relative as relative5, resolve as resolve15, 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.0.1";
9
+ var VERSION = "5.1.0";
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.0.1
8
+ version: 5.1.0
9
9
  ---
10
10
 
11
11
  # review-a11y — accessibility review of changed code
@@ -6,7 +6,7 @@ import { join as join50, relative as relative5, resolve as resolve15, 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.0.1";
9
+ var VERSION = "5.1.0";
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.0.1
7
+ version: 5.1.0
8
8
  ---
9
9
 
10
10
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -33,6 +33,27 @@ than restating them, and a test evaluates the generated `failingFindings` alongs
33
33
  one over the same findings. A fixture that gated differently from the published plugin would
34
34
  fail two projects differently while claiming to be the same tool.
35
35
 
36
+
37
+ ## A page the browser did not stay on (`expectPath`)
38
+
39
+ `as`/`name` is the identity the report speaks, and the fixture applies it to whatever is on
40
+ screen. So a route your test could not open — a guarded step, an expired session — gets filed
41
+ under the requested page's name, and its sheet, screenshot and rate all describe another
42
+ screen. Nothing about the document looks wrong.
43
+
44
+ Pass the path you navigated to and the fixture refuses instead:
45
+
46
+ ```js
47
+ await checkA11y(page, { as: "declaration-etape-4", expectPath: "/declaration/etape/4" });
48
+ // throws: /declaration/etape/4 landed on /declaration/etape/1 — not recording it as
49
+ // "declaration-etape-4". The state that opens this route is not the one the test built.
50
+ ```
51
+
52
+ Opt-in, and path-only: a query or fragment the app appends to its own route is the same page.
53
+ `scan --sample` applies the same rule without being asked, because there the URL *is* the
54
+ instruction. A page reported missing is a bug in your seeding; a page reported under the wrong
55
+ name is a false conformance claim.
56
+
36
57
  ## Playwright
37
58
 
38
59
  ```js
@@ -6,7 +6,7 @@ import { join as join50, relative as relative5, resolve as resolve15, 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.0.1";
9
+ var VERSION = "5.1.0";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -10,7 +10,7 @@
10
10
  // must be wired in `cypress.config.js` too — Cypress test code cannot write to disk, so the
11
11
  // collected page round-trips through a task.
12
12
  import { COLLECT_SNAPSHOT } from "../collector.js";
13
- import { type CheckOptions, buildPayload, slugify } from "./payload.js";
13
+ import { type CheckOptions, buildPayload, slugify, stayedOnPage } from "./payload.js";
14
14
 
15
15
  // Cypress's globals are not a dependency of this package; they exist at run time only.
16
16
  declare const Cypress: {
@@ -45,6 +45,14 @@ export function registerUltra11yCommand(): void {
45
45
  return cy.window({ log: false }).then((win: { eval(s: string): unknown; location: { href: string } }) => {
46
46
  const collected = win.eval(COLLECT_SNAPSHOT) as Parameters<typeof buildPayload>[0];
47
47
  const url = collected.url || win.location.href;
48
+ // Same refusal as the Playwright fixture: a page the browser did not stay on must not
49
+ // be filed under the requested page's identity. See CheckOptions.expectPath.
50
+ if (opts.expectPath && !stayedOnPage(opts.expectPath, url)) {
51
+ throw new Error(
52
+ `ultra11y: ${opts.expectPath} landed on ${url} — not recording it as "${opts.as ?? opts.name ?? "this page"}". ` +
53
+ `The state that opens this route is not the one the test built; seed it first, or drop the page from the sample.`,
54
+ );
55
+ }
48
56
  const payload = {
49
57
  ...buildPayload(collected, url, "cypress", opts),
50
58
  failOn: opts.failOn,
@@ -98,6 +98,30 @@ export interface CheckOptions {
98
98
  notes?: string;
99
99
  /** Capture a viewport screenshot for the pixel tier (Playwright: on by default). */
100
100
  screenshot?: boolean;
101
+ /** The path you NAVIGATED to. Given it, the fixture refuses to record a page the browser
102
+ * did not stay on — a guarded route that redirected, a session that expired — instead of
103
+ * filing that other screen under this page's `as`/`name`. Nothing about such a snapshot
104
+ * looks wrong, which is what makes it the worst thing an accessibility report can carry.
105
+ * Compared by path (a query or fragment the app appends to its own route is the same
106
+ * page); a full URL works too. `scan --sample` applies the same rule. */
107
+ expectPath?: string;
108
+ }
109
+
110
+ /** Did the browser stay on the page the caller asked for? Path-only, trailing slash folded;
111
+ * anything unparseable passes — this catches a redirect, it does not invent one. */
112
+ export function stayedOnPage(expected: string, actual: string): boolean {
113
+ if (!expected || !actual) return true;
114
+ const path = (u: string): string | undefined => {
115
+ try {
116
+ return new URL(u, "http://x.invalid").pathname.replace(/\/+$/, "");
117
+ } catch {
118
+ return undefined;
119
+ }
120
+ };
121
+ const a = path(expected);
122
+ const b = path(actual);
123
+ if (a === undefined || b === undefined) return true;
124
+ return a === b;
101
125
  }
102
126
 
103
127
  /** Build the snapshot payload from a collected page. Shared so every runner stamps the
@@ -18,7 +18,7 @@
18
18
  // criteria without booting the app, and how the report speaks page by page. Commit it.
19
19
  import { createRequire } from "node:module";
20
20
  import { COLLECT_SNAPSHOT } from "../collector.js";
21
- import { type AuditLike, type CheckOptions, auditSnapshot, buildPayload, gate, writePagesReport } from "./core.js";
21
+ import { type AuditLike, type CheckOptions, auditSnapshot, buildPayload, gate, stayedOnPage, writePagesReport } from "./core.js";
22
22
 
23
23
  // Playwright's own types are not a dependency of this package (it is a peer of YOUR repo),
24
24
  // so the page object is structurally typed to exactly what is used.
@@ -52,6 +52,16 @@ export async function checkA11y(page: PlaywrightPage, opts: PlaywrightCheckOptio
52
52
  }
53
53
 
54
54
  const url = collected.url || page.url();
55
+ // Refuse a page the browser did not stay on, when the caller said where it went. The
56
+ // snapshot's identity is `as`/`name`, applied to whatever is on screen — so a guarded route
57
+ // that redirected would be filed under the requested page's name, and the resulting sheet,
58
+ // screenshot and rate would all describe another screen. Skipping loudly beats that.
59
+ if (opts.expectPath && !stayedOnPage(opts.expectPath, url)) {
60
+ throw new Error(
61
+ `ultra11y: ${opts.expectPath} landed on ${url} — not recording it as "${opts.as ?? opts.name ?? "this page"}". ` +
62
+ `The state that opens this route is not the one the test built; seed it first, or drop the page from the sample.`,
63
+ );
64
+ }
55
65
  const payload = buildPayload(collected, url, "playwright", opts, shot);
56
66
  const result = auditSnapshot(payload);
57
67
  if (opts.report) writePagesReport(typeof opts.report === "object" ? opts.report : {});
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.0.1";
3
+ export const VERSION = "5.1.0";
4
4
  export const SCHEMA_VERSION = 2;
5
5
 
6
6
  // Lang = the UI FRAME's language — the `L` localization tables each module (report,