testaro 78.0.8 → 78.4.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.
- package/.gitattributes +94 -0
- package/.github/workflows/ci.yml +58 -0
- package/.github/workflows/publish.yml +49 -0
- package/.github/workflows/typescript.yml +35 -0
- package/AGENTS.md +2 -2
- package/CLAUDE.md +24 -18
- package/CONTAINERS.md +9 -6
- package/CONTRIBUTING.md +29 -3
- package/Dockerfile +1 -1
- package/README.md +73 -19
- package/UPGRADES.md +4 -0
- package/actSpecs-doc.md +13 -1
- package/actSpecs.js +29 -5
- package/call.js +6 -5
- package/docker-compose.yml +8 -2
- package/docs/checkpoint-scanning.md +199 -0
- package/docs/standard-result-outcome.md +165 -0
- package/env.example +44 -10
- package/eslint.config.mjs +101 -0
- package/netWatch.js +372 -222
- package/package.json +46 -31
- package/pour/README.md +47 -0
- package/pour/pour.min.js +8 -0
- package/procs/actDo.js +808 -0
- package/procs/catalog.d.ts +17 -0
- package/procs/catalog.js +312 -221
- package/procs/catalog.ts +398 -0
- package/procs/checkpoint.js +110 -0
- package/procs/config.d.ts +35 -0
- package/procs/config.js +62 -0
- package/procs/dateTime.js +2 -1
- package/procs/doActs.js +442 -849
- package/procs/doTestAct.js +27 -121
- package/procs/flow.js +221 -0
- package/procs/generateRuleRegistry.js +94 -0
- package/procs/getSource.d.ts +26 -0
- package/procs/job.js +115 -1
- package/procs/launch.d.ts +40 -0
- package/procs/launch.js +239 -108
- package/procs/nu.d.ts +48 -0
- package/procs/scope.js +196 -0
- package/procs/shoot.d.ts +33 -0
- package/procs/standard.d.ts +18 -0
- package/procs/standard.js +83 -0
- package/procs/standard.ts +126 -0
- package/procs/testAct.js +145 -0
- package/procs/testaro.d.ts +20 -0
- package/procs/testaro.js +236 -189
- package/procs/testaro.ts +321 -0
- package/procs/userPath.js +126 -0
- package/procs/xPath.d.ts +4 -0
- package/procs/xPath.js +81 -69
- package/procs/xPath.ts +106 -0
- package/procs/xPathScript.d.ts +6 -0
- package/procs/xPathScript.js +70 -0
- package/run.js +3 -2
- package/surea11y/README.md +44 -0
- package/surea11y/surea11y.browser.js +14 -0
- package/testaro/adbID.d.ts +3 -0
- package/testaro/adbID.js +38 -41
- package/testaro/adbID.ts +66 -0
- package/testaro/allCapStyle.d.ts +3 -0
- package/testaro/allCapStyle.js +31 -34
- package/testaro/allCapStyle.ts +55 -0
- package/testaro/allCaps.d.ts +16 -0
- package/testaro/allCaps.js +179 -151
- package/testaro/allCaps.ts +228 -0
- package/testaro/allHidden.d.ts +13 -0
- package/testaro/allHidden.js +29 -30
- package/testaro/allHidden.ts +50 -0
- package/testaro/allSlanted.d.ts +3 -0
- package/testaro/allSlanted.js +30 -33
- package/testaro/allSlanted.ts +54 -0
- package/testaro/altScheme.d.ts +3 -0
- package/testaro/altScheme.js +27 -30
- package/testaro/altScheme.ts +50 -0
- package/testaro/attVal.d.ts +3 -0
- package/testaro/attVal.js +20 -21
- package/testaro/attVal.ts +52 -0
- package/testaro/autocomplete.d.ts +3 -0
- package/testaro/autocomplete.js +59 -75
- package/testaro/autocomplete.ts +101 -0
- package/testaro/bulk.d.ts +13 -0
- package/testaro/bulk.js +32 -33
- package/testaro/bulk.ts +55 -0
- package/testaro/buttonMenu.d.ts +9 -0
- package/testaro/buttonMenu.js +317 -319
- package/testaro/buttonMenu.ts +391 -0
- package/testaro/captionLoc.d.ts +3 -0
- package/testaro/captionLoc.js +17 -20
- package/testaro/captionLoc.ts +40 -0
- package/testaro/datalistRef.d.ts +3 -0
- package/testaro/datalistRef.js +33 -36
- package/testaro/datalistRef.ts +55 -0
- package/testaro/distortion.d.ts +3 -0
- package/testaro/distortion.js +57 -26
- package/testaro/distortion.ts +81 -0
- package/testaro/docType.d.ts +15 -0
- package/testaro/docType.js +25 -25
- package/testaro/docType.ts +45 -0
- package/testaro/dupAtt.d.ts +16 -0
- package/testaro/dupAtt.js +113 -104
- package/testaro/dupAtt.ts +144 -0
- package/testaro/elements.d.ts +6 -0
- package/testaro/elements.js +153 -153
- package/testaro/elements.ts +215 -0
- package/testaro/embAc.d.ts +3 -0
- package/testaro/embAc.js +19 -20
- package/testaro/embAc.ts +40 -0
- package/testaro/focAll.d.ts +13 -0
- package/testaro/focAll.js +185 -191
- package/testaro/focAll.ts +217 -0
- package/testaro/focAndOp.d.ts +3 -0
- package/testaro/focAndOp.js +100 -104
- package/testaro/focAndOp.ts +128 -0
- package/testaro/focInd.d.ts +3 -0
- package/testaro/focInd.js +64 -66
- package/testaro/focInd.ts +96 -0
- package/testaro/focVis.d.ts +3 -0
- package/testaro/focVis.js +28 -30
- package/testaro/focVis.ts +52 -0
- package/testaro/headEl.d.ts +10 -0
- package/testaro/headEl.js +61 -62
- package/testaro/headEl.ts +82 -0
- package/testaro/headingAmb.d.ts +3 -0
- package/testaro/headingAmb.js +54 -63
- package/testaro/headingAmb.ts +72 -0
- package/testaro/hovInd.d.ts +12 -0
- package/testaro/hovInd.js +163 -130
- package/testaro/hovInd.ts +199 -0
- package/testaro/hover.d.ts +3 -0
- package/testaro/hover.js +155 -126
- package/testaro/hover.ts +154 -0
- package/testaro/hr.d.ts +3 -0
- package/testaro/hr.js +14 -17
- package/testaro/hr.ts +36 -0
- package/testaro/imageLink.d.ts +3 -0
- package/testaro/imageLink.js +17 -20
- package/testaro/imageLink.ts +42 -0
- package/testaro/labClash.d.ts +3 -0
- package/testaro/labClash.js +31 -33
- package/testaro/labClash.ts +54 -0
- package/testaro/legendLoc.d.ts +3 -0
- package/testaro/legendLoc.js +17 -20
- package/testaro/legendLoc.ts +42 -0
- package/testaro/lineHeight.d.ts +3 -0
- package/testaro/lineHeight.js +43 -48
- package/testaro/lineHeight.ts +70 -0
- package/testaro/linkAmb.d.ts +7 -0
- package/testaro/linkAmb.js +79 -80
- package/testaro/linkAmb.ts +105 -0
- package/testaro/linkExt.d.ts +3 -0
- package/testaro/linkExt.js +13 -16
- package/testaro/linkExt.ts +35 -0
- package/testaro/linkOldAtt.d.ts +3 -0
- package/testaro/linkOldAtt.js +25 -28
- package/testaro/linkOldAtt.ts +48 -0
- package/testaro/linkTo.d.ts +3 -0
- package/testaro/linkTo.js +22 -22
- package/testaro/linkTo.ts +43 -0
- package/testaro/linkUl.d.ts +3 -0
- package/testaro/linkUl.js +30 -34
- package/testaro/linkUl.ts +54 -0
- package/testaro/miniText.d.ts +3 -0
- package/testaro/miniText.js +41 -44
- package/testaro/miniText.ts +68 -0
- package/testaro/motion.d.ts +10 -0
- package/testaro/motion.js +92 -96
- package/testaro/motion.ts +125 -0
- package/testaro/nonTable.d.ts +3 -0
- package/testaro/nonTable.js +39 -45
- package/testaro/nonTable.ts +66 -0
- package/testaro/optRoleSel.d.ts +3 -0
- package/testaro/optRoleSel.js +16 -19
- package/testaro/optRoleSel.ts +41 -0
- package/testaro/phOnly.d.ts +3 -0
- package/testaro/phOnly.js +18 -21
- package/testaro/phOnly.ts +43 -0
- package/testaro/pseudoP.d.ts +3 -0
- package/testaro/pseudoP.js +36 -38
- package/testaro/pseudoP.ts +59 -0
- package/testaro/radioSet.d.ts +3 -0
- package/testaro/radioSet.js +57 -59
- package/testaro/radioSet.ts +79 -0
- package/testaro/registry.d.ts +63 -0
- package/testaro/registry.js +67 -0
- package/testaro/registry.ts +141 -0
- package/testaro/role.d.ts +3 -0
- package/testaro/role.js +27 -29
- package/testaro/role.ts +53 -0
- package/testaro/secHeading.d.ts +3 -0
- package/testaro/secHeading.js +30 -33
- package/testaro/secHeading.ts +53 -0
- package/testaro/styleDiff.d.ts +25 -0
- package/testaro/styleDiff.js +248 -252
- package/testaro/styleDiff.ts +303 -0
- package/testaro/tabNav.d.ts +33 -0
- package/testaro/tabNav.js +272 -342
- package/testaro/tabNav.ts +454 -0
- package/testaro/targetsNear.d.ts +9 -0
- package/testaro/targetsNear.js +130 -132
- package/testaro/targetsNear.ts +160 -0
- package/testaro/textNodes.d.ts +6 -0
- package/testaro/textNodes.js +139 -135
- package/testaro/textNodes.ts +185 -0
- package/testaro/textSem.d.ts +3 -0
- package/testaro/textSem.js +25 -28
- package/testaro/textSem.ts +47 -0
- package/testaro/title.d.ts +9 -0
- package/testaro/title.js +16 -13
- package/testaro/title.ts +31 -0
- package/testaro/titledEl.d.ts +3 -0
- package/testaro/titledEl.js +15 -18
- package/testaro/titledEl.ts +38 -0
- package/testaro/zIndex.d.ts +3 -0
- package/testaro/zIndex.js +19 -22
- package/testaro/zIndex.ts +42 -0
- package/tests/alfa.d.ts +45 -0
- package/tests/alfa.js +138 -141
- package/tests/alfa.ts +214 -0
- package/tests/aslint.d.ts +33 -0
- package/tests/aslint.js +272 -249
- package/tests/aslint.ts +301 -0
- package/tests/axe.d.ts +27 -0
- package/tests/axe.js +199 -200
- package/tests/axe.ts +277 -0
- package/tests/ed11y.d.ts +28 -0
- package/tests/ed11y.js +141 -99
- package/tests/ed11y.ts +178 -0
- package/tests/htmlcs.d.ts +21 -0
- package/tests/htmlcs.js +174 -140
- package/tests/htmlcs.ts +181 -0
- package/tests/ibm.d.ts +52 -0
- package/tests/ibm.js +164 -166
- package/tests/ibm.ts +251 -0
- package/tests/nuVal.d.ts +13 -0
- package/tests/nuVal.js +107 -112
- package/tests/nuVal.ts +145 -0
- package/tests/nuVnu.d.ts +15 -0
- package/tests/nuVnu.js +141 -111
- package/tests/nuVnu.ts +144 -0
- package/tests/pour.d.ts +31 -0
- package/tests/pour.js +242 -0
- package/tests/pour.ts +273 -0
- package/tests/qualWeb.d.ts +39 -0
- package/tests/qualWeb.js +302 -272
- package/tests/qualWeb.ts +415 -0
- package/tests/surea11y.d.ts +33 -0
- package/tests/surea11y.js +288 -0
- package/tests/surea11y.ts +334 -0
- package/tests/testaro.d.ts +25 -0
- package/tests/testaro.js +745 -652
- package/tests/testaro.ts +862 -0
- package/tests/wave.d.ts +46 -0
- package/tests/wave.js +166 -177
- package/tests/wave.ts +252 -0
- package/tsconfig.json +17 -0
- package/types.d.ts +243 -0
- package/types.js +10 -0
- package/types.ts +376 -0
- package/validation/act/README.md +46 -0
- package/validation/act/capture.js +424 -0
- package/validation/act/chromium-issue-draft.md +66 -0
- package/validation/act/fp-triage-2026-08-22.md +86 -0
- package/validation/act/isolation-notes.md +159 -0
- package/validation/act/playwright-issue-draft.md +73 -0
- package/validation/act/propose-mappings.js +0 -0
- package/validation/act/repro-cdp-raw.js +105 -0
- package/validation/act/repro-metarefresh.js +65 -0
- package/validation/act/score.js +184 -0
- package/validation/act/stage3a-stress-report.md +69 -0
- package/validation/act/stage3b-mapping-proposals.md +94 -0
- package/validation/act/stage3b-triage-draft.md +138 -0
- package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
- package/validation/executors/netWatch.js +180 -90
- package/validation/executors/test.js +17 -2
- package/validation/executors/tests.js +118 -10
- package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
- package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
- package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
- package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
- package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
- package/validation/knownFailures.json +1 -0
- package/validation/tests/jobProperties/adbID.json +27 -2
- package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
- package/validation/tests/jobProperties/allCaps.json +25 -0
- package/validation/tests/jobProperties/allHidden.json +133 -13
- package/validation/tests/jobProperties/allSlanted.json +3 -3
- package/validation/tests/jobProperties/altScheme.json +23 -8
- package/validation/tests/jobProperties/attVal.json +57 -57
- package/validation/tests/jobProperties/autocomplete.json +11 -1
- package/validation/tests/jobProperties/bulk.json +6 -1
- package/validation/tests/jobProperties/buttonMenu.json +63 -42
- package/validation/tests/jobProperties/captionLoc.json +1 -6
- package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
- package/validation/tests/jobProperties/checkpoint-page.json +397 -0
- package/validation/tests/jobProperties/checkpoint.json +395 -0
- package/validation/tests/jobProperties/datalistRef.json +20 -5
- package/validation/tests/jobProperties/distortion.json +28 -3
- package/validation/tests/jobProperties/docType.json +2 -2
- package/validation/tests/jobProperties/dupAtt.json +48 -33
- package/validation/tests/jobProperties/elements.json +28 -28
- package/validation/tests/jobProperties/embAc.json +36 -31
- package/validation/tests/jobProperties/focAndOp.json +284 -0
- package/validation/tests/jobProperties/focInd.json +34 -34
- package/validation/tests/jobProperties/focVis.json +1 -1
- package/validation/tests/jobProperties/hover.json +39 -37
- package/validation/tests/jobProperties/hr.json +13 -3
- package/validation/tests/jobProperties/imageLink.json +0 -5
- package/validation/tests/jobProperties/labClash.json +68 -33
- package/validation/tests/jobProperties/legendLoc.json +1 -6
- package/validation/tests/jobProperties/lineHeight.json +2 -2
- package/validation/tests/jobProperties/linkAmb.json +13 -18
- package/validation/tests/jobProperties/linkExt.json +1 -1
- package/validation/tests/jobProperties/linkOldAtt.json +12 -2
- package/validation/tests/jobProperties/linkTo.json +1 -1
- package/validation/tests/jobProperties/linkUl.json +65 -65
- package/validation/tests/jobProperties/miniText.json +7 -2
- package/validation/tests/jobProperties/motion.json +4 -50
- package/validation/tests/jobProperties/nonTable.json +48 -3
- package/validation/tests/jobProperties/optRoleSel.json +12 -2
- package/validation/tests/jobProperties/phOnly.json +6 -16
- package/validation/tests/jobProperties/pseudoP.json +17 -2
- package/validation/tests/jobProperties/radioSet.json +30 -30
- package/validation/tests/jobProperties/role.json +25 -5
- package/validation/tests/jobProperties/secHeading.json +26 -21
- package/validation/tests/jobProperties/styleDiff.json +35 -5
- package/validation/tests/jobProperties/tabNav.json +3 -1
- package/validation/tests/jobProperties/targetsNear.json +237 -0
- package/validation/tests/jobProperties/textNodes.json +29 -29
- package/validation/tests/jobProperties/textSem.json +26 -1
- package/validation/tests/jobProperties/title.json +12 -2
- package/validation/tests/jobProperties/titledEl.json +39 -9
- package/validation/tests/jobProperties/userPath.json +317 -0
- package/validation/tests/jobProperties/zIndex.json +40 -35
- package/validation/tests/targets/allCapStyle/index.html +26 -0
- package/validation/tests/targets/checkpoint/index.html +38 -0
- package/validation/tests/targets/datalistRef/index.html +1 -1
- package/validation/tests/targets/focAndOp/bad.html +29 -0
- package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
- package/validation/tests/targets/focInd/bad.html +2 -1
- package/validation/tests/targets/headEl/index.html +10 -1
- package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
- package/validation/tests/targets/userPath/index.html +42 -0
- package/validation/validateTest.js +61 -9
- package/.claude/settings.local.json +0 -11
- package/.eslintrc.json +0 -41
- package/htmlcs/.eslintrc.json +0 -67
- package/validation/tests/jobProperties/linkTitle.json +0 -127
- package/validation/tests/jobProperties/opFoc.json +0 -164
- package/validation/tests/jobProperties/targetSmall.json +0 -152
- package/validation/tests/jobProperties/targetTiny.json +0 -142
- package/validation/tests/targets/focOp/bad.html +0 -25
- package/validation/tests/targets/linkTitle/index.html +0 -24
- package/validation/tests/targets/opFoc/bad.html +0 -26
- package/validation/tests/targets/opFoc/good.html +0 -23
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/*
|
|
2
|
+
© 2026 Jeff Witt.
|
|
3
|
+
|
|
4
|
+
Licensed under the MIT License. See LICENSE file at the project root or
|
|
5
|
+
https://opensource.org/license/mit/ for details.
|
|
6
|
+
|
|
7
|
+
SPDX-License-Identifier: MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
capture.js
|
|
12
|
+
Runs tool reporters over a page set and records raw per-page findings to
|
|
13
|
+
JSONL. Two page sources:
|
|
14
|
+
- Default: the W3C ACT-Rules test cases (Track A; score with score.js).
|
|
15
|
+
- --urls <file>: a plain list of URLs, one per line (the Stage-3 real-world
|
|
16
|
+
stress/mapping capture). Rows carry per-instance element paths
|
|
17
|
+
(`instances`) so element-level co-occurrence can be computed downstream.
|
|
18
|
+
Scoring/analysis is separate and cheap, so an expensive capture never has
|
|
19
|
+
to repeat to try a different policy.
|
|
20
|
+
|
|
21
|
+
Usage (cwd must be the repository root, for tool assets such as aceconfig):
|
|
22
|
+
node validation/act/capture.js --engines pour,axe [--match regex]
|
|
23
|
+
[--rules id,id] [--max N] [--out path.jsonl]
|
|
24
|
+
[--urls path.txt] [--recycle N]
|
|
25
|
+
|
|
26
|
+
Each output line: {testcaseId, ruleId, expected, engine, prevented,
|
|
27
|
+
instanceCount, outcomeTotals: {failed, cantTell}, asserted: {SC: count},
|
|
28
|
+
review: {SC: count}, ruleIDs: {engineRuleID: count}, ms}
|
|
29
|
+
`asserted` counts definite failures (standard-instance outcome `failed`);
|
|
30
|
+
`review` counts engine-flagged uncertainty (outcome `cantTell`). Criteria
|
|
31
|
+
are dotted WCAG SC numbers, extracted per engine from native results;
|
|
32
|
+
engines without an extractor still record ruleIDs, instanceCount, and
|
|
33
|
+
outcomeTotals. Every standard instance must carry a valid outcome; a row
|
|
34
|
+
whose instances do not is marked as an error (adapter drift).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// IMPORTS
|
|
38
|
+
|
|
39
|
+
const fs = require('fs');
|
|
40
|
+
const fsp = require('fs/promises');
|
|
41
|
+
const path = require('path');
|
|
42
|
+
const {chromium} = require('playwright');
|
|
43
|
+
const {OUTCOMES} = require('../../procs/standard');
|
|
44
|
+
|
|
45
|
+
// CONSTANTS
|
|
46
|
+
|
|
47
|
+
const TESTCASES_URL = 'https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases.json';
|
|
48
|
+
const CACHE_DIR = path.join(__dirname, 'cache');
|
|
49
|
+
const RESULTS_DIR = path.join(__dirname, 'results');
|
|
50
|
+
// Per-reporter time limit, matching the production limits in procs/doActs.js.
|
|
51
|
+
const REPORTER_TIMEOUT_MS = 45000;
|
|
52
|
+
|
|
53
|
+
// Mirrors procs/doTestAct.js xPathNeeds for the engines this harness supports.
|
|
54
|
+
const XPATH_NEEDS = {
|
|
55
|
+
alfa: 'own',
|
|
56
|
+
aslint: 'own',
|
|
57
|
+
axe: 'attribute',
|
|
58
|
+
ed11y: 'script',
|
|
59
|
+
htmlcs: 'attribute',
|
|
60
|
+
ibm: 'attribute',
|
|
61
|
+
pour: 'script',
|
|
62
|
+
qualWeb: 'attribute',
|
|
63
|
+
surea11y: 'script'
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Converts a wcag### tag (axe/pour style) to a dotted criterion.
|
|
67
|
+
const tagToCriterion = tag => {
|
|
68
|
+
const digits = tag.slice(4);
|
|
69
|
+
return `${digits[0]}.${digits[1]}.${digits.slice(2)}`;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
Per-engine criterion extractors: nativeResult → {asserted, review}, each
|
|
74
|
+
{dottedSC: count}. Engines without one record ruleIDs only; add extractors
|
|
75
|
+
here as engines join Track A.
|
|
76
|
+
*/
|
|
77
|
+
const criterionExtractors = {
|
|
78
|
+
// Shared by pour and surea11y: both adapters flatten to violations/incomplete
|
|
79
|
+
// arrays of findings carrying a dotted `wcag` criterion.
|
|
80
|
+
pour: nativeResult => {
|
|
81
|
+
const buckets = {asserted: {}, review: {}};
|
|
82
|
+
[['violations', 'asserted'], ['incomplete', 'review']].forEach(([source, target]) => {
|
|
83
|
+
(nativeResult[source] || []).forEach(finding => {
|
|
84
|
+
if (finding.wcag) {
|
|
85
|
+
buckets[target][finding.wcag] = (buckets[target][finding.wcag] || 0) + 1;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
return buckets;
|
|
90
|
+
},
|
|
91
|
+
get surea11y() {
|
|
92
|
+
return this.pour;
|
|
93
|
+
},
|
|
94
|
+
axe: nativeResult => {
|
|
95
|
+
const buckets = {asserted: {}, review: {}};
|
|
96
|
+
const details = nativeResult && nativeResult.details;
|
|
97
|
+
[['violations', 'asserted'], ['incomplete', 'review']].forEach(([source, target]) => {
|
|
98
|
+
((details && details[source]) || []).forEach(rule => {
|
|
99
|
+
const criteria = (rule.tags || [])
|
|
100
|
+
.filter(tag => /^wcag\d{3,4}$/.test(tag))
|
|
101
|
+
.map(tagToCriterion);
|
|
102
|
+
rule.nodes.forEach(() => {
|
|
103
|
+
criteria.forEach(criterion => {
|
|
104
|
+
buckets[target][criterion] = (buckets[target][criterion] || 0) + 1;
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
return buckets;
|
|
110
|
+
},
|
|
111
|
+
/*
|
|
112
|
+
QualWeb runs the ACT rules natively: each act-rules assertion carries the
|
|
113
|
+
exact ACT rule ID in `mapping`, so this extractor also emits actAsserted /
|
|
114
|
+
actReview maps keyed by ACT rule ID — score.js prefers those over the
|
|
115
|
+
criterion layer when present.
|
|
116
|
+
*/
|
|
117
|
+
qualWeb: nativeResult => {
|
|
118
|
+
const buckets = {asserted: {}, review: {}, actAsserted: {}, actReview: {}};
|
|
119
|
+
const modules = (nativeResult && nativeResult.modules) || {};
|
|
120
|
+
['act-rules', 'wcag-techniques', 'best-practices'].forEach(section => {
|
|
121
|
+
const assertions = (modules[section] && modules[section].assertions) || {};
|
|
122
|
+
Object.values(assertions).forEach(assertion => {
|
|
123
|
+
const criteria = ((assertion.metadata && assertion.metadata['success-criteria']) || [])
|
|
124
|
+
.map(criterion => criterion.name);
|
|
125
|
+
const actID = section === 'act-rules' ? assertion.mapping : null;
|
|
126
|
+
(assertion.results || []).forEach(assertionResult => {
|
|
127
|
+
const target = assertionResult.verdict === 'failed' ? 'asserted'
|
|
128
|
+
: assertionResult.verdict === 'warning' ? 'review' : null;
|
|
129
|
+
if (! target) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const count = (assertionResult.elements || []).length || 1;
|
|
133
|
+
criteria.forEach(criterion => {
|
|
134
|
+
buckets[target][criterion] = (buckets[target][criterion] || 0) + count;
|
|
135
|
+
});
|
|
136
|
+
if (actID) {
|
|
137
|
+
const actTarget = target === 'asserted' ? 'actAsserted' : 'actReview';
|
|
138
|
+
buckets[actTarget][actID] = (buckets[actTarget][actID] || 0) + count;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
return buckets;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Per-engine rule-ID counters from the standard result.
|
|
148
|
+
const ruleIDCounts = standardResult => {
|
|
149
|
+
const counts = {};
|
|
150
|
+
((standardResult && standardResult.instances) || []).forEach(instance => {
|
|
151
|
+
counts[instance.ruleID] = (counts[instance.ruleID] || 0) + instance.count;
|
|
152
|
+
});
|
|
153
|
+
return counts;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// FUNCTIONS
|
|
157
|
+
|
|
158
|
+
// Parses CLI arguments of the form --name value.
|
|
159
|
+
const parseArgs = argv => {
|
|
160
|
+
const args = {};
|
|
161
|
+
for (let i = 2; i < argv.length; i += 2) {
|
|
162
|
+
args[argv[i].replace(/^--/, '')] = argv[i + 1];
|
|
163
|
+
}
|
|
164
|
+
return args;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Gets the testcase feed, cached on disk.
|
|
168
|
+
const getTestcases = async () => {
|
|
169
|
+
await fsp.mkdir(CACHE_DIR, {recursive: true});
|
|
170
|
+
const cachePath = path.join(CACHE_DIR, 'testcases.json');
|
|
171
|
+
if (! fs.existsSync(cachePath)) {
|
|
172
|
+
const response = await fetch(TESTCASES_URL);
|
|
173
|
+
await fsp.writeFile(cachePath, await response.text());
|
|
174
|
+
}
|
|
175
|
+
return JSON.parse(await fsp.readFile(cachePath, 'utf8')).testcases;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// The window.getXPath injection, as in procs/launch.js.
|
|
179
|
+
const getXPathScript = () => {
|
|
180
|
+
window.getXPath = element => {
|
|
181
|
+
if (! element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
182
|
+
return '';
|
|
183
|
+
}
|
|
184
|
+
const segments = [];
|
|
185
|
+
while (element && element.nodeType === Node.ELEMENT_NODE) {
|
|
186
|
+
const tag = element.tagName.toLowerCase();
|
|
187
|
+
if (element === document.documentElement) {
|
|
188
|
+
segments.unshift('html');
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
const parent = element.parentNode;
|
|
192
|
+
if (! parent || parent.nodeType !== Node.ELEMENT_NODE) {
|
|
193
|
+
segments.unshift(tag);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
const cohort = Array.from(parent.childNodes).filter(
|
|
197
|
+
childNode => childNode.nodeType === Node.ELEMENT_NODE
|
|
198
|
+
&& childNode.tagName === element.tagName
|
|
199
|
+
);
|
|
200
|
+
const subscript = tag === 'body' ? '' : `[${cohort.indexOf(element) + 1}]`;
|
|
201
|
+
segments.unshift(`${tag}${subscript}`);
|
|
202
|
+
element = parent;
|
|
203
|
+
}
|
|
204
|
+
return `/${segments.join('/')}`;
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// OPERATION
|
|
209
|
+
|
|
210
|
+
(async () => {
|
|
211
|
+
const args = parseArgs(process.argv);
|
|
212
|
+
const engines = (args.engines || 'pour').split(',');
|
|
213
|
+
const outPath = args.out
|
|
214
|
+
|| path.join(RESULTS_DIR, `act-${new Date().toISOString().replace(/[:.]/g, '-')}.jsonl`);
|
|
215
|
+
await fsp.mkdir(path.dirname(outPath), {recursive: true});
|
|
216
|
+
let testcases;
|
|
217
|
+
if (args.urls) {
|
|
218
|
+
// URL-list mode: one synthetic testcase per line; the URL is the ID.
|
|
219
|
+
testcases = (await fsp.readFile(args.urls, 'utf8'))
|
|
220
|
+
.split('\n')
|
|
221
|
+
.map(line => line.trim())
|
|
222
|
+
.filter(line => line && ! line.startsWith('#'))
|
|
223
|
+
.map(url => ({testcaseId: url, url}));
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
testcases = await getTestcases();
|
|
227
|
+
}
|
|
228
|
+
if (args.rules) {
|
|
229
|
+
const wanted = new Set(args.rules.split(','));
|
|
230
|
+
testcases = testcases.filter(testcase => wanted.has(testcase.ruleId));
|
|
231
|
+
}
|
|
232
|
+
if (args.match) {
|
|
233
|
+
const matcher = new RegExp(args.match, 'i');
|
|
234
|
+
testcases = testcases.filter(testcase => matcher.test(testcase.ruleName));
|
|
235
|
+
}
|
|
236
|
+
if (args.max) {
|
|
237
|
+
testcases = testcases.slice(0, Number(args.max));
|
|
238
|
+
}
|
|
239
|
+
// Resume support: skip (testcase, engine) pairs already in the out file.
|
|
240
|
+
const alreadyCaptured = new Set();
|
|
241
|
+
if (fs.existsSync(outPath)) {
|
|
242
|
+
fs.readFileSync(outPath, 'utf8').split('\n').filter(Boolean).forEach(line => {
|
|
243
|
+
try {
|
|
244
|
+
const row = JSON.parse(line);
|
|
245
|
+
alreadyCaptured.add(`${row.testcaseId}:${row.engine}`);
|
|
246
|
+
}
|
|
247
|
+
catch(error) {}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
console.log(
|
|
251
|
+
`Capturing ${testcases.length} testcases × ${engines.join('+')} → ${outPath}`
|
|
252
|
+
+ (alreadyCaptured.size ? ` (resuming; ${alreadyCaptured.size} rows already captured)` : '')
|
|
253
|
+
);
|
|
254
|
+
const out = fs.createWriteStream(outPath, {flags: 'a'});
|
|
255
|
+
/*
|
|
256
|
+
The browser is recycled every RECYCLE_EVERY testcases: a 2026-08-22 full
|
|
257
|
+
run showed script-tag injection silently failing (tool global never
|
|
258
|
+
defined) after ~270 testcases in one long-lived browser, while
|
|
259
|
+
evaluate-based injection kept working. Fresh processes reproduce none of
|
|
260
|
+
it, so periodic recycling is the guard.
|
|
261
|
+
*/
|
|
262
|
+
const RECYCLE_EVERY = args.recycle === undefined ? 100 : Number(args.recycle);
|
|
263
|
+
let browser = await chromium.launch();
|
|
264
|
+
let context = await browser.newContext();
|
|
265
|
+
let done = 0;
|
|
266
|
+
/*
|
|
267
|
+
Hang guards. page.close() can hang FOREVER (not reject — hang) on a page
|
|
268
|
+
that performed a meta-refresh(0) navigation (reproduced locally,
|
|
269
|
+
playwright 1.62.1, ~50% of rounds on such pages; see isolation-notes.md).
|
|
270
|
+
So: every await that touches the browser is raced against a deadline, and
|
|
271
|
+
if the per-testcase watchdog trips, the browser is presumed wedged and
|
|
272
|
+
replaced (the old one is closed with its own deadline and abandoned if
|
|
273
|
+
that hangs too).
|
|
274
|
+
*/
|
|
275
|
+
const withDeadline = (promise, ms, label) => Promise.race([
|
|
276
|
+
promise,
|
|
277
|
+
new Promise((resolve, reject) => setTimeout(
|
|
278
|
+
() => reject(new Error(`${label} deadline ${ms}ms`)), ms
|
|
279
|
+
))
|
|
280
|
+
]);
|
|
281
|
+
const replaceBrowser = async () => {
|
|
282
|
+
const oldBrowser = browser;
|
|
283
|
+
withDeadline(oldBrowser.close(), 10000, 'browser.close').catch(() => {});
|
|
284
|
+
browser = await chromium.launch();
|
|
285
|
+
context = await browser.newContext();
|
|
286
|
+
};
|
|
287
|
+
for (const testcase of testcases) {
|
|
288
|
+
if (done > 0 && RECYCLE_EVERY && done % RECYCLE_EVERY === 0) {
|
|
289
|
+
await replaceBrowser();
|
|
290
|
+
}
|
|
291
|
+
for (const engine of engines) {
|
|
292
|
+
if (alreadyCaptured.has(`${testcase.testcaseId}:${engine}`)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const row = {
|
|
296
|
+
testcaseId: testcase.testcaseId,
|
|
297
|
+
ruleId: testcase.ruleId,
|
|
298
|
+
ruleName: testcase.ruleName,
|
|
299
|
+
expected: testcase.expected,
|
|
300
|
+
engine
|
|
301
|
+
};
|
|
302
|
+
const started = Date.now();
|
|
303
|
+
let page;
|
|
304
|
+
let closeHung = false;
|
|
305
|
+
try {
|
|
306
|
+
page = await withDeadline(context.newPage(), 20000, 'newPage');
|
|
307
|
+
const xPathNeed = XPATH_NEEDS[engine] || 'none';
|
|
308
|
+
if (xPathNeed === 'script' || xPathNeed === 'attribute') {
|
|
309
|
+
await page.addInitScript(getXPathScript);
|
|
310
|
+
}
|
|
311
|
+
await page.goto(testcase.url, {waitUntil: 'load', timeout: args.urls ? 30000 : 20000});
|
|
312
|
+
if (xPathNeed === 'attribute') {
|
|
313
|
+
// Stamp data-xpath attributes, as procs/launch.js does.
|
|
314
|
+
await withDeadline(page.evaluate(() => {
|
|
315
|
+
document.querySelectorAll('*').forEach(element => {
|
|
316
|
+
element.setAttribute('data-xpath', window.getXPath(element));
|
|
317
|
+
});
|
|
318
|
+
}), 15000, 'stamping');
|
|
319
|
+
}
|
|
320
|
+
/*
|
|
321
|
+
Capture the page's script nonce (if its CSP uses one) so
|
|
322
|
+
script-injecting adapters can reuse it — mirroring the production
|
|
323
|
+
launch proc's jobData.lastScriptNonce.
|
|
324
|
+
*/
|
|
325
|
+
const scriptNonce = await page.evaluate(() => {
|
|
326
|
+
const nonced = document.querySelector('script[nonce]');
|
|
327
|
+
return (nonced && nonced.nonce) || '';
|
|
328
|
+
}).catch(() => '');
|
|
329
|
+
const report = {
|
|
330
|
+
standard: 'also',
|
|
331
|
+
jobData: scriptNonce ? {lastScriptNonce: scriptNonce} : {},
|
|
332
|
+
catalog: {},
|
|
333
|
+
target: {url: testcase.url},
|
|
334
|
+
acts: [{type: 'test', which: engine, withItems: true}]
|
|
335
|
+
};
|
|
336
|
+
const actReport = await Promise.race([
|
|
337
|
+
require(`../../tests/${engine}`).reporter(page, report, 0, 40),
|
|
338
|
+
new Promise((resolve, reject) => setTimeout(
|
|
339
|
+
() => reject(new Error('reporter timeout')), REPORTER_TIMEOUT_MS
|
|
340
|
+
))
|
|
341
|
+
]);
|
|
342
|
+
const {data, result} = actReport;
|
|
343
|
+
row.prevented = !! (data && data.prevented);
|
|
344
|
+
if (row.prevented) {
|
|
345
|
+
row.error = data.error;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
row.instanceCount = result.standardResult.instances.length;
|
|
349
|
+
row.outcomeTotals = result.standardResult.outcomeTotals;
|
|
350
|
+
row.ruleIDs = ruleIDCounts(result.standardResult);
|
|
351
|
+
const unoutcomed = result.standardResult.instances
|
|
352
|
+
.filter(instance => ! OUTCOMES.includes(instance.outcome)).length;
|
|
353
|
+
if (unoutcomed) {
|
|
354
|
+
row.error = `${unoutcomed} instance(s) without a valid outcome`;
|
|
355
|
+
}
|
|
356
|
+
const extractor = criterionExtractors[engine];
|
|
357
|
+
if (extractor) {
|
|
358
|
+
Object.assign(row, extractor(result.nativeResult));
|
|
359
|
+
}
|
|
360
|
+
if (args.urls) {
|
|
361
|
+
// Per-instance element paths for element-level co-occurrence
|
|
362
|
+
// (Stage-3b mapping bootstrap). Capped: a pathological page must
|
|
363
|
+
// not blow up the output file.
|
|
364
|
+
row.instances = result.standardResult.instances
|
|
365
|
+
.slice(0, 2000)
|
|
366
|
+
.map(instance => ({
|
|
367
|
+
ruleID: instance.ruleID,
|
|
368
|
+
severity: instance.ordinalSeverity,
|
|
369
|
+
outcome: instance.outcome,
|
|
370
|
+
uncertainty: instance.uncertainty,
|
|
371
|
+
xPath: (report.catalog[instance.catalogIndex] || {}).pathID || ''
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch(error) {
|
|
377
|
+
row.prevented = true;
|
|
378
|
+
row.error = error.message.slice(0, 200);
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
if (page) {
|
|
382
|
+
// close() can hang, not reject, after a meta-refresh navigation —
|
|
383
|
+
// race it and abandon the page if the deadline trips.
|
|
384
|
+
await withDeadline(page.close(), 5000, 'page.close').catch(error => {
|
|
385
|
+
closeHung = error.message.includes('deadline');
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/*
|
|
390
|
+
Replace the browser on any sign of a wedged/dead browser, not just a
|
|
391
|
+
tripped deadline: a wedged browser's newPage first throws "Protocol
|
|
392
|
+
error (Target.createTarget)" and, once the process dies, "Target
|
|
393
|
+
page, context or browser has been closed" (observed 2026-08-22 after
|
|
394
|
+
a bc659a meta-refresh close-hang; a deadline-only trigger let 73
|
|
395
|
+
rows fail before the next scheduled recycle).
|
|
396
|
+
*/
|
|
397
|
+
if (
|
|
398
|
+
closeHung
|
|
399
|
+
|| (row.error && /deadline|has been closed|Protocol error/.test(row.error))
|
|
400
|
+
/*
|
|
401
|
+
Canary: a script-tag-injecting tool reporting its global undefined
|
|
402
|
+
on an ordinary text/html page means browser-wide script-element
|
|
403
|
+
execution is wedged (the injection cliff — a probabilistic wedge
|
|
404
|
+
that regional replays never reproduce; it once killed 41
|
|
405
|
+
consecutive rows in a browser only ~36 pages old). XML pages are
|
|
406
|
+
excluded: script elements legitimately never execute there.
|
|
407
|
+
*/
|
|
408
|
+
|| (row.error && /global not defined \(contentType text\/html/.test(row.error))
|
|
409
|
+
) {
|
|
410
|
+
row.browserReplaced = true;
|
|
411
|
+
await replaceBrowser();
|
|
412
|
+
}
|
|
413
|
+
row.ms = Date.now() - started;
|
|
414
|
+
out.write(`${JSON.stringify(row)}\n`);
|
|
415
|
+
}
|
|
416
|
+
done++;
|
|
417
|
+
if (done % 25 === 0) {
|
|
418
|
+
console.log(`${done}/${testcases.length}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
await browser.close();
|
|
422
|
+
out.end();
|
|
423
|
+
console.log(`Done: ${done} testcases → ${outPath}`);
|
|
424
|
+
})();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Chromium bug report — Target.closeTarget acks success without closing during navigation commit
|
|
2
|
+
|
|
3
|
+
**NOT FILED as a new issue — already tracked upstream.** Pre-filing search (2026-08-22) found
|
|
4
|
+
<https://issues.chromium.org/issues/536385539> (filed 2026-07-19 by a Google engineer, P2/S2,
|
|
5
|
+
pending code change 8251379): `Target.closeTarget` is lost when a navigation commit swaps the
|
|
6
|
+
main RenderFrameHost right after `ClosePage` — the pending `ClosePage` callback dies with the
|
|
7
|
+
old RFH and is never re-issued. Same defect; their trigger was back-to-back failing navigations,
|
|
8
|
+
ours (meta-refresh 0) is an additional ordinary-page trigger of the same race.
|
|
9
|
+
|
|
10
|
+
**Actions taken instead:** corroborating comment posted as comment #2 on 536385539 (meta-refresh
|
|
11
|
+
trigger, raw-CDP hit rates, workaround matrix, link to
|
|
12
|
+
<https://github.com/microsoft/playwright/issues/42366>); issue upvoted; cross-link comment added
|
|
13
|
+
on the Playwright issue. Repro retained here: `repro-cdp-raw.js`.
|
|
14
|
+
|
|
15
|
+
The draft below is preserved for reference (its evidence went into the comment).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
**Title:** Target.closeTarget returns success:true without closing the target when the close
|
|
20
|
+
races a client-side (meta refresh) navigation commit; no Target.targetDestroyed is ever emitted
|
|
21
|
+
|
|
22
|
+
## Environment
|
|
23
|
+
|
|
24
|
+
- Chromium 151.0.7922.34, `--headless=new`, Linux
|
|
25
|
+
- Raw CDP over WebSocket (no client library — reproduces identically through Playwright 1.62.1,
|
|
26
|
+
where it strands `page.close()` forever)
|
|
27
|
+
- Not reproducible in Firefox or WebKit equivalents
|
|
28
|
+
|
|
29
|
+
## Steps
|
|
30
|
+
|
|
31
|
+
1. Create a target and navigate it to a page carrying
|
|
32
|
+
`<meta http-equiv="refresh" content="0; URL='/target.html'">` (immediate client-side
|
|
33
|
+
redirect).
|
|
34
|
+
2. Leave a `Runtime.evaluate` (`awaitPromise: true`) in flight for the refresh navigation to
|
|
35
|
+
destroy.
|
|
36
|
+
3. ~45–60 ms after the navigate (during the refresh navigation's commit window), send
|
|
37
|
+
`Target.closeTarget {targetId}` on the browser session.
|
|
38
|
+
|
|
39
|
+
Reproduction rate ~3/10 rounds with the attached script (a timing race; the script sweeps the
|
|
40
|
+
window).
|
|
41
|
+
|
|
42
|
+
## Expected
|
|
43
|
+
|
|
44
|
+
Either the target closes — `Target.targetDestroyed` fires and the target disappears from
|
|
45
|
+
`Target.getTargets` — or `closeTarget` reports `success: false`/an error.
|
|
46
|
+
|
|
47
|
+
## Actual
|
|
48
|
+
|
|
49
|
+
`closeTarget` returns `{"result":{"success":true}}`, but:
|
|
50
|
+
|
|
51
|
+
- no `Target.targetDestroyed` is ever emitted for the target;
|
|
52
|
+
- the target remains listed in `Target.getTargets`;
|
|
53
|
+
- the target proceeds to fully load the redirect destination (frameNavigated,
|
|
54
|
+
DOMContentLoaded, load, paint lifecycle events all arrive *after* the successful-looking
|
|
55
|
+
close acknowledgment).
|
|
56
|
+
|
|
57
|
+
A second `Target.closeTarget` for the same target afterwards does close it, as does closing its
|
|
58
|
+
browser context — the target is not stuck, the close was simply dropped.
|
|
59
|
+
|
|
60
|
+
## Impact
|
|
61
|
+
|
|
62
|
+
Any CDP client that treats `success: true` as "the target will be destroyed" waits forever.
|
|
63
|
+
Playwright's `page.close()` does exactly this: its promise resolves on target destruction, so
|
|
64
|
+
automation against pages using meta-refresh (common in the wild; the W3C ACT-Rules bc659a
|
|
65
|
+
"passed" fixtures are examples) hangs nondeterministically with no error. Observed at ~70–80%
|
|
66
|
+
per attempt via Playwright's timing on real fixture pages.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Pour FP triage — 2026-08-22 capture (440-testcase sample, asserted band)
|
|
2
|
+
|
|
3
|
+
Manual read of every pour false-positive fixture from `results/merged-2026-08-22.jsonl`.
|
|
4
|
+
Verdicts: **real** (pour asserts a failure ACT deems passed/inapplicable — upstream candidate) or
|
|
5
|
+
**artifact** (correct finding counted against the wrong ACT rule by the criterion-level scoring
|
|
6
|
+
policy — not a pour defect).
|
|
7
|
+
|
|
8
|
+
## Real — upstream filing candidates
|
|
9
|
+
|
|
10
|
+
1. **Contrast asserted on ACT-exempt text** (3 FPs, `afw4f7`/`09o5cg`). Symbol-only text
|
|
11
|
+
(`09o5cg` Passed Ex 6: a paragraph of `----====++++…` at 3.66:1) and letter-as-icon glyphs
|
|
12
|
+
(`afw4f7` Passed Ex 7 / `09o5cg` Passed Ex 6: `<button aria-label="Close">X</button>`) are
|
|
13
|
+
expected *passed* by the ACT contrast rules; pour's `color-contrast` /
|
|
14
|
+
`color-contrast-enhanced` assert failures. Axe returns no finding on all three (TN). Given
|
|
15
|
+
pour's "report what it cannot prove" ethos, asserting on ACT-consensus-exempt text is worth an
|
|
16
|
+
upstream issue.
|
|
17
|
+
2. **`button-name` on `<button role="none" disabled>`** (1 FP, `97a4e1` Inapplicable Ex 5). With
|
|
18
|
+
the element disabled (not focusable), `role="none"` holds and the element is out of scope for
|
|
19
|
+
the button-name rule; pour flags it.
|
|
20
|
+
3. **`link-name` misses descendant `aria-labelledby`** (1 FP, `c487ae` Passed Ex 8):
|
|
21
|
+
`<a href><img aria-labelledby="id1"></a>` — the link's name-from-content includes the image's
|
|
22
|
+
aria-labelledby name, so the link is correctly named; pour flags it empty. Accessible-name
|
|
23
|
+
computation gap.
|
|
24
|
+
4. **Language rules skip ACT applicability conditions** (4 FPs, `de46e4`/`off6ek`/`ucwvc8`).
|
|
25
|
+
`valid-lang-parts` flags invalid `lang` values on elements whose text is overridden by a
|
|
26
|
+
descendant `lang` or entirely `display:none` (ACT: passed/inapplicable — the SC concerns text
|
|
27
|
+
that *uses* the language); `html-lang` flags a completely empty `<html></html>` page (ACT:
|
|
28
|
+
inapplicable without text). Defensible strictness, but a divergence from ACT consensus worth
|
|
29
|
+
raising upstream.
|
|
30
|
+
|
|
31
|
+
## Artifact — scoring policy, not pour
|
|
32
|
+
|
|
33
|
+
- **`c487ae` Inapplicable Ex 4** (`<a aria-hidden="true" href>…</a>`): pour's `aria-hidden-focus`
|
|
34
|
+
finding is a *correct* 4.1.2 issue (focusable content hidden from AT); it counts as an FP only
|
|
35
|
+
because the fixture tests a different ACT rule on the same criterion. This is the documented
|
|
36
|
+
generosity of criterion-level matching; an exact engine-rule → ACT-rule map would reclassify it.
|
|
37
|
+
|
|
38
|
+
## Tally
|
|
39
|
+
|
|
40
|
+
Of 15 criterion-level FPs in the sample, ~9 fixtures inspected covering all hotspots: 9 real
|
|
41
|
+
across 4 defect classes, 1 artifact. The 4 classes are small, well-defined applicability/name-
|
|
42
|
+
computation gaps — exactly the kind of finding the candidacy pipeline is meant to hand upstream
|
|
43
|
+
(engine-candidacy-pipeline.md, Stage 5 / benchmarks §6 stewardship).
|
|
44
|
+
|
|
45
|
+
## Recheck 2026-09-01 (pour 1.37.0, surea11y 1.7.0)
|
|
46
|
+
|
|
47
|
+
Re-ran every fixture of the eight ACT rules above with the current bundles
|
|
48
|
+
(`--engines pour,surea11y --rules afw4f7,09o5cg,97a4e1,c487ae,de46e4,off6ek,ucwvc8,4b1c6c`).
|
|
49
|
+
|
|
50
|
+
- Pour classes 1–3 persist unchanged in 1.37.0 (contrast on `afw4f7`/`09o5cg` Passed Example 7,
|
|
51
|
+
`button-name` on `97a4e1` Inapplicable Example 5, `link-name` on `c487ae` Passed Example 8).
|
|
52
|
+
Contrast also asserts on `09o5cg` Inapplicable Examples 6 and 7 — label text of a disabled
|
|
53
|
+
control, which ACT's "disabled label" applicability exemption excludes — a third sub-case of
|
|
54
|
+
class 1. Class 4 narrows to `valid-lang-parts` (`de46e4` Passed Example 4, Inapplicable 3–5,
|
|
55
|
+
`off6ek` Inapplicable 5): the `html-lang` hits on empty documents (`ucwvc8` Inapplicable 2–3,
|
|
56
|
+
`97a4e1` Passed 6, `c487ae` Passed 9) are correct under ACT `b5c3f8`, whose applicability has
|
|
57
|
+
no content condition, and count as FPs only through criterion-level scoring. Same for
|
|
58
|
+
surea11y's `html-lang-attr-present` on those fixtures.
|
|
59
|
+
- **surea11y `4b1c6c` (11 FP)** is `iframe-title-unique`, not the 1.7.0 rule
|
|
60
|
+
`identical-iframes-same-purpose` (which already reports differing resources as `cantTell`):
|
|
61
|
+
it fails any two frames sharing a title attribute, including Passed Examples 1, 4–9 where the
|
|
62
|
+
frames embed the same or equivalent resource, and Inapplicable Example 5.
|
|
63
|
+
- Upstream filings prepared from this recheck: pour — presentational-role conflict ignoring
|
|
64
|
+
`disabled`, name-from-content ignoring descendant `aria-labelledby`, contrast on
|
|
65
|
+
non-human-language text / icon glyphs / disabled labels, `valid-lang-parts` applicability;
|
|
66
|
+
surea11y — `iframe-title-unique` vs ACT 4b1c6c.
|
|
67
|
+
|
|
68
|
+
### Filed upstream 2026-09-01 (pour-engine)
|
|
69
|
+
|
|
70
|
+
| Class | Issue | Pull request |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| button-name on `<button role="none" disabled>` (presentational-role conflict ignores `:disabled`) | [#3](https://github.com/pourdev/pour-engine/issues/3) | [#7](https://github.com/pourdev/pour-engine/pull/7) |
|
|
73
|
+
| link-name: name from content ignores a descendant's `aria-labelledby` | [#4](https://github.com/pourdev/pour-engine/issues/4) | [#8](https://github.com/pourdev/pour-engine/pull/8) |
|
|
74
|
+
| contrast on symbol-only text, single-letter icons, labels of disabled widgets | [#5](https://github.com/pourdev/pour-engine/issues/5) | [#9](https://github.com/pourdev/pour-engine/pull/9) |
|
|
75
|
+
| valid-lang-parts on `lang` attributes that govern no text | [#6](https://github.com/pourdev/pour-engine/issues/6) | [#10](https://github.com/pourdev/pour-engine/pull/10) |
|
|
76
|
+
|
|
77
|
+
### Filed upstream 2026-09-01 (surea11y core)
|
|
78
|
+
|
|
79
|
+
| Class | Issue | Pull request |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `iframe-title-unique` fails frames sharing a title that embed the same resource (ACT 4b1c6c) | [#16](https://github.com/SureA11y/core/issues/16) | [#17](https://github.com/SureA11y/core/pull/17) |
|
|
82
|
+
|
|
83
|
+
The maintainers document the strict behaviour as deliberate (`docs/ACT_RULE_MAPPING.md`), so the
|
|
84
|
+
issue asks them to choose between aligning the rule with 4b1c6c (the PR) and deprecating it in
|
|
85
|
+
favour of `identical-iframes-same-purpose`. Fixture branches live on the forks
|
|
86
|
+
`wittjeff/pour-engine` and `wittjeff/core`.
|