ultra11y 5.18.0 → 5.18.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 +33 -8
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +33 -8
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +33 -8
- package/src/crawl.ts +29 -5
- package/src/pages.ts +8 -3
- package/src/scan-local.ts +26 -1
- package/src/types.ts +1 -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": "5.18.
|
|
13
|
+
"version": "5.18.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": "5.18.
|
|
4
|
+
"version": "5.18.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": "5.18.
|
|
4
|
+
"version": "5.18.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 = "5.18.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.18.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": "5.18.
|
|
3
|
+
"version": "5.18.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 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.18.
|
|
9
|
+
var VERSION = "5.18.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -54612,7 +54612,7 @@ function pageGridModel(result, derived, standard, lang) {
|
|
|
54612
54612
|
function renderRedirected(redirected, lang = "en") {
|
|
54613
54613
|
const fr = lang === "fr";
|
|
54614
54614
|
const out2 = [
|
|
54615
|
-
fr ? `> \u26A0\uFE0F **${redirected.length} page(s)
|
|
54615
|
+
fr ? `> \u26A0\uFE0F **${redirected.length} page(s) n'ont pas \xE9t\xE9 enregistr\xE9es** \u2014 le navigateur n'est pas rest\xE9 sur l'adresse demand\xE9e. Les enregistrer aurait d\xE9crit un autre \xE9cran sous le nom demand\xE9. Elles ne comptent donc ni comme conformes ni comme non conformes : elles manquent.` : `> \u26A0\uFE0F **${redirected.length} page(s) were not recorded** \u2014 the browser did not stay on the address asked for. Recording them would have described another screen under the requested name. They count as neither conforming nor non-conforming: they are missing.`,
|
|
54616
54616
|
"",
|
|
54617
54617
|
fr ? "| Page | Demand\xE9 | Atteint | Motif |" : "| Page | Requested | Landed | Reason |",
|
|
54618
54618
|
"| --- | --- | --- | --- |"
|
|
@@ -59019,12 +59019,23 @@ function nameFromUrl(url) {
|
|
|
59019
59019
|
if (!words) return "Accueil";
|
|
59020
59020
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
59021
59021
|
}
|
|
59022
|
+
function canonicalUrl(url) {
|
|
59023
|
+
try {
|
|
59024
|
+
const u = new URL(url);
|
|
59025
|
+
u.hash = "";
|
|
59026
|
+
u.pathname = u.pathname.replace(/(^|\/)index\.x?html?$/i, "$1");
|
|
59027
|
+
return u.href;
|
|
59028
|
+
} catch {
|
|
59029
|
+
return url;
|
|
59030
|
+
}
|
|
59031
|
+
}
|
|
59022
59032
|
async function crawlUrls(start2, opts) {
|
|
59023
59033
|
const depth = opts.depth ?? 1;
|
|
59024
59034
|
const max = opts.max ?? 50;
|
|
59025
59035
|
const order = [];
|
|
59026
|
-
const
|
|
59027
|
-
const
|
|
59036
|
+
const first = canonicalUrl(start2);
|
|
59037
|
+
const seen = /* @__PURE__ */ new Set([first]);
|
|
59038
|
+
const queue = [{ url: first, d: 0 }];
|
|
59028
59039
|
while (queue.length > 0 && order.length < max) {
|
|
59029
59040
|
const { url, d } = queue.shift();
|
|
59030
59041
|
order.push(url);
|
|
@@ -59036,9 +59047,10 @@ async function crawlUrls(start2, opts) {
|
|
|
59036
59047
|
continue;
|
|
59037
59048
|
}
|
|
59038
59049
|
for (const link of extractLinks(html, url)) {
|
|
59039
|
-
|
|
59040
|
-
seen.
|
|
59041
|
-
|
|
59050
|
+
const canon = canonicalUrl(link);
|
|
59051
|
+
if (seen.has(canon)) continue;
|
|
59052
|
+
seen.add(canon);
|
|
59053
|
+
queue.push({ url: canon, d: d + 1 });
|
|
59042
59054
|
}
|
|
59043
59055
|
}
|
|
59044
59056
|
return order;
|
|
@@ -60370,6 +60382,7 @@ async function runScanManyLocal(urls, opts) {
|
|
|
60370
60382
|
const browser = await launchChromium(chromium);
|
|
60371
60383
|
const findings = [];
|
|
60372
60384
|
const snapshots = [];
|
|
60385
|
+
const redirected = [];
|
|
60373
60386
|
try {
|
|
60374
60387
|
for (const url of urls) {
|
|
60375
60388
|
const out2 = await runOnPage(browser, AxeBuilder, url, false, {
|
|
@@ -60379,6 +60392,17 @@ async function runScanManyLocal(urls, opts) {
|
|
|
60379
60392
|
lang,
|
|
60380
60393
|
snapshot: Boolean(opts.snapshotRoot)
|
|
60381
60394
|
});
|
|
60395
|
+
if (out2.httpStatus !== void 0 && out2.httpStatus >= 400) {
|
|
60396
|
+
redirected.push({
|
|
60397
|
+
id: slugifyPageId(url),
|
|
60398
|
+
name: url,
|
|
60399
|
+
requested: url,
|
|
60400
|
+
landed: out2.landedUrl ?? out2.url,
|
|
60401
|
+
reason: "http-status",
|
|
60402
|
+
status: out2.httpStatus
|
|
60403
|
+
});
|
|
60404
|
+
continue;
|
|
60405
|
+
}
|
|
60382
60406
|
findings.push(...toDynamicResult(out2, url, lang, LOCAL_ENGINE).findings);
|
|
60383
60407
|
const id = opts.snapshotRoot ? writeRunnerSnapshot(opts.snapshotRoot, out2, url) : void 0;
|
|
60384
60408
|
if (id) snapshots.push(id);
|
|
@@ -60393,7 +60417,8 @@ async function runScanManyLocal(urls, opts) {
|
|
|
60393
60417
|
date: today(),
|
|
60394
60418
|
findings,
|
|
60395
60419
|
testedScs: localTestedScs(interact),
|
|
60396
|
-
...snapshots.length ? { snapshots } : {}
|
|
60420
|
+
...snapshots.length ? { snapshots } : {},
|
|
60421
|
+
...redirected.length ? { redirected } : {}
|
|
60397
60422
|
};
|
|
60398
60423
|
}
|
|
60399
60424
|
async function runSampleScanLocal(pages, opts) {
|
|
@@ -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.18.
|
|
8
|
+
version: 5.18.2
|
|
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.18.
|
|
9
|
+
var VERSION = "5.18.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -54612,7 +54612,7 @@ function pageGridModel(result, derived, standard, lang) {
|
|
|
54612
54612
|
function renderRedirected(redirected, lang = "en") {
|
|
54613
54613
|
const fr = lang === "fr";
|
|
54614
54614
|
const out2 = [
|
|
54615
|
-
fr ? `> \u26A0\uFE0F **${redirected.length} page(s)
|
|
54615
|
+
fr ? `> \u26A0\uFE0F **${redirected.length} page(s) n'ont pas \xE9t\xE9 enregistr\xE9es** \u2014 le navigateur n'est pas rest\xE9 sur l'adresse demand\xE9e. Les enregistrer aurait d\xE9crit un autre \xE9cran sous le nom demand\xE9. Elles ne comptent donc ni comme conformes ni comme non conformes : elles manquent.` : `> \u26A0\uFE0F **${redirected.length} page(s) were not recorded** \u2014 the browser did not stay on the address asked for. Recording them would have described another screen under the requested name. They count as neither conforming nor non-conforming: they are missing.`,
|
|
54616
54616
|
"",
|
|
54617
54617
|
fr ? "| Page | Demand\xE9 | Atteint | Motif |" : "| Page | Requested | Landed | Reason |",
|
|
54618
54618
|
"| --- | --- | --- | --- |"
|
|
@@ -59019,12 +59019,23 @@ function nameFromUrl(url) {
|
|
|
59019
59019
|
if (!words) return "Accueil";
|
|
59020
59020
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
59021
59021
|
}
|
|
59022
|
+
function canonicalUrl(url) {
|
|
59023
|
+
try {
|
|
59024
|
+
const u = new URL(url);
|
|
59025
|
+
u.hash = "";
|
|
59026
|
+
u.pathname = u.pathname.replace(/(^|\/)index\.x?html?$/i, "$1");
|
|
59027
|
+
return u.href;
|
|
59028
|
+
} catch {
|
|
59029
|
+
return url;
|
|
59030
|
+
}
|
|
59031
|
+
}
|
|
59022
59032
|
async function crawlUrls(start2, opts) {
|
|
59023
59033
|
const depth = opts.depth ?? 1;
|
|
59024
59034
|
const max = opts.max ?? 50;
|
|
59025
59035
|
const order = [];
|
|
59026
|
-
const
|
|
59027
|
-
const
|
|
59036
|
+
const first = canonicalUrl(start2);
|
|
59037
|
+
const seen = /* @__PURE__ */ new Set([first]);
|
|
59038
|
+
const queue = [{ url: first, d: 0 }];
|
|
59028
59039
|
while (queue.length > 0 && order.length < max) {
|
|
59029
59040
|
const { url, d } = queue.shift();
|
|
59030
59041
|
order.push(url);
|
|
@@ -59036,9 +59047,10 @@ async function crawlUrls(start2, opts) {
|
|
|
59036
59047
|
continue;
|
|
59037
59048
|
}
|
|
59038
59049
|
for (const link of extractLinks(html, url)) {
|
|
59039
|
-
|
|
59040
|
-
seen.
|
|
59041
|
-
|
|
59050
|
+
const canon = canonicalUrl(link);
|
|
59051
|
+
if (seen.has(canon)) continue;
|
|
59052
|
+
seen.add(canon);
|
|
59053
|
+
queue.push({ url: canon, d: d + 1 });
|
|
59042
59054
|
}
|
|
59043
59055
|
}
|
|
59044
59056
|
return order;
|
|
@@ -60370,6 +60382,7 @@ async function runScanManyLocal(urls, opts) {
|
|
|
60370
60382
|
const browser = await launchChromium(chromium);
|
|
60371
60383
|
const findings = [];
|
|
60372
60384
|
const snapshots = [];
|
|
60385
|
+
const redirected = [];
|
|
60373
60386
|
try {
|
|
60374
60387
|
for (const url of urls) {
|
|
60375
60388
|
const out2 = await runOnPage(browser, AxeBuilder, url, false, {
|
|
@@ -60379,6 +60392,17 @@ async function runScanManyLocal(urls, opts) {
|
|
|
60379
60392
|
lang,
|
|
60380
60393
|
snapshot: Boolean(opts.snapshotRoot)
|
|
60381
60394
|
});
|
|
60395
|
+
if (out2.httpStatus !== void 0 && out2.httpStatus >= 400) {
|
|
60396
|
+
redirected.push({
|
|
60397
|
+
id: slugifyPageId(url),
|
|
60398
|
+
name: url,
|
|
60399
|
+
requested: url,
|
|
60400
|
+
landed: out2.landedUrl ?? out2.url,
|
|
60401
|
+
reason: "http-status",
|
|
60402
|
+
status: out2.httpStatus
|
|
60403
|
+
});
|
|
60404
|
+
continue;
|
|
60405
|
+
}
|
|
60382
60406
|
findings.push(...toDynamicResult(out2, url, lang, LOCAL_ENGINE).findings);
|
|
60383
60407
|
const id = opts.snapshotRoot ? writeRunnerSnapshot(opts.snapshotRoot, out2, url) : void 0;
|
|
60384
60408
|
if (id) snapshots.push(id);
|
|
@@ -60393,7 +60417,8 @@ async function runScanManyLocal(urls, opts) {
|
|
|
60393
60417
|
date: today(),
|
|
60394
60418
|
findings,
|
|
60395
60419
|
testedScs: localTestedScs(interact),
|
|
60396
|
-
...snapshots.length ? { snapshots } : {}
|
|
60420
|
+
...snapshots.length ? { snapshots } : {},
|
|
60421
|
+
...redirected.length ? { redirected } : {}
|
|
60397
60422
|
};
|
|
60398
60423
|
}
|
|
60399
60424
|
async function runSampleScanLocal(pages, opts) {
|
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: 5.18.
|
|
7
|
+
version: 5.18.2
|
|
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.18.
|
|
9
|
+
var VERSION = "5.18.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -54612,7 +54612,7 @@ function pageGridModel(result, derived, standard, lang) {
|
|
|
54612
54612
|
function renderRedirected(redirected, lang = "en") {
|
|
54613
54613
|
const fr = lang === "fr";
|
|
54614
54614
|
const out2 = [
|
|
54615
|
-
fr ? `> \u26A0\uFE0F **${redirected.length} page(s)
|
|
54615
|
+
fr ? `> \u26A0\uFE0F **${redirected.length} page(s) n'ont pas \xE9t\xE9 enregistr\xE9es** \u2014 le navigateur n'est pas rest\xE9 sur l'adresse demand\xE9e. Les enregistrer aurait d\xE9crit un autre \xE9cran sous le nom demand\xE9. Elles ne comptent donc ni comme conformes ni comme non conformes : elles manquent.` : `> \u26A0\uFE0F **${redirected.length} page(s) were not recorded** \u2014 the browser did not stay on the address asked for. Recording them would have described another screen under the requested name. They count as neither conforming nor non-conforming: they are missing.`,
|
|
54616
54616
|
"",
|
|
54617
54617
|
fr ? "| Page | Demand\xE9 | Atteint | Motif |" : "| Page | Requested | Landed | Reason |",
|
|
54618
54618
|
"| --- | --- | --- | --- |"
|
|
@@ -59019,12 +59019,23 @@ function nameFromUrl(url) {
|
|
|
59019
59019
|
if (!words) return "Accueil";
|
|
59020
59020
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
59021
59021
|
}
|
|
59022
|
+
function canonicalUrl(url) {
|
|
59023
|
+
try {
|
|
59024
|
+
const u = new URL(url);
|
|
59025
|
+
u.hash = "";
|
|
59026
|
+
u.pathname = u.pathname.replace(/(^|\/)index\.x?html?$/i, "$1");
|
|
59027
|
+
return u.href;
|
|
59028
|
+
} catch {
|
|
59029
|
+
return url;
|
|
59030
|
+
}
|
|
59031
|
+
}
|
|
59022
59032
|
async function crawlUrls(start2, opts) {
|
|
59023
59033
|
const depth = opts.depth ?? 1;
|
|
59024
59034
|
const max = opts.max ?? 50;
|
|
59025
59035
|
const order = [];
|
|
59026
|
-
const
|
|
59027
|
-
const
|
|
59036
|
+
const first = canonicalUrl(start2);
|
|
59037
|
+
const seen = /* @__PURE__ */ new Set([first]);
|
|
59038
|
+
const queue = [{ url: first, d: 0 }];
|
|
59028
59039
|
while (queue.length > 0 && order.length < max) {
|
|
59029
59040
|
const { url, d } = queue.shift();
|
|
59030
59041
|
order.push(url);
|
|
@@ -59036,9 +59047,10 @@ async function crawlUrls(start2, opts) {
|
|
|
59036
59047
|
continue;
|
|
59037
59048
|
}
|
|
59038
59049
|
for (const link of extractLinks(html, url)) {
|
|
59039
|
-
|
|
59040
|
-
seen.
|
|
59041
|
-
|
|
59050
|
+
const canon = canonicalUrl(link);
|
|
59051
|
+
if (seen.has(canon)) continue;
|
|
59052
|
+
seen.add(canon);
|
|
59053
|
+
queue.push({ url: canon, d: d + 1 });
|
|
59042
59054
|
}
|
|
59043
59055
|
}
|
|
59044
59056
|
return order;
|
|
@@ -60370,6 +60382,7 @@ async function runScanManyLocal(urls, opts) {
|
|
|
60370
60382
|
const browser = await launchChromium(chromium);
|
|
60371
60383
|
const findings = [];
|
|
60372
60384
|
const snapshots = [];
|
|
60385
|
+
const redirected = [];
|
|
60373
60386
|
try {
|
|
60374
60387
|
for (const url of urls) {
|
|
60375
60388
|
const out2 = await runOnPage(browser, AxeBuilder, url, false, {
|
|
@@ -60379,6 +60392,17 @@ async function runScanManyLocal(urls, opts) {
|
|
|
60379
60392
|
lang,
|
|
60380
60393
|
snapshot: Boolean(opts.snapshotRoot)
|
|
60381
60394
|
});
|
|
60395
|
+
if (out2.httpStatus !== void 0 && out2.httpStatus >= 400) {
|
|
60396
|
+
redirected.push({
|
|
60397
|
+
id: slugifyPageId(url),
|
|
60398
|
+
name: url,
|
|
60399
|
+
requested: url,
|
|
60400
|
+
landed: out2.landedUrl ?? out2.url,
|
|
60401
|
+
reason: "http-status",
|
|
60402
|
+
status: out2.httpStatus
|
|
60403
|
+
});
|
|
60404
|
+
continue;
|
|
60405
|
+
}
|
|
60382
60406
|
findings.push(...toDynamicResult(out2, url, lang, LOCAL_ENGINE).findings);
|
|
60383
60407
|
const id = opts.snapshotRoot ? writeRunnerSnapshot(opts.snapshotRoot, out2, url) : void 0;
|
|
60384
60408
|
if (id) snapshots.push(id);
|
|
@@ -60393,7 +60417,8 @@ async function runScanManyLocal(urls, opts) {
|
|
|
60393
60417
|
date: today(),
|
|
60394
60418
|
findings,
|
|
60395
60419
|
testedScs: localTestedScs(interact),
|
|
60396
|
-
...snapshots.length ? { snapshots } : {}
|
|
60420
|
+
...snapshots.length ? { snapshots } : {},
|
|
60421
|
+
...redirected.length ? { redirected } : {}
|
|
60397
60422
|
};
|
|
60398
60423
|
}
|
|
60399
60424
|
async function runSampleScanLocal(pages, opts) {
|
package/src/crawl.ts
CHANGED
|
@@ -83,6 +83,28 @@ export function nameFromUrl(url: string): string {
|
|
|
83
83
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** `/index.html` and `/` are the same document on every conventional server, and a crawl
|
|
87
|
+
* reaches both: the entry point is `/` while the site's own nav links to `/index.html`.
|
|
88
|
+
* Left distinct they become two pages in the deliverable — the same screen audited twice,
|
|
89
|
+
* two identical columns in the grid, and its criteria weighted double in every per-page
|
|
90
|
+
* aggregate. Measured on a three-page test site: four pages reported, two of them the home
|
|
91
|
+
* page.
|
|
92
|
+
*
|
|
93
|
+
* Only the DIRECTORY-INDEX filenames, and only as a whole path segment. Whatever else a
|
|
94
|
+
* server may alias is its own business: guessing there would merge two pages that really
|
|
95
|
+
* are different, and a page merged away is a page nobody audits — the worse error of the
|
|
96
|
+
* two, and a silent one. */
|
|
97
|
+
export function canonicalUrl(url: string): string {
|
|
98
|
+
try {
|
|
99
|
+
const u = new URL(url);
|
|
100
|
+
u.hash = "";
|
|
101
|
+
u.pathname = u.pathname.replace(/(^|\/)index\.x?html?$/i, "$1");
|
|
102
|
+
return u.href;
|
|
103
|
+
} catch {
|
|
104
|
+
return url; // not a URL — the caller's own string is the best identity available
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
86
108
|
export interface CrawlOpts {
|
|
87
109
|
fetchHtml: (url: string) => Promise<string>;
|
|
88
110
|
depth?: number;
|
|
@@ -95,8 +117,9 @@ export async function crawlUrls(start: string, opts: CrawlOpts): Promise<string[
|
|
|
95
117
|
const depth = opts.depth ?? 1;
|
|
96
118
|
const max = opts.max ?? 50;
|
|
97
119
|
const order: string[] = [];
|
|
98
|
-
const
|
|
99
|
-
const
|
|
120
|
+
const first = canonicalUrl(start);
|
|
121
|
+
const seen = new Set<string>([first]);
|
|
122
|
+
const queue: { url: string; d: number }[] = [{ url: first, d: 0 }];
|
|
100
123
|
|
|
101
124
|
while (queue.length > 0 && order.length < max) {
|
|
102
125
|
const { url, d } = queue.shift()!;
|
|
@@ -109,9 +132,10 @@ export async function crawlUrls(start: string, opts: CrawlOpts): Promise<string[
|
|
|
109
132
|
continue;
|
|
110
133
|
}
|
|
111
134
|
for (const link of extractLinks(html, url)) {
|
|
112
|
-
|
|
113
|
-
seen.
|
|
114
|
-
|
|
135
|
+
const canon = canonicalUrl(link);
|
|
136
|
+
if (seen.has(canon)) continue;
|
|
137
|
+
seen.add(canon);
|
|
138
|
+
queue.push({ url: canon, d: d + 1 });
|
|
115
139
|
}
|
|
116
140
|
}
|
|
117
141
|
return order;
|
package/src/pages.ts
CHANGED
|
@@ -464,13 +464,18 @@ export function pageGridModel(result: AuditResult, derived: PageResult[], standa
|
|
|
464
464
|
* there is re-added to the grid with the same basis as one really visited. Removing it is
|
|
465
465
|
* correct and insufficient: the report would then just be shorter than the sample the
|
|
466
466
|
* project declares, and a silently shorter report reads as a complete one. So the drop is
|
|
467
|
-
* stated — which page, where it went, and why — because that is what makes it fixable.
|
|
467
|
+
* stated — which page, where it went, and why — because that is what makes it fixable.
|
|
468
|
+
*
|
|
469
|
+
* The notice says "page(s)", not "sample page(s)": a crawl refuses pages too — a followed
|
|
470
|
+
* link that answers 404 — and there is no sample behind those. Naming one in an auditor's
|
|
471
|
+
* document that never declared a sample is a small false statement in a document whose
|
|
472
|
+
* whole value is that it makes none. */
|
|
468
473
|
export function renderRedirected(redirected: ScanRedirect[], lang: Lang = "en"): string[] {
|
|
469
474
|
const fr = lang === "fr";
|
|
470
475
|
const out: string[] = [
|
|
471
476
|
fr
|
|
472
|
-
? `> ⚠️ **${redirected.length} page(s)
|
|
473
|
-
: `> ⚠️ **${redirected.length}
|
|
477
|
+
? `> ⚠️ **${redirected.length} page(s) n'ont pas été enregistrées** — le navigateur n'est pas resté sur l'adresse demandée. Les enregistrer aurait décrit un autre écran sous le nom demandé. Elles ne comptent donc ni comme conformes ni comme non conformes : elles manquent.`
|
|
478
|
+
: `> ⚠️ **${redirected.length} page(s) were not recorded** — the browser did not stay on the address asked for. Recording them would have described another screen under the requested name. They count as neither conforming nor non-conforming: they are missing.`,
|
|
474
479
|
"",
|
|
475
480
|
fr ? "| Page | Demandé | Atteint | Motif |" : "| Page | Requested | Landed | Reason |",
|
|
476
481
|
"| --- | --- | --- | --- |",
|
package/src/scan-local.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
TEXT_SPACING_PROBE,
|
|
33
33
|
} from "./probes.js";
|
|
34
34
|
import { sampleScope } from "./sample.js";
|
|
35
|
-
import { COLLECT_SNAPSHOT, type CollectedPage } from "./snapshot.js";
|
|
35
|
+
import { COLLECT_SNAPSHOT, type CollectedPage, slugifyPageId } from "./snapshot.js";
|
|
36
36
|
import { today } from "./util.js";
|
|
37
37
|
|
|
38
38
|
export const LOCAL_ENGINE = "axe-core@playwright (local)";
|
|
@@ -784,6 +784,7 @@ export async function runScanManyLocal(urls: string[], opts: LocalManyOpts): Pro
|
|
|
784
784
|
const browser = await launchChromium(chromium);
|
|
785
785
|
const findings: DynamicFinding[] = [];
|
|
786
786
|
const snapshots: string[] = [];
|
|
787
|
+
const redirected: ScanRedirect[] = [];
|
|
787
788
|
try {
|
|
788
789
|
for (const url of urls) {
|
|
789
790
|
const out = await runOnPage(browser, AxeBuilder, url, false, {
|
|
@@ -793,6 +794,29 @@ export async function runScanManyLocal(urls: string[], opts: LocalManyOpts): Pro
|
|
|
793
794
|
lang,
|
|
794
795
|
snapshot: Boolean(opts.snapshotRoot),
|
|
795
796
|
});
|
|
797
|
+
// A CRAWLED URL THAT ANSWERED AN ERROR IS NOT A PAGE OF THE SITE.
|
|
798
|
+
//
|
|
799
|
+
// The sample scan has refused these since it existed, for the reason its own comment
|
|
800
|
+
// gives: a page filed under a name nobody looked at is a false conformance claim. The
|
|
801
|
+
// URL path never applied the same rule, so a crawl that followed a broken link audited
|
|
802
|
+
// the SERVER'S ERROR PAGE and scored it. Measured on a three-page test site with one
|
|
803
|
+
// dead link: the deliverable carried a fourth page called « Error response » at 97 %
|
|
804
|
+
// RGAA, and its criteria counted towards the run.
|
|
805
|
+
//
|
|
806
|
+
// Refused rather than merely skipped: `renderRedirected` states the drop — which URL,
|
|
807
|
+
// what it answered — because a report that is simply SHORTER reads exactly like a
|
|
808
|
+
// complete one, and a dead link is a fixable bug in the site rather than in the audit.
|
|
809
|
+
if (out.httpStatus !== undefined && out.httpStatus >= 400) {
|
|
810
|
+
redirected.push({
|
|
811
|
+
id: slugifyPageId(url),
|
|
812
|
+
name: url,
|
|
813
|
+
requested: url,
|
|
814
|
+
landed: out.landedUrl ?? out.url,
|
|
815
|
+
reason: "http-status",
|
|
816
|
+
status: out.httpStatus,
|
|
817
|
+
});
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
796
820
|
findings.push(...toDynamicResult(out, url, lang, LOCAL_ENGINE).findings);
|
|
797
821
|
const id = opts.snapshotRoot ? writeRunnerSnapshot(opts.snapshotRoot, out, url) : undefined;
|
|
798
822
|
if (id) snapshots.push(id);
|
|
@@ -808,6 +832,7 @@ export async function runScanManyLocal(urls: string[], opts: LocalManyOpts): Pro
|
|
|
808
832
|
findings,
|
|
809
833
|
testedScs: localTestedScs(interact),
|
|
810
834
|
...(snapshots.length ? { snapshots } : {}),
|
|
835
|
+
...(redirected.length ? { redirected } : {}),
|
|
811
836
|
};
|
|
812
837
|
}
|
|
813
838
|
|
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.18.
|
|
3
|
+
export const VERSION = "5.18.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,
|