@surea11y/core 1.6.0 → 1.7.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/CHANGELOG.md +47 -0
- package/README.md +24 -38
- package/docs/ACT_RULE_MAPPING.md +8 -6
- package/docs/API_STABILITY.md +51 -3
- package/docs/BINDING_AUTHORS_GUIDE.md +104 -2
- package/docs/DESIGN_CHALLENGES.md +66 -0
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +28 -2
- package/docs/INTEGRATION.md +4 -2
- package/docs/LIMITATIONS.md +3 -1
- package/docs/OUTPUT_SCHEMA.md +44 -6
- package/docs/POLICY.md +1 -1
- package/docs/RULE_AUTHORING.md +11 -12
- package/docs/RULE_CATALOG.md +76 -26
- package/docs/RULE_HELPERS.md +333 -0
- package/docs/RULE_TAXONOMY.md +25 -4
- package/docs/SARIF.md +21 -2
- package/docs/WCAG_CONFORMANCE.md +9 -1
- package/package.json +9 -3
- package/src/checks/automatic/aria-allowed-attr.js +6 -0
- package/src/checks/automatic/aria-allowed-role.js +32 -23
- package/src/checks/automatic/aria-braille-equivalent.js +18 -10
- package/src/checks/automatic/aria-conditional-attr.js +17 -10
- package/src/checks/automatic/aria-deprecated-role.js +12 -0
- package/src/checks/automatic/aria-hidden-body.js +1 -1
- package/src/checks/automatic/aria-prohibited-attr.js +5 -0
- package/src/checks/automatic/aria-prohibited-children.js +6 -6
- package/src/checks/automatic/aria-required-attr.js +59 -12
- package/src/checks/automatic/aria-required-children.js +33 -16
- package/src/checks/automatic/aria-required-parent.js +32 -6
- package/src/checks/automatic/aria-role-name-present.js +1 -1
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +74 -21
- package/src/checks/automatic/aria-valid-attr.js +14 -9
- package/src/checks/automatic/avoid-inline-spacing.js +133 -6
- package/src/checks/automatic/contrast-computable.js +10 -0
- package/src/checks/automatic/contrast-enhanced.js +12 -0
- package/src/checks/automatic/contrast-minimum.js +12 -0
- package/src/checks/automatic/css-orientation-lock.js +42 -5
- package/src/checks/automatic/duplicate-id-aria.js +5 -0
- package/src/checks/automatic/duplicate-id.js +13 -8
- package/src/checks/automatic/form-control-single-label.js +9 -0
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +5 -0
- package/src/checks/automatic/label-in-name.js +38 -56
- package/src/checks/automatic/link-in-text-block.js +279 -23
- package/src/checks/automatic/target-size-minimum.js +84 -5
- package/src/checks/automatic/td-has-header.js +19 -18
- package/src/checks/manual/form-control-label-quality-manual.js +134 -24
- package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
- package/src/checks/manual/password-paste-enabled-manual.js +255 -0
- package/src/core.js +3863 -44184
- package/src/earl.js +144 -0
- package/src/sarif.js +22 -2
- package/surea11y.browser.js +10 -41039
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- /package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +0 -0
package/docs/RULE_TAXONOMY.md
CHANGED
|
@@ -11,13 +11,34 @@ It reflects **what already exists in the rules and tests**, not theory.
|
|
|
11
11
|
Encoded by: `meta.type`
|
|
12
12
|
|
|
13
13
|
- `automatic`
|
|
14
|
-
- Rule
|
|
15
|
-
- Allowed outcomes: `pass`, `fail`, `notApplicable`,
|
|
14
|
+
- Rule **decides deterministically**, with no heuristics and no guessing
|
|
15
|
+
- Allowed outcomes: `pass`, `fail`, `notApplicable`, `cantTell`
|
|
16
|
+
- `cantTell` is the primary path in two distinct cases, and a defensive
|
|
17
|
+
fallback in a third:
|
|
18
|
+
- the rule decides that a real violation exists, but the violation does
|
|
19
|
+
not on its own establish that the mapped criterion fails — an ARIA
|
|
20
|
+
author requirement the exposed name, role and value survive
|
|
21
|
+
(`aria-valid-attr`, `aria-braille-equivalent`,
|
|
22
|
+
`aria-conditional-attr`), or the graded tier of a rule that fails
|
|
23
|
+
elsewhere (`aria-required-attr`, `aria-roles-valid`)
|
|
24
|
+
- the rule decides deterministically but claims no Success Criterion at
|
|
25
|
+
all (`aria-allowed-role`, tagged `best-practice`)
|
|
26
|
+
- the rule asks whether something is PRESENT, and its absence conveys
|
|
27
|
+
nothing false, while the question of whether what IS present is valid
|
|
28
|
+
belongs to a sibling rule that still fails
|
|
29
|
+
(`aria-required-children`, paired with `aria-prohibited-children`)
|
|
30
|
+
- a computability gate the rule cannot resolve, or an internal-failure
|
|
31
|
+
safety net (`contrast-minimum.js`/`contrast-enhanced.js`/
|
|
32
|
+
`contrast-computable.js`/`target-size-minimum.js`)
|
|
16
33
|
- `manual`
|
|
17
|
-
- Rule signals **human review required
|
|
34
|
+
- Rule signals **human review required**: it cannot decide at all
|
|
18
35
|
- Allowed outcomes: `cantTell`, `notApplicable`
|
|
19
36
|
|
|
20
|
-
Manual rules MUST NOT make normative failure decisions.
|
|
37
|
+
Manual rules MUST NOT make normative failure decisions. The dividing line
|
|
38
|
+
between the two types is whether the rule can decide, not which outcome it
|
|
39
|
+
reports: an automatic rule that reports `cantTell` has decided, and is saying
|
|
40
|
+
what it found; a manual rule reports `cantTell` because the question is not
|
|
41
|
+
decidable from markup.
|
|
21
42
|
|
|
22
43
|
---
|
|
23
44
|
|
package/docs/SARIF.md
CHANGED
|
@@ -14,11 +14,30 @@ See [`CI_INTEGRATIONS.md`](./CI_INTEGRATIONS.md) for a ready-to-paste GitHub Act
|
|
|
14
14
|
|
|
15
15
|
## What becomes a SARIF result
|
|
16
16
|
|
|
17
|
-
Only `fail`/`cantTell` occurrences produce SARIF results
|
|
17
|
+
Only `fail`/`cantTell` occurrences produce SARIF results (same "violations only" framing as [`REPORT.md`](./REPORT.md)'s HTML report).
|
|
18
|
+
|
|
19
|
+
A `notApplicable` check is not always empty: a rule may attach one occurrence explaining why it had nothing to judge, which the contrast rules do when no text had a computable background. Those never become results — a consumer treats every result as an alert, and "this was not evaluated" is not one — but they are not dropped either. They are carried as `note`-level entries in `runs[0].invocations[0].toolExecutionNotices`, each naming the rule it came from via `associatedRule.id`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
"invocations": [
|
|
23
|
+
{
|
|
24
|
+
"executionSuccessful": true,
|
|
25
|
+
"toolExecutionNotices": [
|
|
26
|
+
{
|
|
27
|
+
"level": "note",
|
|
28
|
+
"message": { "text": "No eligible text had computable contrast (eligible text nodes: 13). See the contrast computability rule for details." },
|
|
29
|
+
"associatedRule": { "id": "contrast-minimum" }
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That keeps a SARIF-only pipeline from reading silence as a clean bill of health: no contrast alerts can mean the page is fine, or that contrast was never computable, and only the notice separates the two. The block is emitted only when there is something to say, so a run with nothing to report has no `invocations` key at all.
|
|
18
37
|
|
|
19
38
|
| Engine outcome | SARIF `level` | Meaning |
|
|
20
39
|
|---|---|---|
|
|
21
|
-
| `fail` | `error` | Deterministic
|
|
40
|
+
| `fail` | `error` | Deterministic violation — the CI-gating case. |
|
|
22
41
|
| `cantTell` | `warning` | Needs human review — surfaced, but shouldn't block a build on its own. |
|
|
23
42
|
|
|
24
43
|
Every rule that ran (regardless of whether it produced a result) is listed once in `runs[0].tool.driver.rules`, with `defaultConfiguration.level` set from the rule's `type`: `automatic` (fail-capable) → `error`, `manual` (capped at `cantTell`) → `warning`.
|
package/docs/WCAG_CONFORMANCE.md
CHANGED
|
@@ -44,6 +44,14 @@ only its own origin tag — so a full conformance target is a union of tag sets.
|
|
|
44
44
|
ready-made sets per version, including the one criterion WCAG 2.2 removed rather than
|
|
45
45
|
added.
|
|
46
46
|
|
|
47
|
+
**The removed criterion is handled for you.** Every run resolves a target WCAG version
|
|
48
|
+
(`engineOptions.wcagVersion`, else whatever your version tags imply, else `2.2`) and
|
|
49
|
+
reports it back as `engine.wcagVersion`. Under a 2.2 target, a rule mapped only to SC
|
|
50
|
+
4.1.1 Parsing cannot report `fail` — it runs, reports its occurrences, and comes back
|
|
51
|
+
`cantTell` with a `wcagVersionScope` field explaining the coercion. So a default scan
|
|
52
|
+
never gates on a criterion WCAG 2.2 does not contain, and a 2.0/2.1 scan still gets a
|
|
53
|
+
real 4.1.1 verdict.
|
|
54
|
+
|
|
47
55
|
Composites, unlike atomic rules, *are* filtered cumulatively. The runner reads the
|
|
48
56
|
highest level named in `tags` and drops every composite above it, so requesting
|
|
49
57
|
`['wcag2a', 'wcag2aa']` returns no `rulesResults` entry for an AAA-only SC. That is
|
|
@@ -58,7 +66,7 @@ suppression.
|
|
|
58
66
|
No automated tool — this one included — can certify full WCAG conformance. That's not a limitation specific to surea11y; it's inherent to WCAG itself; a meaningful fraction of Success Criteria require human judgment (is this alt text *accurate*, not just *present*; is this error message *understandable*) or dynamic testing this engine's static-DOM-scan architecture cannot do at all (keyboard-trap detection, real layout/reflow at zoom). See [`LIMITATIONS.md`](./LIMITATIONS.md) for the full, explicit list of what's out of scope and why.
|
|
59
67
|
|
|
60
68
|
What surea11y *can* give you, honestly:
|
|
61
|
-
- Every `fail` is a real, deterministic, normative violation — never a guess.
|
|
69
|
+
- Every `fail` is a real, deterministic, normative violation under the version you targeted — never a guess.
|
|
62
70
|
- Every `cantTell` is an explicit flag for human review, not a swallowed uncertainty.
|
|
63
71
|
- The facet coverage table tells you exactly which parts of which SCs have zero automated coverage, so you know where a `pass` is silent rather than exhaustive.
|
|
64
72
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surea11y/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Deterministic WCAG 2.2 accessibility engine that tells you what it can't tell you. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"./baseline": "./src/baseline.js",
|
|
31
31
|
"./report": "./src/report.js",
|
|
32
32
|
"./sarif": "./src/sarif.js",
|
|
33
|
+
"./earl": "./src/earl.js",
|
|
33
34
|
"./browser": "./surea11y.browser.js",
|
|
35
|
+
"./i18n/*": "./surea11y.i18n.*.js",
|
|
34
36
|
"./package.json": "./package.json"
|
|
35
37
|
},
|
|
36
38
|
"author": "Jorge Rumoroso",
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
"src/baseline.js",
|
|
56
58
|
"src/report.js",
|
|
57
59
|
"src/sarif.js",
|
|
60
|
+
"src/earl.js",
|
|
58
61
|
"src/checks/**/*.js",
|
|
59
62
|
"surea11y.browser.js",
|
|
60
63
|
"surea11y.i18n.*.js",
|
|
@@ -74,7 +77,7 @@
|
|
|
74
77
|
"format:check": "prettier --check \"src/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
|
|
75
78
|
"build": "node scripts/build-core.js && node scripts/build-browser.js",
|
|
76
79
|
"pretest": "playwright install chromium",
|
|
77
|
-
"test": "npm run format:check && npm run build && node scripts/run-tests.js",
|
|
80
|
+
"test": "npm run lint && npm run format:check && npm run build && npm run validate:rules && node scripts/run-tests.js",
|
|
78
81
|
"test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js",
|
|
79
82
|
"test:contrast-helpers": "node tests/contrast-helpers.test.js",
|
|
80
83
|
"helpers-perf-bench": "node --expose-gc scripts/dom-helpers-perf-bench.js",
|
|
@@ -85,11 +88,13 @@
|
|
|
85
88
|
"coverage": "node scripts/generate-wcag-coverage.js --rulesDir src/checks",
|
|
86
89
|
"coverage:strict": "node scripts/generate-wcag-coverage.js --rulesDir src/checks --strictFacets",
|
|
87
90
|
"coverage:check": "node scripts/generate-wcag-coverage.js --rulesDir src/checks --check",
|
|
91
|
+
"finding-ids": "npm run build && node scripts/generate-finding-ids.js",
|
|
88
92
|
"fixtures:index": "node scripts/generate-fixture-index.js",
|
|
93
|
+
"fixtures:check": "node scripts/generate-fixture-index.js --check",
|
|
89
94
|
"docs:rule-catalog": "npm run build && node scripts/generate-rule-catalog.js",
|
|
90
95
|
"validate:automatic-rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic",
|
|
91
96
|
"validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual",
|
|
92
|
-
"validate:rules": "npm run
|
|
97
|
+
"validate:rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic src/checks/manual",
|
|
93
98
|
"i18n:new": "node scripts/i18n-scaffold.js",
|
|
94
99
|
"i18n:sync": "node scripts/i18n-sync.js",
|
|
95
100
|
"i18n:check": "node scripts/i18n-sync.js --check",
|
|
@@ -98,6 +103,7 @@
|
|
|
98
103
|
"devDependencies": {
|
|
99
104
|
"@eslint/js": "^10.0.1",
|
|
100
105
|
"aria-query": "^5.3.2",
|
|
106
|
+
"esbuild": "^0.28.2",
|
|
101
107
|
"eslint": "^10.8.0",
|
|
102
108
|
"eslint-config-prettier": "^10.1.8",
|
|
103
109
|
"globals": "^17.8.0",
|
|
@@ -934,6 +934,12 @@ function runInPage(ctx) {
|
|
|
934
934
|
hintKey: 'ariaAllowedAttr_hint_cantTell',
|
|
935
935
|
params: { attr: name, role }
|
|
936
936
|
},
|
|
937
|
+
uncertainty: {
|
|
938
|
+
code: 'spec-only',
|
|
939
|
+
needed:
|
|
940
|
+
'Whether the deprecated attribute is still honoured by the assistive technology in use.',
|
|
941
|
+
evidence: { attribute: name, role, status: 'deprecated-for-role' }
|
|
942
|
+
},
|
|
937
943
|
data: {
|
|
938
944
|
details: { reasonCode: 'ARIA_ATTR_DEPRECATED', attr: name, role }
|
|
939
945
|
}
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* @check aria-allowed-role
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Explicit role must be permitted by the ARIA-in-HTML spec for its host element
|
|
9
|
-
* @standard WCAG
|
|
10
|
-
* @sc 4.1.2
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
11
10
|
* @applicability
|
|
12
11
|
* Applies to elements with an explicit, valid, non-abstract role, where
|
|
13
12
|
* the host element/attribute combination has an asserted permitted-roles
|
|
@@ -16,7 +15,18 @@
|
|
|
16
15
|
* @expectation
|
|
17
16
|
* The explicit role is one of the roles the ARIA-in-HTML specification
|
|
18
17
|
* permits for that host element.
|
|
18
|
+
* Reported at CANTTELL rather than FAIL: ARIA-in-HTML's permitted-roles
|
|
19
|
+
* table is an author conformance requirement with no ACT rule and no WCAG
|
|
20
|
+
* mapping in any source. The role the author asked for is still the role
|
|
21
|
+
* assistive technology exposes, so whether the combination harms anyone
|
|
22
|
+
* depends on the widget, not on the table.
|
|
19
23
|
* @implementation-notes
|
|
24
|
+
* - Not WCAG-normative. ARIA-in-HTML's permitted-roles table is an author
|
|
25
|
+
* conformance requirement of that specification; no ACT rule covers it and
|
|
26
|
+
* no source maps it to a Success Criterion, so the rule reports the
|
|
27
|
+
* violation without claiming a criterion is failed. Deterministic all the
|
|
28
|
+
* same, so it stays `type: 'automatic'` and keeps deciding rather than
|
|
29
|
+
* deferring to a reviewer -- see docs/RULE_TAXONOMY.md 1.1.
|
|
20
30
|
* - Scoped to elements present in ALLOWED_ROLES_BY_ELEMENT;
|
|
21
31
|
* elements without an asserted constraint are treated as "no constraint"
|
|
22
32
|
* (not flagged) rather than guessed at, see that table's header comment.
|
|
@@ -36,22 +46,14 @@ const meta = {
|
|
|
36
46
|
descriptionKey: 'ariaAllowedRole_description'
|
|
37
47
|
},
|
|
38
48
|
helpUrl: null,
|
|
39
|
-
tags: ['
|
|
40
|
-
wcagSc: [
|
|
41
|
-
normativeMappings: [
|
|
42
|
-
{
|
|
43
|
-
standard: 'WCAG',
|
|
44
|
-
version: '2.2',
|
|
45
|
-
requirement: '4.1.2',
|
|
46
|
-
title: 'Name, Role, Value',
|
|
47
|
-
conformanceLevel: 'A'
|
|
48
|
-
}
|
|
49
|
-
],
|
|
49
|
+
tags: ['best-practice', 'aria', 'structure', 'atomic', 'automatic'],
|
|
50
|
+
wcagSc: [],
|
|
51
|
+
normativeMappings: [],
|
|
50
52
|
defaultSeverity: 'moderate',
|
|
51
53
|
category: 'robust',
|
|
52
54
|
type: 'automatic',
|
|
53
55
|
defaultConfidence: 'high',
|
|
54
|
-
coverage: {
|
|
56
|
+
coverage: {}
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
function runInPage(ctx) {
|
|
@@ -93,6 +95,16 @@ function runInPage(ctx) {
|
|
|
93
95
|
hintKey: 'ariaAllowedRole_hint_fail',
|
|
94
96
|
params: { role, element: tag }
|
|
95
97
|
},
|
|
98
|
+
uncertainty: {
|
|
99
|
+
code: 'spec-only',
|
|
100
|
+
needed: 'Whether the role misstates this element to assistive technology in practice.',
|
|
101
|
+
evidence: {
|
|
102
|
+
role,
|
|
103
|
+
element: tag,
|
|
104
|
+
source: 'ARIA in HTML permitted-roles table',
|
|
105
|
+
wcagSc: []
|
|
106
|
+
}
|
|
107
|
+
},
|
|
96
108
|
data: {
|
|
97
109
|
details: { reasonCode: 'ARIA_ROLE_NOT_ALLOWED_FOR_ELEMENT', role, element: tag }
|
|
98
110
|
}
|
|
@@ -103,15 +115,12 @@ function runInPage(ctx) {
|
|
|
103
115
|
if (applicableCount === 0) {
|
|
104
116
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
105
117
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
118
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
119
|
+
[],
|
|
120
|
+
occurrences,
|
|
121
|
+
rule.defaultSeverity || 'moderate'
|
|
122
|
+
);
|
|
123
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
module.exports = { id, meta, runInPage };
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* Using either braille-specific attribute as the ONLY naming mechanism
|
|
24
24
|
* leaves non-braille assistive technology (most screen readers, voice
|
|
25
25
|
* control, etc.) with no accessible name/role description at all.
|
|
26
|
+
* Reported at CANTTELL rather than FAIL: aria-brailleroledescription
|
|
27
|
+
* without aria-roledescription reaches no user at all, and a missing
|
|
28
|
+
* accessible name is the naming rules' decision for the roles that require
|
|
29
|
+
* one. The braille attribute being unpaired is worth surfacing, but it is
|
|
30
|
+
* not itself a criterion failing.
|
|
26
31
|
* @implementation-notes
|
|
27
32
|
* - `aria-braillelabel`/`aria-brailleroledescription` do not participate
|
|
28
33
|
* in the standard accessible-name computation, so
|
|
@@ -52,7 +57,7 @@ const meta = {
|
|
|
52
57
|
conformanceLevel: 'A'
|
|
53
58
|
}
|
|
54
59
|
],
|
|
55
|
-
defaultSeverity: '
|
|
60
|
+
defaultSeverity: 'moderate',
|
|
56
61
|
category: 'robust',
|
|
57
62
|
type: 'automatic',
|
|
58
63
|
defaultConfidence: 'high',
|
|
@@ -131,6 +136,12 @@ function runInPage(ctx) {
|
|
|
131
136
|
hintKey: 'ariaBrailleEquivalent_hint_fail',
|
|
132
137
|
params: { element: tag, attr: m.attr, requires: m.requires }
|
|
133
138
|
},
|
|
139
|
+
uncertainty: {
|
|
140
|
+
code: 'spec-only',
|
|
141
|
+
needed:
|
|
142
|
+
'Whether any user reaches this attribute, given no braille equivalent is exposed.',
|
|
143
|
+
evidence: { element: tag, attribute: m.attr, requires: m.requires }
|
|
144
|
+
},
|
|
134
145
|
data: {
|
|
135
146
|
details: {
|
|
136
147
|
reasonCode: 'BRAILLE_ATTR_WITHOUT_EQUIVALENT',
|
|
@@ -146,15 +157,12 @@ function runInPage(ctx) {
|
|
|
146
157
|
if (applicableCount === 0) {
|
|
147
158
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
148
159
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
160
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
161
|
+
[],
|
|
162
|
+
occurrences,
|
|
163
|
+
rule.defaultSeverity || 'moderate'
|
|
164
|
+
);
|
|
165
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
module.exports = { id, meta, runInPage };
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
* An element with `aria-errormessage` but `aria-invalid` absent or
|
|
18
18
|
* `"false"` silently drops the error message from the accessibility
|
|
19
19
|
* tree, authors almost always intend it to be exposed.
|
|
20
|
+
* Reported at CANTTELL rather than FAIL: aria-errormessage is only
|
|
21
|
+
* exposed once aria-invalid is set, so the reference is currently inert.
|
|
22
|
+
* Whether that costs the user anything depends on whether the message is
|
|
23
|
+
* conveyed some other way (visible text next to the field, aria-describedby),
|
|
24
|
+
* which static markup does not settle.
|
|
20
25
|
* @implementation-notes
|
|
21
26
|
* - This is narrow: the broader space is a table of many
|
|
22
27
|
* attribute/condition pairs. This rule implements only the one pairing
|
|
@@ -52,7 +57,7 @@ const meta = {
|
|
|
52
57
|
conformanceLevel: 'A'
|
|
53
58
|
}
|
|
54
59
|
],
|
|
55
|
-
defaultSeverity: '
|
|
60
|
+
defaultSeverity: 'moderate',
|
|
56
61
|
category: 'robust',
|
|
57
62
|
type: 'automatic',
|
|
58
63
|
defaultConfidence: 'high',
|
|
@@ -98,6 +103,11 @@ function runInPage(ctx) {
|
|
|
98
103
|
hintKey: 'ariaConditionalAttr_hint_fail',
|
|
99
104
|
params: { element: tag, ariaInvalid: invalidValue || '(absent)' }
|
|
100
105
|
},
|
|
106
|
+
uncertainty: {
|
|
107
|
+
code: 'spec-only',
|
|
108
|
+
needed: 'Whether the field is ever in an invalid state that should expose this message.',
|
|
109
|
+
evidence: { element: tag, ariaInvalid: invalidValue || null }
|
|
110
|
+
},
|
|
101
111
|
data: {
|
|
102
112
|
details: {
|
|
103
113
|
reasonCode: 'ARIA_ERRORMESSAGE_WITHOUT_TRUTHY_INVALID',
|
|
@@ -111,15 +121,12 @@ function runInPage(ctx) {
|
|
|
111
121
|
if (applicableCount === 0) {
|
|
112
122
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
113
123
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
124
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
125
|
+
[],
|
|
126
|
+
occurrences,
|
|
127
|
+
rule.defaultSeverity || 'moderate'
|
|
128
|
+
);
|
|
129
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
module.exports = { id, meta, runInPage };
|
|
@@ -161,6 +161,12 @@ function runInPage(ctx) {
|
|
|
161
161
|
hintKey: guidance.key,
|
|
162
162
|
params: { role }
|
|
163
163
|
},
|
|
164
|
+
uncertainty: {
|
|
165
|
+
code: 'spec-only',
|
|
166
|
+
needed:
|
|
167
|
+
'Whether this user-agent-reserved role misleads the assistive technology in use.',
|
|
168
|
+
evidence: { role, ariaStatus: 'discouraged-for-authors' }
|
|
169
|
+
},
|
|
164
170
|
data: {
|
|
165
171
|
details: { reasonCode: 'ARIA_ROLE_AUTHOR_DISCOURAGED', role, guidance: guidance.text }
|
|
166
172
|
}
|
|
@@ -178,6 +184,12 @@ function runInPage(ctx) {
|
|
|
178
184
|
hintKey: guidance.key,
|
|
179
185
|
params: { role }
|
|
180
186
|
},
|
|
187
|
+
uncertainty: {
|
|
188
|
+
code: 'spec-only',
|
|
189
|
+
needed:
|
|
190
|
+
'Whether the deprecated role is still honoured, and what it should be replaced with.',
|
|
191
|
+
evidence: { role, ariaStatus: 'deprecated' }
|
|
192
|
+
},
|
|
181
193
|
data: {
|
|
182
194
|
details: { reasonCode: 'ARIA_ROLE_DEPRECATED', role, guidance: guidance.text }
|
|
183
195
|
}
|
|
@@ -33,7 +33,7 @@ const meta = {
|
|
|
33
33
|
descriptionKey: 'ariaHiddenBody_description'
|
|
34
34
|
},
|
|
35
35
|
helpUrl: null,
|
|
36
|
-
tags: ['wcag2a', 'wcag131', 'wcag412', 'structure', 'atomic', 'automatic'],
|
|
36
|
+
tags: ['wcag2a', 'wcag131', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
37
37
|
wcagSc: ['1.3.1', '4.1.2'],
|
|
38
38
|
normativeMappings: [
|
|
39
39
|
{
|
|
@@ -370,6 +370,11 @@ function runInPage(ctx) {
|
|
|
370
370
|
hintKey: 'ariaProhibitedAttr_hint_cantTell_roleless',
|
|
371
371
|
params: { attr, element: tag }
|
|
372
372
|
},
|
|
373
|
+
uncertainty: {
|
|
374
|
+
code: 'judgement-required',
|
|
375
|
+
needed: 'Whether the element’s own content already serves as its label.',
|
|
376
|
+
evidence: { attribute: attr, element: tag, role: null, hasOwnContent: true }
|
|
377
|
+
},
|
|
373
378
|
data: {
|
|
374
379
|
details: {
|
|
375
380
|
reasonCode: 'ARIA_ATTR_PROHIBITED_ROLELESS_NEEDS_REVIEW',
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Container roles must not own an accessible-tree child with a disallowed role
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
|
-
* @sc
|
|
10
|
+
* @sc 1.3.1
|
|
11
11
|
* @applicability
|
|
12
12
|
* Applies to elements with an explicit, valid role that is one of the
|
|
13
13
|
* container roles with a documented "required owned elements" entry
|
|
@@ -126,14 +126,14 @@ const meta = {
|
|
|
126
126
|
descriptionKey: 'ariaProhibitedChildren_description'
|
|
127
127
|
},
|
|
128
128
|
helpUrl: null,
|
|
129
|
-
tags: ['wcag2a', '
|
|
130
|
-
wcagSc: ['
|
|
129
|
+
tags: ['wcag2a', 'wcag131', 'aria', 'structure', 'atomic', 'automatic'],
|
|
130
|
+
wcagSc: ['1.3.1'],
|
|
131
131
|
normativeMappings: [
|
|
132
132
|
{
|
|
133
133
|
standard: 'WCAG',
|
|
134
134
|
version: '2.2',
|
|
135
|
-
requirement: '
|
|
136
|
-
title: '
|
|
135
|
+
requirement: '1.3.1',
|
|
136
|
+
title: 'Info and Relationships',
|
|
137
137
|
conformanceLevel: 'A'
|
|
138
138
|
}
|
|
139
139
|
],
|
|
@@ -141,7 +141,7 @@ const meta = {
|
|
|
141
141
|
category: 'robust',
|
|
142
142
|
type: 'automatic',
|
|
143
143
|
defaultConfidence: 'medium',
|
|
144
|
-
coverage: { facetsBySc: { '
|
|
144
|
+
coverage: { facetsBySc: { '1.3.1': ['aria-role-owned-children-allowed'] } }
|
|
145
145
|
};
|
|
146
146
|
|
|
147
147
|
function runInPage(ctx) {
|
|
@@ -19,8 +19,22 @@
|
|
|
19
19
|
* native control's own state exposure already covers it; no aria-checked
|
|
20
20
|
* is required. helpers.aria.getNativeRoleForElement resolves this).
|
|
21
21
|
* @expectation
|
|
22
|
-
* Every required
|
|
22
|
+
* Every required state/property for that role is present and non-empty.
|
|
23
|
+
* Graded by whether ARIA supplies a stand-in for the missing attribute:
|
|
24
|
+
* - FAIL where it does not, so the state is simply not exposed
|
|
25
|
+
* (aria-checked on checkbox/radio/switch/menuitemcheckbox/menuitemradio,
|
|
26
|
+
* aria-valuenow on slider/scrollbar/meter and on a focusable separator).
|
|
27
|
+
* - CANTTELL where ARIA defines an implicit value the role falls back to
|
|
28
|
+
* (aria-expanded on combobox, aria-level on heading), so the role still
|
|
29
|
+
* exposes a value and only the author knows whether it is the right one.
|
|
23
30
|
* @implementation-notes
|
|
31
|
+
* - The implicit-value table is generated from aria-query's requiredProps by
|
|
32
|
+
* scripts/generate-aria-tables.js (REQUIRED_PROP_IMPLICIT_VALUES in
|
|
33
|
+
* src/core/aria-helpers.js), so the two tiers cannot drift apart from the
|
|
34
|
+
* spec by hand. ACT 4e8ab6 maps this rule's requirement to WAI-ARIA rather
|
|
35
|
+
* than to WCAG, and names 1.3.1/4.1.2 as "less strict" precisely because
|
|
36
|
+
* they "allow for fallback default values"; the cantTell tier is that
|
|
37
|
+
* carve-out, not a softening of the fail tier.
|
|
24
38
|
* - Scoped to REQUIRED_PROPS_BY_ROLE in src/core/aria-helpers.js,
|
|
25
39
|
* which only lists a required property when the spec is unambiguous and
|
|
26
40
|
* context-independent, see that file's header for the rationale.
|
|
@@ -131,7 +145,8 @@ function runInPage(ctx) {
|
|
|
131
145
|
? helpers.queryAllSmart('[role]')
|
|
132
146
|
: helpers.queryAll('[role]');
|
|
133
147
|
|
|
134
|
-
const
|
|
148
|
+
const failOccurrences = [];
|
|
149
|
+
const cantTellOccurrences = [];
|
|
135
150
|
let applicableCount = 0;
|
|
136
151
|
|
|
137
152
|
for (const el of nodes) {
|
|
@@ -178,8 +193,43 @@ function runInPage(ctx) {
|
|
|
178
193
|
if (!missing.length) continue;
|
|
179
194
|
|
|
180
195
|
for (const attr of missing) {
|
|
181
|
-
|
|
196
|
+
const implicit =
|
|
197
|
+
typeof ariaHelpers.getRequiredAttrImplicitValue === 'function'
|
|
198
|
+
? ariaHelpers.getRequiredAttrImplicitValue(role, attr)
|
|
199
|
+
: null;
|
|
200
|
+
|
|
201
|
+
if (implicit) {
|
|
202
|
+
cantTellOccurrences.push(
|
|
203
|
+
helpers.reportOccurrence(el, {
|
|
204
|
+
occurrenceOutcome: 'cantTell',
|
|
205
|
+
summary: `This attribute is required for this element’s role and is missing, but ARIA falls back to "${implicit}".`,
|
|
206
|
+
hint: 'Set the attribute explicitly if the implicit value is not the state you mean.',
|
|
207
|
+
i18n: {
|
|
208
|
+
summaryKey: 'ariaRequiredAttr_summary_cantTell',
|
|
209
|
+
hintKey: 'ariaRequiredAttr_hint_cantTell',
|
|
210
|
+
params: { attr, role, implicit }
|
|
211
|
+
},
|
|
212
|
+
uncertainty: {
|
|
213
|
+
code: 'spec-only',
|
|
214
|
+
needed: 'Whether the implicit fallback is the state the author meant.',
|
|
215
|
+
evidence: { attribute: attr, role, implicitValue: implicit }
|
|
216
|
+
},
|
|
217
|
+
data: {
|
|
218
|
+
details: {
|
|
219
|
+
reasonCode: 'ARIA_ATTR_REQUIRED_MISSING_IMPLICIT',
|
|
220
|
+
attr,
|
|
221
|
+
role,
|
|
222
|
+
implicitValue: implicit
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
failOccurrences.push(
|
|
182
231
|
helpers.reportOccurrence(el, {
|
|
232
|
+
occurrenceOutcome: 'fail',
|
|
183
233
|
summary: 'This attribute is required for this element’s role, but is missing.',
|
|
184
234
|
hint: 'Add this attribute with a valid value for this role.',
|
|
185
235
|
i18n: {
|
|
@@ -198,15 +248,12 @@ function runInPage(ctx) {
|
|
|
198
248
|
if (applicableCount === 0) {
|
|
199
249
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
200
250
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
251
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
252
|
+
failOccurrences,
|
|
253
|
+
cantTellOccurrences,
|
|
254
|
+
rule.defaultSeverity || 'serious'
|
|
255
|
+
);
|
|
256
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
210
257
|
}
|
|
211
258
|
|
|
212
259
|
module.exports = { id, meta, runInPage };
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Container roles that require specific owned elements must contain at least one
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
|
-
* @sc
|
|
10
|
+
* @sc 1.3.1
|
|
11
11
|
* @applicability
|
|
12
12
|
* Applies to elements with an explicit, valid, non-abstract role that is
|
|
13
13
|
* also one of the container roles with a documented "required owned
|
|
@@ -15,7 +15,17 @@
|
|
|
15
15
|
* table, grid, treegrid, tablist, tree, row).
|
|
16
16
|
* @expectation
|
|
17
17
|
* At least one descendant, or one aria-owns-referenced element, has one
|
|
18
|
-
* of the acceptable owned roles for that container role.
|
|
18
|
+
* of the acceptable owned roles for that container role. Reported at
|
|
19
|
+
* CANTTELL, never FAIL: this rule asks only whether the required content
|
|
20
|
+
* is PRESENT, and a container that owns nothing conveys nothing false --
|
|
21
|
+
* an empty role="list" is announced as a list with no items, which is what
|
|
22
|
+
* it is. Whether the content a container does own is VALID is
|
|
23
|
+
* aria-prohibited-children's decision, and that rule still fails, so a
|
|
24
|
+
* genuinely misdescribed structure (a role="button" among list items, a
|
|
25
|
+
* tablist of plain buttons) is caught with the same strength as before.
|
|
26
|
+
* The native-HTML equivalents already work this way: nothing in this
|
|
27
|
+
* ruleset fails an empty <ul>, and list-children-valid judges only the
|
|
28
|
+
* children that exist.
|
|
19
29
|
* @implementation-notes
|
|
20
30
|
* - Scoped to REQUIRED_OWNED_ROLES in src/core/aria-helpers.js
|
|
21
31
|
* (see that file's header for the conservative-scope rationale).
|
|
@@ -75,14 +85,14 @@ const meta = {
|
|
|
75
85
|
descriptionKey: 'ariaRequiredChildren_description'
|
|
76
86
|
},
|
|
77
87
|
helpUrl: null,
|
|
78
|
-
tags: ['wcag2a', '
|
|
79
|
-
wcagSc: ['
|
|
88
|
+
tags: ['wcag2a', 'wcag131', 'aria', 'structure', 'atomic', 'automatic'],
|
|
89
|
+
wcagSc: ['1.3.1'],
|
|
80
90
|
normativeMappings: [
|
|
81
91
|
{
|
|
82
92
|
standard: 'WCAG',
|
|
83
93
|
version: '2.2',
|
|
84
|
-
requirement: '
|
|
85
|
-
title: '
|
|
94
|
+
requirement: '1.3.1',
|
|
95
|
+
title: 'Info and Relationships',
|
|
86
96
|
conformanceLevel: 'A'
|
|
87
97
|
}
|
|
88
98
|
],
|
|
@@ -90,7 +100,7 @@ const meta = {
|
|
|
90
100
|
category: 'robust',
|
|
91
101
|
type: 'automatic',
|
|
92
102
|
defaultConfidence: 'medium',
|
|
93
|
-
coverage: { facetsBySc: { '
|
|
103
|
+
coverage: { facetsBySc: { '1.3.1': ['aria-role-required-owned-children'] } }
|
|
94
104
|
};
|
|
95
105
|
|
|
96
106
|
function runInPage(ctx) {
|
|
@@ -269,6 +279,16 @@ function runInPage(ctx) {
|
|
|
269
279
|
hintKey: 'ariaRequiredChildren_hint_fail',
|
|
270
280
|
params: { role, requiredRoles: requiredOwned.join(', ') }
|
|
271
281
|
},
|
|
282
|
+
uncertainty: {
|
|
283
|
+
code: 'spec-only',
|
|
284
|
+
needed:
|
|
285
|
+
'Whether this container is legitimately empty, or holds items that never got their role.',
|
|
286
|
+
evidence: {
|
|
287
|
+
role,
|
|
288
|
+
requiredOwnedRoles: requiredOwned,
|
|
289
|
+
childElementCount: el.children ? el.children.length : null
|
|
290
|
+
}
|
|
291
|
+
},
|
|
272
292
|
data: {
|
|
273
293
|
details: {
|
|
274
294
|
reasonCode: 'ARIA_REQUIRED_CHILD_MISSING',
|
|
@@ -283,15 +303,12 @@ function runInPage(ctx) {
|
|
|
283
303
|
if (applicableCount === 0) {
|
|
284
304
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
285
305
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
306
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
307
|
+
[],
|
|
308
|
+
occurrences,
|
|
309
|
+
rule.defaultSeverity || 'moderate'
|
|
310
|
+
);
|
|
311
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
295
312
|
}
|
|
296
313
|
|
|
297
314
|
module.exports = { id, meta, runInPage };
|