@surea11y/core 1.2.0 → 1.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.
Files changed (168) hide show
  1. package/CHANGELOG.md +81 -7
  2. package/LICENSE +373 -21
  3. package/README.md +175 -35
  4. package/bin/surea11y-core.js +20 -0
  5. package/docs/API_STABILITY.md +27 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/ENGINE_OPTIONS.md +2 -0
  9. package/docs/I18N.md +12 -9
  10. package/docs/INTEGRATION.md +19 -1
  11. package/docs/LIMITATIONS.md +1 -1
  12. package/docs/OUTPUT_SCHEMA.md +1 -1
  13. package/docs/REPORT.md +1 -1
  14. package/docs/RULE_CATALOG.md +1 -1
  15. package/docs/SARIF.md +59 -0
  16. package/package.json +63 -18
  17. package/src/baseline.js +0 -0
  18. package/src/checks/automatic/area-alt-present.js +63 -31
  19. package/src/checks/automatic/aria-allowed-attr.js +204 -80
  20. package/src/checks/automatic/aria-allowed-role.js +23 -7
  21. package/src/checks/automatic/aria-braille-equivalent.js +34 -10
  22. package/src/checks/automatic/aria-conditional-attr.js +32 -14
  23. package/src/checks/automatic/aria-deprecated-role.js +26 -11
  24. package/src/checks/automatic/aria-hidden-body.js +48 -23
  25. package/src/checks/automatic/aria-hidden-focus.js +420 -66
  26. package/src/checks/automatic/aria-prohibited-attr.js +327 -60
  27. package/src/checks/automatic/aria-prohibited-children.js +111 -103
  28. package/src/checks/automatic/aria-required-attr.js +29 -15
  29. package/src/checks/automatic/aria-required-children.js +44 -24
  30. package/src/checks/automatic/aria-required-parent.js +64 -35
  31. package/src/checks/automatic/aria-role-name-present.js +49 -21
  32. package/src/checks/automatic/aria-roles-valid.js +24 -12
  33. package/src/checks/automatic/aria-valid-attr-value.js +46 -22
  34. package/src/checks/automatic/aria-valid-attr.js +19 -5
  35. package/src/checks/automatic/autocomplete-valid.js +76 -16
  36. package/src/checks/automatic/avoid-inline-spacing.js +23 -8
  37. package/src/checks/automatic/binary-control-name-present.js +62 -50
  38. package/src/checks/automatic/button-name-present.js +54 -24
  39. package/src/checks/automatic/bypass-blocks-present.js +51 -32
  40. package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
  41. package/src/checks/automatic/combobox-name-present.js +40 -45
  42. package/src/checks/automatic/contrast-computable.js +363 -341
  43. package/src/checks/automatic/contrast-enhanced.js +489 -466
  44. package/src/checks/automatic/contrast-minimum.js +488 -465
  45. package/src/checks/automatic/css-orientation-lock.js +51 -35
  46. package/src/checks/automatic/definition-list-children-valid.js +46 -25
  47. package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
  48. package/src/checks/automatic/dialog-name-present.js +47 -85
  49. package/src/checks/automatic/dlitem-parent-valid.js +25 -8
  50. package/src/checks/automatic/duplicate-id-aria.js +28 -9
  51. package/src/checks/automatic/embed-text-alternative-present.js +88 -35
  52. package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
  53. package/src/checks/automatic/form-control-single-label.js +50 -14
  54. package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
  55. package/src/checks/automatic/iframe-focusable-content.js +265 -22
  56. package/src/checks/automatic/iframe-name-present.js +33 -9
  57. package/src/checks/automatic/iframe-title-unique.js +32 -9
  58. package/src/checks/automatic/img-alt-present.js +54 -52
  59. package/src/checks/automatic/input-image-alt-present.js +141 -112
  60. package/src/checks/automatic/label-in-name.js +65 -41
  61. package/src/checks/automatic/language-page-present.js +111 -109
  62. package/src/checks/automatic/link-in-text-block.js +61 -19
  63. package/src/checks/automatic/link-name-present.js +47 -14
  64. package/src/checks/automatic/list-children-valid.js +40 -33
  65. package/src/checks/automatic/listbox-name-present.js +41 -19
  66. package/src/checks/automatic/listitem-parent-valid.js +48 -13
  67. package/src/checks/automatic/menuitem-name-present.js +41 -61
  68. package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
  69. package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
  70. package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
  71. package/src/checks/automatic/meter-name-present.js +40 -36
  72. package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
  73. package/src/checks/automatic/object-text-alternative-present.js +93 -39
  74. package/src/checks/automatic/option-name-present.js +40 -21
  75. package/src/checks/automatic/page-title-present.js +19 -6
  76. package/src/checks/automatic/progressbar-name-present.js +49 -44
  77. package/src/checks/automatic/role-img-alt-present.js +211 -159
  78. package/src/checks/automatic/searchbox-name-present.js +41 -19
  79. package/src/checks/automatic/server-side-image-map-absent.js +27 -11
  80. package/src/checks/automatic/slider-name-present.js +42 -47
  81. package/src/checks/automatic/spinbutton-name-present.js +41 -19
  82. package/src/checks/automatic/summary-name-present.js +39 -17
  83. package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
  84. package/src/checks/automatic/svg-text-alternative-present.js +262 -230
  85. package/src/checks/automatic/tab-name-present.js +39 -60
  86. package/src/checks/automatic/table-headers-attr-valid.js +27 -10
  87. package/src/checks/automatic/table-th-has-data-cells.js +24 -8
  88. package/src/checks/automatic/target-size-minimum.js +123 -48
  89. package/src/checks/automatic/td-has-header.js +53 -12
  90. package/src/checks/automatic/textbox-name-present.js +41 -19
  91. package/src/checks/automatic/tooltip-name-present.js +39 -18
  92. package/src/checks/automatic/treeitem-name-present.js +40 -21
  93. package/src/checks/automatic/valid-lang.js +22 -6
  94. package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
  95. package/src/checks/manual/accesskeys-manual.js +17 -6
  96. package/src/checks/manual/area-alt-decorative-manual.js +194 -193
  97. package/src/checks/manual/area-alt-quality-manual.js +184 -141
  98. package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
  99. package/src/checks/manual/aria-text-manual.js +20 -11
  100. package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
  101. package/src/checks/manual/css-hidden-focus.js +375 -169
  102. package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
  103. package/src/checks/manual/empty-heading-manual.js +41 -24
  104. package/src/checks/manual/empty-table-header-manual.js +69 -31
  105. package/src/checks/manual/focus-order-semantics-manual.js +60 -13
  106. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
  107. package/src/checks/manual/heading-order-manual.js +50 -8
  108. package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
  109. package/src/checks/manual/image-redundant-alt-manual.js +38 -8
  110. package/src/checks/manual/img-alt-decorative-manual.js +133 -96
  111. package/src/checks/manual/img-alt-quality-manual.js +178 -127
  112. package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
  113. package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
  114. package/src/checks/manual/label-title-only-manual.js +44 -28
  115. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
  116. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
  117. package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
  118. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
  119. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
  120. package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
  121. package/src/checks/manual/landmark-one-main-manual.js +38 -43
  122. package/src/checks/manual/landmark-unique-manual.js +78 -67
  123. package/src/checks/manual/link-name-quality-manual.js +45 -12
  124. package/src/checks/manual/media-transcript-present-manual.js +37 -22
  125. package/src/checks/manual/meta-viewport-large-manual.js +19 -6
  126. package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
  127. package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
  128. package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
  129. package/src/checks/manual/p-as-heading-manual.js +24 -7
  130. package/src/checks/manual/page-has-heading-one-manual.js +42 -32
  131. package/src/checks/manual/page-title-patterns-manual.js +80 -50
  132. package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
  133. package/src/checks/manual/region-manual.js +244 -60
  134. package/src/checks/manual/scope-attr-valid-manual.js +13 -4
  135. package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
  136. package/src/checks/manual/skip-link-manual.js +42 -18
  137. package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
  138. package/src/checks/manual/tabindex-manual.js +13 -4
  139. package/src/checks/manual/table-duplicate-name-manual.js +22 -11
  140. package/src/checks/manual/table-fake-caption-manual.js +48 -10
  141. package/src/checks/manual/video-caption-manual.js +17 -4
  142. package/src/checks/manual-review.js +58 -12
  143. package/src/core.js +41705 -29650
  144. package/src/index.js +2 -0
  145. package/src/report.js +109 -47
  146. package/src/sarif.js +190 -0
  147. package/surea11y.browser.js +37774 -0
  148. package/bin/core.js +0 -348
  149. package/docs/CLI.md +0 -75
  150. package/src/catalogs/composites.wcag.js +0 -490
  151. package/src/checks/rules-and-tags.full.csv +0 -19
  152. package/src/checks/rules-and-tags.full.json +0 -259
  153. package/src/core/aria-helpers.js +0 -970
  154. package/src/core/contrast-helpers.js +0 -1147
  155. package/src/core/dom-helpers.js +0 -4235
  156. package/src/core/dom-runner.js +0 -671
  157. package/src/core/frame-messaging.js +0 -210
  158. package/src/core/frame-scan.js +0 -178
  159. package/src/core/rollup-composites.js +0 -135
  160. package/src/core/rule-meta.js +0 -159
  161. package/src/coverage/wcag-facets.js +0 -1079
  162. package/src/coverage/wcag-version-map.js +0 -84
  163. package/src/i18n/en.js +0 -923
  164. package/src/i18n/fr.js +0 -844
  165. package/src/policy/contracts.js +0 -18
  166. package/src/policy/resolvePolicy.js +0 -55
  167. package/src/policy/schemas/engine-options.schema.json +0 -103
  168. package/src/policy/schemas/policy-contract.schema.json +0 -40
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -11,20 +13,26 @@
11
13
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
14
  */
13
15
 
14
- const id = "area-alt-quality";
16
+ const id = 'area-alt-quality';
15
17
 
16
18
  const meta = {
17
- title: "<area> alt text must be appropriate (manual review)",
18
- description: "Flags <area> elements with non-empty alt text for human review of appropriateness.",
19
+ title: '<area> alt text must be appropriate (manual review)',
20
+ description: 'Flags <area> elements with non-empty alt text for human review of appropriateness.',
19
21
  i18n: {
20
- titleKey: "area_altQuality_title",
21
- descriptionKey: "area_altQuality_description"
22
+ titleKey: 'area_altQuality_title',
23
+ descriptionKey: 'area_altQuality_description'
22
24
  },
23
25
  helpUrl: null,
24
- tags: ["wcag2a", "wcag111", "nontext", "images", "imagemap", "manual", "atomic"],
26
+ tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'imagemap', 'manual', 'atomic'],
25
27
  wcagSc: ['1.1.1'],
26
28
  normativeMappings: [
27
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
29
+ {
30
+ standard: 'WCAG',
31
+ version: '2.2',
32
+ requirement: '1.1.1',
33
+ title: 'Non-text Content',
34
+ conformanceLevel: 'A'
35
+ }
28
36
  ],
29
37
  defaultSeverity: 'minor',
30
38
  category: 'perceivable',
@@ -38,167 +46,202 @@ const meta = {
38
46
  };
39
47
 
40
48
  function runInPage(ctx) {
49
+ const { document, root, helpers, rule } = ctx;
50
+ const safeRoot = root || document;
51
+
52
+ const queryAllSmart =
53
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
54
+ const queryAll =
55
+ helpers && typeof helpers.queryAll === 'function'
56
+ ? helpers.queryAll
57
+ : (sel) => {
58
+ try {
59
+ return safeRoot && safeRoot.querySelectorAll
60
+ ? Array.from(safeRoot.querySelectorAll(sel))
61
+ : [];
62
+ } catch {
63
+ return [];
64
+ }
65
+ };
41
66
 
42
- const { document, root, helpers, rule } = ctx;
43
- const safeRoot = root || document;
67
+ const getEligibilityInfo =
68
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
44
69
 
45
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
46
- const queryAll = helpers && typeof helpers.queryAll === 'function'
47
- ? helpers.queryAll
48
- : (sel) => {
49
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
50
- catch { return []; }
51
- };
70
+ const isAccTreeEligible =
71
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
52
72
 
53
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
54
- ? helpers.getEligibilityInfo
55
- : null;
56
-
57
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
58
- ? helpers.isAccTreeEligible
59
- : null;
60
-
61
- const __accEligCache = new WeakMap();
62
- function accEligibleCached(node) {
63
- if (!isAccTreeEligible) return { eligible: true, reasons: [] };
64
- if (!node || typeof node !== 'object') return { eligible: true, reasons: [] };
65
- const c = __accEligCache.get(node);
66
- if (c) return c;
67
- let r;
68
- try { r = isAccTreeEligible(node, ctx); } catch { r = { eligible: true, reasons: [] }; }
69
- r = r && typeof r === 'object' ? r : { eligible: !!r, reasons: [] };
70
- __accEligCache.set(node, r);
71
- return r;
73
+ const __accEligCache = new WeakMap();
74
+ function accEligibleCached(node) {
75
+ if (!isAccTreeEligible) return { eligible: true, reasons: [] };
76
+ if (!node || typeof node !== 'object') return { eligible: true, reasons: [] };
77
+ const c = __accEligCache.get(node);
78
+ if (c) return c;
79
+ let r;
80
+ try {
81
+ r = isAccTreeEligible(node, ctx);
82
+ } catch {
83
+ r = { eligible: true, reasons: [] };
72
84
  }
85
+ r = r && typeof r === 'object' ? r : { eligible: !!r, reasons: [] };
86
+ __accEligCache.set(node, r);
87
+ return r;
88
+ }
73
89
 
74
- // --- image-map semantics (rule-local; match automatic <area> applicability) ---
75
- function normUsemap(val) {
90
+ // --- image-map semantics (rule-local; match automatic <area> applicability) ---
91
+ function normUsemap(val) {
76
92
  try {
77
- const t = String(val || '').trim();
78
- if (!t) return '';
79
- return (t[0] === '#') ? t.slice(1).trim().toLowerCase() : t.toLowerCase();
80
- } catch { return ''; }
81
- }
82
- function getMapName(mapEl) {
93
+ const t = String(val || '').trim();
94
+ if (!t) return '';
95
+ return t[0] === '#' ? t.slice(1).trim().toLowerCase() : t.toLowerCase();
96
+ } catch {
97
+ return '';
98
+ }
99
+ }
100
+ function getMapName(mapEl) {
83
101
  try {
84
- if (!mapEl || !mapEl.getAttribute) return '';
85
- const n = String(mapEl.getAttribute('name') || mapEl.getAttribute('id') || '').trim();
86
- return n ? n.toLowerCase() : '';
87
- } catch { return ''; }
88
- }
102
+ if (!mapEl || !mapEl.getAttribute) return '';
103
+ const n = String(mapEl.getAttribute('name') || mapEl.getAttribute('id') || '').trim();
104
+ return n ? n.toLowerCase() : '';
105
+ } catch {
106
+ return '';
107
+ }
108
+ }
89
109
 
90
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
91
- ? helpers.getFocusableInfo
92
- : null;
93
-
94
- function isRolePresentationExcluded(el) {
95
- const role = (() => {
96
- try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
97
- catch { return ''; }
98
- })();
99
- if (role !== 'presentation' && role !== 'none') return false;
100
-
101
- // Exclude only when NOT focusable (mirrors img-alt-present policy)
102
- let focusable = false;
103
- if (getFocusableInfo) {
104
- const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
105
- focusable = !!(fi && fi.focusable);
106
- } else {
107
- const tabindex = el.getAttribute('tabindex');
108
- focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
110
+ const getFocusableInfo =
111
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
112
+
113
+ function isRolePresentationExcluded(el) {
114
+ const role = (() => {
115
+ try {
116
+ return String(el.getAttribute('role') || '')
117
+ .trim()
118
+ .toLowerCase();
119
+ } catch {
120
+ return '';
121
+ }
122
+ })();
123
+ if (role !== 'presentation' && role !== 'none') return false;
124
+
125
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
126
+ let focusable;
127
+ if (getFocusableInfo) {
128
+ const fi = (() => {
129
+ try {
130
+ return getFocusableInfo(el, ctx);
131
+ } catch {
132
+ return null;
109
133
  }
110
- return !focusable;
134
+ })();
135
+ focusable = !!(fi && fi.focusable);
136
+ } else {
137
+ const tabindex = el.getAttribute('tabindex');
138
+ focusable =
139
+ tabindex != null &&
140
+ String(tabindex).trim() !== '' &&
141
+ !Number.isNaN(Number(String(tabindex).trim()));
111
142
  }
143
+ return !focusable;
144
+ }
112
145
 
146
+ const els = (() => {
147
+ try {
148
+ return Array.from((queryAllSmart ? queryAllSmart('area') : queryAll('area')) || []);
149
+ } catch {
150
+ return queryAll('area');
151
+ }
152
+ })();
113
153
 
114
- const els = (() => {
115
- try { return Array.from((queryAllSmart ? queryAllSmart('area') : queryAll('area')) || []); }
116
- catch { return queryAll('area'); }
117
- })();
154
+ if (!els.length) {
155
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
156
+ }
157
+
158
+ const occurrences = [];
159
+ let applicableCount = 0;
118
160
 
119
- if (!els.length) {
120
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
161
+ const __usemapIndex = new Map(); // mapName -> img (first in document order)
162
+ try {
163
+ const imgs = Array.from(document.querySelectorAll('img[usemap]'));
164
+ for (const img of imgs) {
165
+ const u = normUsemap(img.getAttribute('usemap'));
166
+ if (!u) continue;
167
+ if (!__usemapIndex.has(u)) __usemapIndex.set(u, img);
121
168
  }
169
+ } catch {}
122
170
 
123
- const occurrences = [];
124
- let applicableCount = 0;
171
+ for (const el of els) {
172
+ if (!el || !el.getAttribute) continue;
125
173
 
126
- const __usemapIndex = new Map(); // mapName -> img (first in document order)
174
+ // Must belong to a *used* image map (referenced by an <img usemap>). If unused, not applicable.
175
+ let img;
127
176
  try {
128
- const imgs = Array.from(document.querySelectorAll('img[usemap]'));
129
- for (const img of imgs) {
130
- const u = normUsemap(img.getAttribute('usemap'));
131
- if (!u) continue;
132
- if (!__usemapIndex.has(u)) __usemapIndex.set(u, img);
133
- }
134
- } catch {}
177
+ const map = el.closest && el.closest('map');
178
+ const mapName = map ? getMapName(map) : '';
179
+ img = mapName ? __usemapIndex.get(mapName) || null : null;
180
+ } catch {
181
+ img = null;
182
+ }
183
+ if (!img) continue;
135
184
 
136
- for (const el of els) {
137
- if (!el || !el.getAttribute) continue;
185
+ // The referencing <img> must be eligible in the accessibility tree.
186
+ if (isAccTreeEligible) {
187
+ const imgElig = accEligibleCached(img);
188
+ if (imgElig && imgElig.eligible === false) continue;
189
+ }
138
190
 
139
- // Must belong to a *used* image map (referenced by an <img usemap>). If unused, not applicable.
140
- let img = null;
141
- try {
142
- const map = el.closest && el.closest('map');
143
- const mapName = map ? getMapName(map) : '';
144
- img = mapName ? (__usemapIndex.get(mapName) || null) : null;
145
- } catch {
146
- img = null;
147
- }
148
- if (!img) continue;
191
+ if (isAccTreeEligible) {
192
+ const elig = accEligibleCached(el);
193
+ if (elig && elig.eligible === false) continue;
194
+ }
149
195
 
150
- // The referencing <img> must be eligible in the accessibility tree.
151
- if (isAccTreeEligible) {
152
- const imgElig = accEligibleCached(img);
153
- if (imgElig && imgElig.eligible === false) continue;
154
- }
196
+ if (isRolePresentationExcluded(el)) continue;
155
197
 
156
- if (isAccTreeEligible) {
157
- const elig = accEligibleCached(el);
158
- if (elig && elig.eligible === false) continue;
159
- }
198
+ // Rule-specific applicability (only elements that already have a text alternative mechanism)
199
+ let alt;
200
+ try {
201
+ alt = el.getAttribute('alt');
202
+ } catch {
203
+ alt = null;
204
+ }
205
+ if (alt === null) continue;
206
+ if (String(alt).trim() === '') continue; // only non-empty alt is applicable here
160
207
 
161
- if (isRolePresentationExcluded(el)) continue;
162
-
163
- // Rule-specific applicability (only elements that already have a text alternative mechanism)
164
- let alt = null;
165
- try { alt = el.getAttribute('alt'); } catch { alt = null; }
166
- if (alt === null) continue;
167
- if (String(alt).trim() === '') continue; // only non-empty alt is applicable here
168
-
169
- applicableCount += 1;
170
-
171
- const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
172
-
173
- let altVal = '';
174
- try { altVal = String(el.getAttribute('alt') || ''); } catch { altVal = ''; }
175
-
176
- const baseOccurrence = {
177
- summary: 'Review alt text on <area> for accuracy and appropriateness.',
178
- hint: 'Ensure the alt text identifies the destination/action of the image map area in context.',
179
- i18n: {
180
- summaryKey: 'area_altQuality_summary_cantTell',
181
- hintKey: 'area_altQuality_hint_cantTell',
182
- params: { element: (el.tagName || '').toLowerCase() }
183
- },
184
- data: {
185
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
186
- details: { alt: altVal.trim() } // optional but useful for manual review
187
- }
188
- };
208
+ applicableCount += 1;
189
209
 
190
- if (helpers && typeof helpers.reportOccurrence === 'function') {
191
- occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
192
- } else {
193
- occurrences.push({ selector: '', html: '', ...baseOccurrence });
194
- }
210
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
211
+
212
+ let altVal;
213
+ try {
214
+ altVal = String(el.getAttribute('alt') || '');
215
+ } catch {
216
+ altVal = '';
195
217
  }
196
218
 
197
- if (applicableCount === 0) {
198
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
219
+ const baseOccurrence = {
220
+ summary: 'Review alt text on <area> for accuracy and appropriateness.',
221
+ hint: 'Ensure the alt text identifies the destination/action of the image map area in context.',
222
+ i18n: {
223
+ summaryKey: 'area_altQuality_summary_cantTell',
224
+ hintKey: 'area_altQuality_hint_cantTell',
225
+ params: { element: (el.tagName || '').toLowerCase() }
226
+ },
227
+ data: {
228
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
229
+ details: { alt: altVal.trim() } // optional but useful for manual review
230
+ }
231
+ };
232
+
233
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
234
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
235
+ } else {
236
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
199
237
  }
238
+ }
239
+
240
+ if (applicableCount === 0) {
241
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
242
+ }
200
243
 
201
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
244
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
202
245
  }
203
246
 
204
247
  module.exports = { id, meta, runInPage };
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -14,34 +16,23 @@
14
16
  * checked state is already exposed natively), but when an author sets
15
17
  * it explicitly it should agree with the element's actual state —
16
18
  * otherwise assistive technology is told something different from what
17
- * a sighted user perceives. This is the same static/live-DOM check
18
- * a widely-used reference engine's own `aria-conditional-attr` rule
19
- * performs (verified directly against its `ariaConditionalCheckboxAttr`/
20
- * `ariaConditionalRadioAttr` source).
19
+ * a sighted user perceives.
21
20
  * @implementation-notes
22
21
  * - Deliberately authored as `type: 'manual'` (cantTell-capped, never
23
- * fail), unlike most ARIA-validity rules in this file family. That
24
- * reference engine runs as a script injected into the LIVE, already-hydrated DOM, so a
25
- * mismatch it reports is a genuine, current-state fact. This engine
26
- * analyzes STATIC markup only (no script execution) — `.checked`
22
+ * fail), unlike most ARIA-validity rules in this file family. This
23
+ * engine analyzes STATIC markup only (no script execution) — `.checked`
27
24
  * reliably reflects the static `checked` attribute for freshly-parsed
28
- * markup (verified directly against jsdom), but a very common, entirely
29
- * legitimate real-world pattern is a JS-hydrated widget whose
30
- * server-rendered HTML intentionally ships `aria-checked="true"` (or
31
- * `"mixed"`) BEFORE client JS sets the native `checked`/`indeterminate`
32
- * property to match on hydration. A hard `fail` here would misfire on
33
- * that pattern constantly — real risk given how common it is in exactly
34
- * the kind of JS-framework-heavy pages this engine's own real-world
35
- * corpus is full of. Flagging for human review (cantTell) captures the
36
- * same signal without the false-positive risk of asserting it as a
37
- * confirmed violation.
25
+ * markup, but a very common, entirely legitimate real-world pattern is a
26
+ * JS-hydrated widget whose server-rendered HTML intentionally ships
27
+ * `aria-checked="true"` (or `"mixed"`) BEFORE client JS sets the native
28
+ * `checked`/`indeterminate` property to match on hydration. A hard
29
+ * `fail` here would misfire on that pattern constantly. Flagging for
30
+ * human review (cantTell) captures the same signal without the
31
+ * false-positive risk of asserting it as a confirmed violation.
38
32
  * - `aria-checked="mixed"` is only meaningful for checkboxes (radio
39
- * buttons have no indeterminate state) — matches that reference engine's
40
- * own `normalizeAriaChecked` (radio) vs `normalizeAriaChecked2` (checkbox)
41
- * split, verified directly against its source.
33
+ * buttons have no indeterminate state).
42
34
  * - Any `aria-checked` value other than "true"/"false"/"mixed" (checkbox)
43
- * or "true"/"false" (radio) is treated as equivalent to "false" — also
44
- * matches that reference engine's own normalization exactly.
35
+ * or "true"/"false" (radio) is treated as equivalent to "false".
45
36
  * - Not rule-gated on isAccTreeEligible: whether the accessible state
46
37
  * matches is still a markup-correctness property, while engine-level
47
38
  * hidden-subtree filtering applies unless engineOptions.includeHiddenElements
@@ -52,7 +43,8 @@ const id = 'aria-checked-state-mismatch';
52
43
 
53
44
  const meta = {
54
45
  title: 'Native checkbox/radio aria-checked should match its actual state',
55
- description: 'Flags a native <input type="checkbox">/<input type="radio"> whose explicit aria-checked value disagrees with its actual checked/indeterminate state, for manual review.',
46
+ description:
47
+ 'Flags a native <input type="checkbox">/<input type="radio"> whose explicit aria-checked value disagrees with its actual checked/indeterminate state, for manual review.',
56
48
  i18n: {
57
49
  titleKey: 'ariaCheckedStateMismatch_title',
58
50
  descriptionKey: 'ariaCheckedStateMismatch_description'
@@ -61,7 +53,13 @@ const meta = {
61
53
  tags: ['wcag2a', 'wcag412', 'aria', 'forms', 'atomic', 'manual'],
62
54
  wcagSc: ['4.1.2'],
63
55
  normativeMappings: [
64
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
56
+ {
57
+ standard: 'WCAG',
58
+ version: '2.2',
59
+ requirement: '4.1.2',
60
+ title: 'Name, Role, Value',
61
+ conformanceLevel: 'A'
62
+ }
65
63
  ],
66
64
  defaultSeverity: 'moderate',
67
65
  category: 'robust',
@@ -71,12 +69,16 @@ const meta = {
71
69
  };
72
70
 
73
71
  function runInPage(ctx) {
74
- const { document, helpers, rule } = ctx;
72
+ const { helpers, rule } = ctx;
75
73
 
76
- function trim(v) { return (v == null ? '' : String(v)).trim(); }
74
+ function trim(v) {
75
+ return (v == null ? '' : String(v)).trim();
76
+ }
77
77
 
78
78
  const nodes = helpers.queryAllSmart
79
- ? helpers.queryAllSmart('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]')
79
+ ? helpers.queryAllSmart(
80
+ 'input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]'
81
+ )
80
82
  : helpers.queryAll('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]');
81
83
 
82
84
  const occurrences = [];
@@ -97,9 +99,10 @@ function runInPage(ctx) {
97
99
 
98
100
  let normalizedAriaChecked;
99
101
  if (isCheckbox) {
100
- normalizedAriaChecked = (rawAriaChecked === 'mixed' || rawAriaChecked === 'true') ? rawAriaChecked : 'false';
102
+ normalizedAriaChecked =
103
+ rawAriaChecked === 'mixed' || rawAriaChecked === 'true' ? rawAriaChecked : 'false';
101
104
  } else {
102
- normalizedAriaChecked = (rawAriaChecked === 'true') ? 'true' : 'false';
105
+ normalizedAriaChecked = rawAriaChecked === 'true' ? 'true' : 'false';
103
106
  }
104
107
 
105
108
  let actualState;
@@ -112,12 +115,13 @@ function runInPage(ctx) {
112
115
  if (normalizedAriaChecked === actualState) continue;
113
116
 
114
117
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
115
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
118
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
116
119
 
117
120
  occurrences.push({
118
121
  selector: stableSelector,
119
122
  html,
120
- summary: 'This element’s aria-checked value does not match its actual checked/indeterminate state.',
123
+ summary:
124
+ 'This element’s aria-checked value does not match its actual checked/indeterminate state.',
121
125
  hint: 'Set aria-checked to match the element’s real state, or remove it — a native checkbox/radio already exposes this state without it.',
122
126
  i18n: {
123
127
  summaryKey: 'ariaCheckedStateMismatch_summary_cantTell',
@@ -125,7 +129,12 @@ function runInPage(ctx) {
125
129
  params: { ariaChecked: normalizedAriaChecked, actualState, type }
126
130
  },
127
131
  data: {
128
- details: { reasonCode: 'ARIA_CHECKED_STATE_MISMATCH', ariaChecked: normalizedAriaChecked, actualState, type }
132
+ details: {
133
+ reasonCode: 'ARIA_CHECKED_STATE_MISMATCH',
134
+ ariaChecked: normalizedAriaChecked,
135
+ actualState,
136
+ type
137
+ }
129
138
  }
130
139
  });
131
140
  }
@@ -134,7 +143,12 @@ function runInPage(ctx) {
134
143
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
135
144
  }
136
145
  if (occurrences.length) {
137
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'moderate', occurrences };
146
+ return {
147
+ ruleId: rule.ruleId,
148
+ outcome: 'cantTell',
149
+ severity: rule.defaultSeverity || 'moderate',
150
+ occurrences
151
+ };
138
152
  }
139
153
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
140
154
  }
@@ -1,10 +1,12 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
4
6
  * @check aria-text
5
7
  * @atomic true
6
8
  * @summary role="text" elements should have no focusable descendants
7
- * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
10
  * @applicability
9
11
  * Elements with an explicit `role="text"`.
10
12
  * @expectation
@@ -15,10 +17,9 @@
15
17
  * focusable content — a focusable descendant inside a "this is just
16
18
  * text" region is unreachable or confusing for keyboard/AT users.
17
19
  * @implementation-notes
18
- * - Not WCAG-normative by a widely-used reference engine's own classification — authored as an
19
- * advisory, cantTell-capped `type: 'manual'` rule, matching the Tier 1b
20
- * precedent (see `landmark-unique`'s header comment for the shared
21
- * rationale).
20
+ * - Not WCAG-normative — authored as an advisory, cantTell-capped
21
+ * `type: 'manual'` rule, matching the Tier 1b precedent (see
22
+ * `landmark-unique`'s header comment for the shared rationale).
22
23
  * - "Focusable descendant" is a presence check (link/button/form
23
24
  * control/`[tabindex]`/`iframe`/`[contenteditable]`), the same
24
25
  * conservative selector `scrollable-region-focusable` uses, not a full
@@ -29,7 +30,8 @@ const id = 'aria-text';
29
30
 
30
31
  const meta = {
31
32
  title: 'role="text" elements should have no focusable descendants',
32
- description: 'Checks that elements with role="text" contain no focusable descendant (link, button, form control, tabindex, iframe, or contenteditable).',
33
+ description:
34
+ 'Checks that elements with role="text" contain no focusable descendant (link, button, form control, tabindex, iframe, or contenteditable).',
33
35
  i18n: {
34
36
  titleKey: 'ariaText_title',
35
37
  descriptionKey: 'ariaText_description'
@@ -46,12 +48,14 @@ const meta = {
46
48
  };
47
49
 
48
50
  function runInPage(ctx) {
49
- const { document, helpers, rule } = ctx;
51
+ const { helpers, rule } = ctx;
50
52
 
51
53
  const FOCUSABLE_DESCENDANT_SELECTOR =
52
54
  'a[href], button, input, select, textarea, [tabindex], iframe, [contenteditable]:not([contenteditable="false"])';
53
55
 
54
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role="text"]') : helpers.queryAll('[role="text"]');
56
+ const nodes = helpers.queryAllSmart
57
+ ? helpers.queryAllSmart('[role="text"]')
58
+ : helpers.queryAll('[role="text"]');
55
59
 
56
60
  const occurrences = [];
57
61
  let applicableCount = 0;
@@ -61,7 +65,7 @@ function runInPage(ctx) {
61
65
 
62
66
  applicableCount += 1;
63
67
 
64
- let focusableDescendant = null;
68
+ let focusableDescendant;
65
69
  try {
66
70
  focusableDescendant = el.querySelector(FOCUSABLE_DESCENDANT_SELECTOR);
67
71
  } catch {
@@ -70,7 +74,7 @@ function runInPage(ctx) {
70
74
  if (!focusableDescendant) continue;
71
75
 
72
76
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
73
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
77
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
74
78
 
75
79
  const baseOccurrence = {
76
80
  selector: stableSelector,
@@ -99,7 +103,12 @@ function runInPage(ctx) {
99
103
  }
100
104
 
101
105
  if (occurrences.length) {
102
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
106
+ return {
107
+ ruleId: rule.ruleId,
108
+ outcome: 'cantTell',
109
+ severity: rule.defaultSeverity || 'minor',
110
+ occurrences
111
+ };
103
112
  }
104
113
 
105
114
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };