ultra11y 4.5.0 → 4.5.2
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/package.json +1 -1
- package/scripts/ultra11y.mjs +31 -3
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +31 -3
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/references/pages.md +11 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +31 -3
- package/src/cli.ts +29 -2
- package/src/scan-local.ts +42 -1
- package/src/types.ts +16 -1
|
@@ -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": "4.5.
|
|
13
|
+
"version": "4.5.2",
|
|
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": "4.5.
|
|
4
|
+
"version": "4.5.2",
|
|
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": "4.5.
|
|
4
|
+
"version": "4.5.2",
|
|
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 = "4.5.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "4.5.2";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.2",
|
|
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",
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -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 = "4.5.
|
|
9
|
+
var VERSION = "4.5.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -58352,6 +58352,20 @@ var LIVE_REGION_DETAIL = {
|
|
|
58352
58352
|
function clicksAllowed(storageState, interactClicks) {
|
|
58353
58353
|
return interactClicks === true || !storageState;
|
|
58354
58354
|
}
|
|
58355
|
+
function landedOnRequestedPage(requested, landed) {
|
|
58356
|
+
if (!landed || requested === landed) return true;
|
|
58357
|
+
const path = (u) => {
|
|
58358
|
+
try {
|
|
58359
|
+
return new URL(u, "http://x.invalid").pathname.replace(/\/+$/, "");
|
|
58360
|
+
} catch {
|
|
58361
|
+
return void 0;
|
|
58362
|
+
}
|
|
58363
|
+
};
|
|
58364
|
+
const a = path(requested);
|
|
58365
|
+
const b = path(landed);
|
|
58366
|
+
if (a === void 0 || b === void 0) return true;
|
|
58367
|
+
return a === b;
|
|
58368
|
+
}
|
|
58355
58369
|
async function probeLiveRegion(page, lang, allowClicks) {
|
|
58356
58370
|
const detail = LIVE_REGION_DETAIL[lang] ?? LIVE_REGION_DETAIL.en;
|
|
58357
58371
|
return await page.evaluate(liveRegionExpr(detail, allowClicks)).catch(() => []);
|
|
@@ -58520,6 +58534,7 @@ async function runSampleScanLocal(pages, opts) {
|
|
|
58520
58534
|
const browser = await launchChromium(chromium);
|
|
58521
58535
|
const findings = [];
|
|
58522
58536
|
const snapshots = [];
|
|
58537
|
+
const redirected = [];
|
|
58523
58538
|
try {
|
|
58524
58539
|
for (const page of pages) {
|
|
58525
58540
|
const storageState = page.storageState ?? opts.storageState;
|
|
@@ -58531,6 +58546,10 @@ async function runSampleScanLocal(pages, opts) {
|
|
|
58531
58546
|
lang,
|
|
58532
58547
|
snapshot: Boolean(opts.snapshotRoot)
|
|
58533
58548
|
});
|
|
58549
|
+
if (!landedOnRequestedPage(page.url, out2.url)) {
|
|
58550
|
+
redirected.push({ id: page.id, name: page.name, requested: page.url, landed: out2.url });
|
|
58551
|
+
continue;
|
|
58552
|
+
}
|
|
58534
58553
|
findings.push(...tagSampleFindings(toDynamicResult(out2, page.url, lang, LOCAL_ENGINE).findings, page));
|
|
58535
58554
|
const id = opts.snapshotRoot ? writeRunnerSnapshot(opts.snapshotRoot, out2, page.url, page) : void 0;
|
|
58536
58555
|
if (id) snapshots.push(id);
|
|
@@ -58546,7 +58565,8 @@ async function runSampleScanLocal(pages, opts) {
|
|
|
58546
58565
|
findings,
|
|
58547
58566
|
sample: sampleScope({ pages }),
|
|
58548
58567
|
testedScs: localTestedScs(interact),
|
|
58549
|
-
...snapshots.length ? { snapshots } : {}
|
|
58568
|
+
...snapshots.length ? { snapshots } : {},
|
|
58569
|
+
...redirected.length ? { redirected } : {}
|
|
58550
58570
|
};
|
|
58551
58571
|
}
|
|
58552
58572
|
async function runCrawlScanLocal(opts) {
|
|
@@ -66615,8 +66635,9 @@ async function cmdScan(p) {
|
|
|
66615
66635
|
}
|
|
66616
66636
|
sampleConfig = v.sample;
|
|
66617
66637
|
if (!useLocal && sampleConfig.pages.some((pg) => pg.storageState)) {
|
|
66638
|
+
const forced = typeof p.flags.runtime === "string" && p.flags.runtime === "docker";
|
|
66618
66639
|
console.error(
|
|
66619
|
-
lang === "fr" ?
|
|
66640
|
+
lang === "fr" ? `ultra11y scan : l'\xE9chantillon comporte des pages authentifi\xE9es (storageState), non prises en charge par le runtime Docker.${forced ? " Retirez --runtime docker." : " Le runtime local n'a pas pu \xEAtre r\xE9solu : installez @playwright/test et @axe-core/playwright dans le projet audit\xE9 (ou pointez --cwd dessus). Via l'Action, ce sont des d\xE9pendances du d\xE9p\xF4t."}` : `ultra11y scan: the sample has authenticated pages (storageState), unsupported by the Docker runtime.${forced ? " Drop --runtime docker." : " The local runtime could not be resolved: install @playwright/test and @axe-core/playwright in the audited project (or point --cwd at it). Through the Action, those are dependencies of the repository."}`
|
|
66620
66641
|
);
|
|
66621
66642
|
return 2;
|
|
66622
66643
|
}
|
|
@@ -66645,6 +66666,13 @@ async function cmdScan(p) {
|
|
|
66645
66666
|
console.error(`ultra11y scan: ${e instanceof Error ? e.message : String(e)}`);
|
|
66646
66667
|
return 1;
|
|
66647
66668
|
}
|
|
66669
|
+
if (dynamic.redirected?.length) {
|
|
66670
|
+
for (const r of dynamic.redirected) {
|
|
66671
|
+
console.error(
|
|
66672
|
+
lang === "fr" ? `\u26A0\uFE0F ultra11y scan : \xAB ${r.name} \xBB (${r.id}) non enregistr\xE9e \u2014 ${r.requested} a redirig\xE9 vers ${r.landed}. L'enregistrer aurait d\xE9crit cet \xE9cran sous le nom du premier.` : `\u26A0\uFE0F ultra11y scan: "${r.name}" (${r.id}) not recorded \u2014 ${r.requested} redirected to ${r.landed}. Recording it would have described that screen under the first one's name.`
|
|
66673
|
+
);
|
|
66674
|
+
}
|
|
66675
|
+
}
|
|
66648
66676
|
if (useSample && sampleConfig) {
|
|
66649
66677
|
const lintKey = typeof p.flags.standard === "string" && p.flags.standard ? p.flags.standard : "rgaa";
|
|
66650
66678
|
try {
|
|
@@ -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: 4.5.
|
|
8
|
+
version: 4.5.2
|
|
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 = "4.5.
|
|
9
|
+
var VERSION = "4.5.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -58352,6 +58352,20 @@ var LIVE_REGION_DETAIL = {
|
|
|
58352
58352
|
function clicksAllowed(storageState, interactClicks) {
|
|
58353
58353
|
return interactClicks === true || !storageState;
|
|
58354
58354
|
}
|
|
58355
|
+
function landedOnRequestedPage(requested, landed) {
|
|
58356
|
+
if (!landed || requested === landed) return true;
|
|
58357
|
+
const path = (u) => {
|
|
58358
|
+
try {
|
|
58359
|
+
return new URL(u, "http://x.invalid").pathname.replace(/\/+$/, "");
|
|
58360
|
+
} catch {
|
|
58361
|
+
return void 0;
|
|
58362
|
+
}
|
|
58363
|
+
};
|
|
58364
|
+
const a = path(requested);
|
|
58365
|
+
const b = path(landed);
|
|
58366
|
+
if (a === void 0 || b === void 0) return true;
|
|
58367
|
+
return a === b;
|
|
58368
|
+
}
|
|
58355
58369
|
async function probeLiveRegion(page, lang, allowClicks) {
|
|
58356
58370
|
const detail = LIVE_REGION_DETAIL[lang] ?? LIVE_REGION_DETAIL.en;
|
|
58357
58371
|
return await page.evaluate(liveRegionExpr(detail, allowClicks)).catch(() => []);
|
|
@@ -58520,6 +58534,7 @@ async function runSampleScanLocal(pages, opts) {
|
|
|
58520
58534
|
const browser = await launchChromium(chromium);
|
|
58521
58535
|
const findings = [];
|
|
58522
58536
|
const snapshots = [];
|
|
58537
|
+
const redirected = [];
|
|
58523
58538
|
try {
|
|
58524
58539
|
for (const page of pages) {
|
|
58525
58540
|
const storageState = page.storageState ?? opts.storageState;
|
|
@@ -58531,6 +58546,10 @@ async function runSampleScanLocal(pages, opts) {
|
|
|
58531
58546
|
lang,
|
|
58532
58547
|
snapshot: Boolean(opts.snapshotRoot)
|
|
58533
58548
|
});
|
|
58549
|
+
if (!landedOnRequestedPage(page.url, out2.url)) {
|
|
58550
|
+
redirected.push({ id: page.id, name: page.name, requested: page.url, landed: out2.url });
|
|
58551
|
+
continue;
|
|
58552
|
+
}
|
|
58534
58553
|
findings.push(...tagSampleFindings(toDynamicResult(out2, page.url, lang, LOCAL_ENGINE).findings, page));
|
|
58535
58554
|
const id = opts.snapshotRoot ? writeRunnerSnapshot(opts.snapshotRoot, out2, page.url, page) : void 0;
|
|
58536
58555
|
if (id) snapshots.push(id);
|
|
@@ -58546,7 +58565,8 @@ async function runSampleScanLocal(pages, opts) {
|
|
|
58546
58565
|
findings,
|
|
58547
58566
|
sample: sampleScope({ pages }),
|
|
58548
58567
|
testedScs: localTestedScs(interact),
|
|
58549
|
-
...snapshots.length ? { snapshots } : {}
|
|
58568
|
+
...snapshots.length ? { snapshots } : {},
|
|
58569
|
+
...redirected.length ? { redirected } : {}
|
|
58550
58570
|
};
|
|
58551
58571
|
}
|
|
58552
58572
|
async function runCrawlScanLocal(opts) {
|
|
@@ -66615,8 +66635,9 @@ async function cmdScan(p) {
|
|
|
66615
66635
|
}
|
|
66616
66636
|
sampleConfig = v.sample;
|
|
66617
66637
|
if (!useLocal && sampleConfig.pages.some((pg) => pg.storageState)) {
|
|
66638
|
+
const forced = typeof p.flags.runtime === "string" && p.flags.runtime === "docker";
|
|
66618
66639
|
console.error(
|
|
66619
|
-
lang === "fr" ?
|
|
66640
|
+
lang === "fr" ? `ultra11y scan : l'\xE9chantillon comporte des pages authentifi\xE9es (storageState), non prises en charge par le runtime Docker.${forced ? " Retirez --runtime docker." : " Le runtime local n'a pas pu \xEAtre r\xE9solu : installez @playwright/test et @axe-core/playwright dans le projet audit\xE9 (ou pointez --cwd dessus). Via l'Action, ce sont des d\xE9pendances du d\xE9p\xF4t."}` : `ultra11y scan: the sample has authenticated pages (storageState), unsupported by the Docker runtime.${forced ? " Drop --runtime docker." : " The local runtime could not be resolved: install @playwright/test and @axe-core/playwright in the audited project (or point --cwd at it). Through the Action, those are dependencies of the repository."}`
|
|
66620
66641
|
);
|
|
66621
66642
|
return 2;
|
|
66622
66643
|
}
|
|
@@ -66645,6 +66666,13 @@ async function cmdScan(p) {
|
|
|
66645
66666
|
console.error(`ultra11y scan: ${e instanceof Error ? e.message : String(e)}`);
|
|
66646
66667
|
return 1;
|
|
66647
66668
|
}
|
|
66669
|
+
if (dynamic.redirected?.length) {
|
|
66670
|
+
for (const r of dynamic.redirected) {
|
|
66671
|
+
console.error(
|
|
66672
|
+
lang === "fr" ? `\u26A0\uFE0F ultra11y scan : \xAB ${r.name} \xBB (${r.id}) non enregistr\xE9e \u2014 ${r.requested} a redirig\xE9 vers ${r.landed}. L'enregistrer aurait d\xE9crit cet \xE9cran sous le nom du premier.` : `\u26A0\uFE0F ultra11y scan: "${r.name}" (${r.id}) not recorded \u2014 ${r.requested} redirected to ${r.landed}. Recording it would have described that screen under the first one's name.`
|
|
66673
|
+
);
|
|
66674
|
+
}
|
|
66675
|
+
}
|
|
66648
66676
|
if (useSample && sampleConfig) {
|
|
66649
66677
|
const lintKey = typeof p.flags.standard === "string" && p.flags.standard ? p.flags.standard : "rgaa";
|
|
66650
66678
|
try {
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -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: 4.5.
|
|
7
|
+
version: 4.5.2
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -220,7 +220,17 @@ Sheets are named `page-<id>.md`: a page whose id is `index` — the ordinary id
|
|
|
220
220
|
gates that mean something here — above all **no invented criterion** — instead of demanding
|
|
221
221
|
the §1–5 structure of a conformance document.
|
|
222
222
|
|
|
223
|
-
###
|
|
223
|
+
### Four honesty rules
|
|
224
|
+
|
|
225
|
+
**0. A page is recorded only if the browser stayed on it.** A sample page's declared `id` and
|
|
226
|
+
`name` are the identity the report speaks, and they are applied to whatever was on screen. So a
|
|
227
|
+
route that redirects — an expired session bouncing to sign-in, a wizard step the application
|
|
228
|
+
state does not open — would be filed under the requested page's name while showing something
|
|
229
|
+
else: a sheet, a screenshot and a rate that describe a different screen, with nothing about the
|
|
230
|
+
document looking wrong. `scan --sample` compares the landed **path** against the requested one
|
|
231
|
+
(a query string or fragment the app appends is the same page) and **drops** a page that moved,
|
|
232
|
+
naming it and where it went. A page reported missing is a bug in the sample or in the state you
|
|
233
|
+
seeded, and both are fixable; a page reported under the wrong name is a false conformance claim.
|
|
224
234
|
|
|
225
235
|
**1. A finding is attributed to a page only when something says so.** In order: the snapshot
|
|
226
236
|
path it was raised on (`.ultra11y/pages/<id>/dom.html` — the id is *in the path*, which is what
|
|
@@ -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 = "4.5.
|
|
9
|
+
var VERSION = "4.5.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -58352,6 +58352,20 @@ var LIVE_REGION_DETAIL = {
|
|
|
58352
58352
|
function clicksAllowed(storageState, interactClicks) {
|
|
58353
58353
|
return interactClicks === true || !storageState;
|
|
58354
58354
|
}
|
|
58355
|
+
function landedOnRequestedPage(requested, landed) {
|
|
58356
|
+
if (!landed || requested === landed) return true;
|
|
58357
|
+
const path = (u) => {
|
|
58358
|
+
try {
|
|
58359
|
+
return new URL(u, "http://x.invalid").pathname.replace(/\/+$/, "");
|
|
58360
|
+
} catch {
|
|
58361
|
+
return void 0;
|
|
58362
|
+
}
|
|
58363
|
+
};
|
|
58364
|
+
const a = path(requested);
|
|
58365
|
+
const b = path(landed);
|
|
58366
|
+
if (a === void 0 || b === void 0) return true;
|
|
58367
|
+
return a === b;
|
|
58368
|
+
}
|
|
58355
58369
|
async function probeLiveRegion(page, lang, allowClicks) {
|
|
58356
58370
|
const detail = LIVE_REGION_DETAIL[lang] ?? LIVE_REGION_DETAIL.en;
|
|
58357
58371
|
return await page.evaluate(liveRegionExpr(detail, allowClicks)).catch(() => []);
|
|
@@ -58520,6 +58534,7 @@ async function runSampleScanLocal(pages, opts) {
|
|
|
58520
58534
|
const browser = await launchChromium(chromium);
|
|
58521
58535
|
const findings = [];
|
|
58522
58536
|
const snapshots = [];
|
|
58537
|
+
const redirected = [];
|
|
58523
58538
|
try {
|
|
58524
58539
|
for (const page of pages) {
|
|
58525
58540
|
const storageState = page.storageState ?? opts.storageState;
|
|
@@ -58531,6 +58546,10 @@ async function runSampleScanLocal(pages, opts) {
|
|
|
58531
58546
|
lang,
|
|
58532
58547
|
snapshot: Boolean(opts.snapshotRoot)
|
|
58533
58548
|
});
|
|
58549
|
+
if (!landedOnRequestedPage(page.url, out2.url)) {
|
|
58550
|
+
redirected.push({ id: page.id, name: page.name, requested: page.url, landed: out2.url });
|
|
58551
|
+
continue;
|
|
58552
|
+
}
|
|
58534
58553
|
findings.push(...tagSampleFindings(toDynamicResult(out2, page.url, lang, LOCAL_ENGINE).findings, page));
|
|
58535
58554
|
const id = opts.snapshotRoot ? writeRunnerSnapshot(opts.snapshotRoot, out2, page.url, page) : void 0;
|
|
58536
58555
|
if (id) snapshots.push(id);
|
|
@@ -58546,7 +58565,8 @@ async function runSampleScanLocal(pages, opts) {
|
|
|
58546
58565
|
findings,
|
|
58547
58566
|
sample: sampleScope({ pages }),
|
|
58548
58567
|
testedScs: localTestedScs(interact),
|
|
58549
|
-
...snapshots.length ? { snapshots } : {}
|
|
58568
|
+
...snapshots.length ? { snapshots } : {},
|
|
58569
|
+
...redirected.length ? { redirected } : {}
|
|
58550
58570
|
};
|
|
58551
58571
|
}
|
|
58552
58572
|
async function runCrawlScanLocal(opts) {
|
|
@@ -66615,8 +66635,9 @@ async function cmdScan(p) {
|
|
|
66615
66635
|
}
|
|
66616
66636
|
sampleConfig = v.sample;
|
|
66617
66637
|
if (!useLocal && sampleConfig.pages.some((pg) => pg.storageState)) {
|
|
66638
|
+
const forced = typeof p.flags.runtime === "string" && p.flags.runtime === "docker";
|
|
66618
66639
|
console.error(
|
|
66619
|
-
lang === "fr" ?
|
|
66640
|
+
lang === "fr" ? `ultra11y scan : l'\xE9chantillon comporte des pages authentifi\xE9es (storageState), non prises en charge par le runtime Docker.${forced ? " Retirez --runtime docker." : " Le runtime local n'a pas pu \xEAtre r\xE9solu : installez @playwright/test et @axe-core/playwright dans le projet audit\xE9 (ou pointez --cwd dessus). Via l'Action, ce sont des d\xE9pendances du d\xE9p\xF4t."}` : `ultra11y scan: the sample has authenticated pages (storageState), unsupported by the Docker runtime.${forced ? " Drop --runtime docker." : " The local runtime could not be resolved: install @playwright/test and @axe-core/playwright in the audited project (or point --cwd at it). Through the Action, those are dependencies of the repository."}`
|
|
66620
66641
|
);
|
|
66621
66642
|
return 2;
|
|
66622
66643
|
}
|
|
@@ -66645,6 +66666,13 @@ async function cmdScan(p) {
|
|
|
66645
66666
|
console.error(`ultra11y scan: ${e instanceof Error ? e.message : String(e)}`);
|
|
66646
66667
|
return 1;
|
|
66647
66668
|
}
|
|
66669
|
+
if (dynamic.redirected?.length) {
|
|
66670
|
+
for (const r of dynamic.redirected) {
|
|
66671
|
+
console.error(
|
|
66672
|
+
lang === "fr" ? `\u26A0\uFE0F ultra11y scan : \xAB ${r.name} \xBB (${r.id}) non enregistr\xE9e \u2014 ${r.requested} a redirig\xE9 vers ${r.landed}. L'enregistrer aurait d\xE9crit cet \xE9cran sous le nom du premier.` : `\u26A0\uFE0F ultra11y scan: "${r.name}" (${r.id}) not recorded \u2014 ${r.requested} redirected to ${r.landed}. Recording it would have described that screen under the first one's name.`
|
|
66673
|
+
);
|
|
66674
|
+
}
|
|
66675
|
+
}
|
|
66648
66676
|
if (useSample && sampleConfig) {
|
|
66649
66677
|
const lintKey = typeof p.flags.standard === "string" && p.flags.standard ? p.flags.standard : "rgaa";
|
|
66650
66678
|
try {
|
package/src/cli.ts
CHANGED
|
@@ -3004,10 +3004,25 @@ async function cmdScan(p: ParsedArgs): Promise<number> {
|
|
|
3004
3004
|
}
|
|
3005
3005
|
sampleConfig = v.sample;
|
|
3006
3006
|
if (!useLocal && sampleConfig.pages.some((pg) => pg.storageState)) {
|
|
3007
|
+
// `--runtime local` is not always the caller's to pass: inside the GitHub Action the
|
|
3008
|
+
// scan is invoked for them. On `auto` the runtime fell back to Docker because the local
|
|
3009
|
+
// tier could not resolve, so naming the missing dependency is the only actionable half
|
|
3010
|
+
// of this message — "use --runtime local" alone sends an Action user to a flag they
|
|
3011
|
+
// cannot reach, and telling them to force a runtime that still cannot load would
|
|
3012
|
+
// exchange this error for a worse one.
|
|
3013
|
+
const forced = typeof p.flags.runtime === "string" && p.flags.runtime === "docker";
|
|
3007
3014
|
console.error(
|
|
3008
3015
|
lang === "fr"
|
|
3009
|
-
?
|
|
3010
|
-
|
|
3016
|
+
? `ultra11y scan : l'échantillon comporte des pages authentifiées (storageState), non prises en charge par le runtime Docker.${
|
|
3017
|
+
forced
|
|
3018
|
+
? " Retirez --runtime docker."
|
|
3019
|
+
: " Le runtime local n'a pas pu être résolu : installez @playwright/test et @axe-core/playwright dans le projet audité (ou pointez --cwd dessus). Via l'Action, ce sont des dépendances du dépôt."
|
|
3020
|
+
}`
|
|
3021
|
+
: `ultra11y scan: the sample has authenticated pages (storageState), unsupported by the Docker runtime.${
|
|
3022
|
+
forced
|
|
3023
|
+
? " Drop --runtime docker."
|
|
3024
|
+
: " The local runtime could not be resolved: install @playwright/test and @axe-core/playwright in the audited project (or point --cwd at it). Through the Action, those are dependencies of the repository."
|
|
3025
|
+
}`,
|
|
3011
3026
|
);
|
|
3012
3027
|
return 2;
|
|
3013
3028
|
}
|
|
@@ -3044,6 +3059,18 @@ async function cmdScan(p: ParsedArgs): Promise<number> {
|
|
|
3044
3059
|
console.error(`ultra11y scan: ${e instanceof Error ? e.message : String(e)}`);
|
|
3045
3060
|
return 1;
|
|
3046
3061
|
}
|
|
3062
|
+
// Pages the scan refused to file, because the browser ended up on another route. Loud, and
|
|
3063
|
+
// on stderr: a silently shorter sample is indistinguishable from a sample that passed, and
|
|
3064
|
+
// the cause is always fixable — an expired session, or state the wizard step needs.
|
|
3065
|
+
if (dynamic.redirected?.length) {
|
|
3066
|
+
for (const r of dynamic.redirected) {
|
|
3067
|
+
console.error(
|
|
3068
|
+
lang === "fr"
|
|
3069
|
+
? `⚠️ ultra11y scan : « ${r.name} » (${r.id}) non enregistrée — ${r.requested} a redirigé vers ${r.landed}. L'enregistrer aurait décrit cet écran sous le nom du premier.`
|
|
3070
|
+
: `⚠️ ultra11y scan: "${r.name}" (${r.id}) not recorded — ${r.requested} redirected to ${r.landed}. Recording it would have described that screen under the first one's name.`,
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3047
3074
|
// Advisory sample-methodology lint: which normative page KINDS the configured sample
|
|
3048
3075
|
// lacks. Standard-agnostic mechanics + RGAA-specific required kinds; scan has no
|
|
3049
3076
|
// --standard, so we lint against the config's default standard (else RGAA if registered).
|
package/src/scan-local.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync, statSync } from "node:fs";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { resolve } from "node:path";
|
|
14
|
-
import type { DynamicFinding, DynamicResult, Lang, SamplePage } from "./types.js";
|
|
14
|
+
import type { DynamicFinding, DynamicResult, Lang, SamplePage, ScanRedirect } from "./types.js";
|
|
15
15
|
import { type DiscoverOpts, discoverUrls, type ProbeHit, type RunnerOutput, tagSampleFindings, toDynamicResult, writeRunnerSnapshot } from "./scan.js";
|
|
16
16
|
import { sampleScope } from "./sample.js";
|
|
17
17
|
import { COLLECT_SNAPSHOT, type CollectedPage } from "./snapshot.js";
|
|
@@ -604,6 +604,36 @@ export function clicksAllowed(storageState: string | undefined, interactClicks:
|
|
|
604
604
|
return interactClicks === true || !storageState;
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
+
/** Did the browser end up on the page we ASKED for?
|
|
608
|
+
*
|
|
609
|
+
* A sample page's declared id/name is the identity the report and the per-page grid speak,
|
|
610
|
+
* and it is applied to whatever the browser had on screen. So a route that redirects — an
|
|
611
|
+
* expired session bouncing to /login, a wizard step the application state does not open —
|
|
612
|
+
* gets recorded under the requested page's name while showing another screen entirely.
|
|
613
|
+
* Nothing about the resulting document looks wrong, which is what makes it the worst
|
|
614
|
+
* failure mode an accessibility report has: a reader sees a page sheet, a screenshot and a
|
|
615
|
+
* conformance rate, and none of it describes the page named at the top.
|
|
616
|
+
*
|
|
617
|
+
* Only the PATH is compared. A query string or fragment the app appends to its own route is
|
|
618
|
+
* the same page; a different path is a different page. Anything unparseable is treated as a
|
|
619
|
+
* match — this guard exists to catch a redirect, not to invent one. Exported for the
|
|
620
|
+
* browser-free policy test. */
|
|
621
|
+
export function landedOnRequestedPage(requested: string, landed: string): boolean {
|
|
622
|
+
if (!landed || requested === landed) return true;
|
|
623
|
+
const path = (u: string): string | undefined => {
|
|
624
|
+
try {
|
|
625
|
+
// `file://` targets and relative paths never redirect; base makes them parseable.
|
|
626
|
+
return new URL(u, "http://x.invalid").pathname.replace(/\/+$/, "");
|
|
627
|
+
} catch {
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
const a = path(requested);
|
|
632
|
+
const b = path(landed);
|
|
633
|
+
if (a === undefined || b === undefined) return true;
|
|
634
|
+
return a === b;
|
|
635
|
+
}
|
|
636
|
+
|
|
607
637
|
async function probeLiveRegion(page: Any, lang: Lang, allowClicks: boolean): Promise<ProbeHit[]> {
|
|
608
638
|
const detail = LIVE_REGION_DETAIL[lang] ?? LIVE_REGION_DETAIL.en;
|
|
609
639
|
return (await page.evaluate(liveRegionExpr(detail, allowClicks)).catch(() => [])) as ProbeHit[];
|
|
@@ -880,6 +910,7 @@ export async function runSampleScanLocal(pages: SamplePage[], opts: LocalManyOpt
|
|
|
880
910
|
const browser = await launchChromium(chromium);
|
|
881
911
|
const findings: DynamicFinding[] = [];
|
|
882
912
|
const snapshots: string[] = [];
|
|
913
|
+
const redirected: ScanRedirect[] = [];
|
|
883
914
|
try {
|
|
884
915
|
for (const page of pages) {
|
|
885
916
|
const storageState = page.storageState ?? opts.storageState; // per-page override
|
|
@@ -891,6 +922,15 @@ export async function runSampleScanLocal(pages: SamplePage[], opts: LocalManyOpt
|
|
|
891
922
|
lang,
|
|
892
923
|
snapshot: Boolean(opts.snapshotRoot),
|
|
893
924
|
});
|
|
925
|
+
// The declared identity is applied to whatever the browser had on screen, so a page
|
|
926
|
+
// that bounced elsewhere would be filed under the name of the page nobody looked at.
|
|
927
|
+
// Drop it instead, and say which one and where it went: a page reported missing is a
|
|
928
|
+
// bug in the sample or the seeded state, and both are fixable. A page reported under
|
|
929
|
+
// the wrong name is neither — it is a false conformance claim.
|
|
930
|
+
if (!landedOnRequestedPage(page.url, out.url)) {
|
|
931
|
+
redirected.push({ id: page.id, name: page.name, requested: page.url, landed: out.url });
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
894
934
|
findings.push(...tagSampleFindings(toDynamicResult(out, page.url, lang, LOCAL_ENGINE).findings, page));
|
|
895
935
|
// The sample page's declared id/name/auth/notes win over anything derived from the
|
|
896
936
|
// URL: that identity is what the report and the per-page grid speak.
|
|
@@ -909,6 +949,7 @@ export async function runSampleScanLocal(pages: SamplePage[], opts: LocalManyOpt
|
|
|
909
949
|
sample: sampleScope({ pages }),
|
|
910
950
|
testedScs: localTestedScs(interact),
|
|
911
951
|
...(snapshots.length ? { snapshots } : {}),
|
|
952
|
+
...(redirected.length ? { redirected } : {}),
|
|
912
953
|
};
|
|
913
954
|
}
|
|
914
955
|
|
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 = "4.5.
|
|
3
|
+
export const VERSION = "4.5.2";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|
|
@@ -47,6 +47,18 @@ export interface SampleScope {
|
|
|
47
47
|
transverse?: string[];
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// A sample page the scan REFUSED to record, because the browser ended up somewhere else —
|
|
51
|
+
// a session that had expired, a wizard step the application state does not open. Recording
|
|
52
|
+
// it would have filed another screen under this page's name, which reads as a complete
|
|
53
|
+
// report and is a false conformance claim. Reported so the sample or the seeded state can
|
|
54
|
+
// be fixed; never silently dropped.
|
|
55
|
+
export interface ScanRedirect {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
requested: string;
|
|
59
|
+
landed: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
// ---- WCAG 2.2 canonical core (src/data/wcag.json, produced by scripts/build-standards.mjs)
|
|
51
63
|
// The engine's canonical key. Success-criterion ids/titles/levels are derived from the
|
|
52
64
|
// official W3C source (https://github.com/w3c/wcag); rule coverage + automatability are
|
|
@@ -560,4 +572,7 @@ export interface DynamicResult {
|
|
|
560
572
|
// static rules never ran against its DOM, so src/pages.ts keeps it `manual` forever).
|
|
561
573
|
// Empty/absent when snapshotting was off or every collection failed. Optional/additive.
|
|
562
574
|
snapshots?: string[];
|
|
575
|
+
// Sample pages dropped because the browser landed on a different path than the one asked
|
|
576
|
+
// for. Absent when every page was reached. Optional/additive — see ScanRedirect.
|
|
577
|
+
redirected?: ScanRedirect[];
|
|
563
578
|
}
|