@surea11y/core 1.1.2 → 1.3.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 (160) hide show
  1. package/CHANGELOG.md +48 -4
  2. package/LICENSE +373 -21
  3. package/README.md +70 -1
  4. package/bin/core.js +240 -11
  5. package/docs/API_STABILITY.md +61 -0
  6. package/docs/BASELINE.md +66 -0
  7. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  8. package/docs/CI_INTEGRATIONS.md +103 -0
  9. package/docs/CLI.md +80 -1
  10. package/docs/ENGINE_OPTIONS.md +4 -0
  11. package/docs/INTEGRATION.md +19 -1
  12. package/docs/OUTPUT_SCHEMA.md +2 -2
  13. package/docs/REPORT.md +33 -0
  14. package/docs/RULE_AUTHORING.md +31 -0
  15. package/docs/RULE_CATALOG.md +1 -1
  16. package/docs/SARIF.md +59 -0
  17. package/package.json +15 -4
  18. package/src/baseline.js +0 -0
  19. package/src/catalogs/composites.wcag.js +414 -450
  20. package/src/checks/automatic/area-alt-present.js +59 -25
  21. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  22. package/src/checks/automatic/aria-allowed-role.js +21 -7
  23. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  24. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  25. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  26. package/src/checks/automatic/aria-hidden-body.js +50 -19
  27. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  28. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  29. package/src/checks/automatic/aria-prohibited-children.js +125 -29
  30. package/src/checks/automatic/aria-required-attr.js +23 -8
  31. package/src/checks/automatic/aria-required-children.js +37 -14
  32. package/src/checks/automatic/aria-required-parent.js +48 -14
  33. package/src/checks/automatic/aria-role-name-present.js +47 -21
  34. package/src/checks/automatic/aria-roles-valid.js +22 -12
  35. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  36. package/src/checks/automatic/aria-valid-attr.js +17 -5
  37. package/src/checks/automatic/autocomplete-valid.js +74 -16
  38. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  39. package/src/checks/automatic/binary-control-name-present.js +60 -50
  40. package/src/checks/automatic/button-name-present.js +48 -18
  41. package/src/checks/automatic/bypass-blocks-present.js +50 -25
  42. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  43. package/src/checks/automatic/combobox-name-present.js +38 -45
  44. package/src/checks/automatic/contrast-computable.js +361 -341
  45. package/src/checks/automatic/contrast-enhanced.js +487 -466
  46. package/src/checks/automatic/contrast-minimum.js +486 -465
  47. package/src/checks/automatic/css-orientation-lock.js +39 -9
  48. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  49. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  50. package/src/checks/automatic/dialog-name-present.js +37 -75
  51. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  52. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  53. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  54. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  55. package/src/checks/automatic/form-control-single-label.js +47 -10
  56. package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
  57. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  58. package/src/checks/automatic/iframe-name-present.js +31 -9
  59. package/src/checks/automatic/iframe-title-unique.js +29 -8
  60. package/src/checks/automatic/img-alt-present.js +47 -43
  61. package/src/checks/automatic/input-image-alt-present.js +143 -112
  62. package/src/checks/automatic/label-in-name.js +50 -22
  63. package/src/checks/automatic/language-page-present.js +117 -109
  64. package/src/checks/automatic/link-in-text-block.js +59 -19
  65. package/src/checks/automatic/link-name-present.js +45 -14
  66. package/src/checks/automatic/list-children-valid.js +26 -9
  67. package/src/checks/automatic/listbox-name-present.js +39 -19
  68. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  69. package/src/checks/automatic/menuitem-name-present.js +39 -61
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
  72. package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
  73. package/src/checks/automatic/meter-name-present.js +36 -33
  74. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  75. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  76. package/src/checks/automatic/option-name-present.js +38 -21
  77. package/src/checks/automatic/page-title-present.js +26 -7
  78. package/src/checks/automatic/progressbar-name-present.js +41 -34
  79. package/src/checks/automatic/role-img-alt-present.js +209 -157
  80. package/src/checks/automatic/searchbox-name-present.js +39 -19
  81. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  82. package/src/checks/automatic/slider-name-present.js +40 -47
  83. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  84. package/src/checks/automatic/summary-name-present.js +37 -17
  85. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  86. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  87. package/src/checks/automatic/tab-name-present.js +37 -60
  88. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  89. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  90. package/src/checks/automatic/target-size-minimum.js +118 -48
  91. package/src/checks/automatic/td-has-header.js +29 -11
  92. package/src/checks/automatic/textbox-name-present.js +39 -19
  93. package/src/checks/automatic/tooltip-name-present.js +37 -18
  94. package/src/checks/automatic/treeitem-name-present.js +38 -21
  95. package/src/checks/automatic/valid-lang.js +20 -6
  96. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  97. package/src/checks/manual/accesskeys-manual.js +14 -5
  98. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  99. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  100. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  101. package/src/checks/manual/aria-text-manual.js +14 -6
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  103. package/src/checks/manual/css-hidden-focus.js +196 -165
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  105. package/src/checks/manual/empty-heading-manual.js +24 -12
  106. package/src/checks/manual/empty-table-header-manual.js +21 -14
  107. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  108. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  109. package/src/checks/manual/heading-order-manual.js +22 -12
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  111. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  112. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  113. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  115. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  116. package/src/checks/manual/label-title-only-manual.js +14 -5
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  123. package/src/checks/manual/landmark-one-main-manual.js +35 -21
  124. package/src/checks/manual/landmark-unique-manual.js +73 -45
  125. package/src/checks/manual/link-name-quality-manual.js +43 -12
  126. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  127. package/src/checks/manual/meta-viewport-large-manual.js +25 -6
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  129. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  131. package/src/checks/manual/p-as-heading-manual.js +22 -7
  132. package/src/checks/manual/page-has-heading-one-manual.js +40 -23
  133. package/src/checks/manual/page-title-patterns-manual.js +87 -51
  134. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  135. package/src/checks/manual/region-manual.js +275 -62
  136. package/src/checks/manual/scope-attr-valid-manual.js +11 -9
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  138. package/src/checks/manual/skip-link-manual.js +35 -12
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  140. package/src/checks/manual/tabindex-manual.js +11 -9
  141. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  142. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  143. package/src/checks/manual/video-caption-manual.js +15 -4
  144. package/src/checks/manual-review.js +56 -12
  145. package/src/core/aria-helpers.js +1128 -823
  146. package/src/core/contrast-helpers.js +1217 -1062
  147. package/src/core/dom-helpers.js +4176 -3886
  148. package/src/core/dom-runner.js +720 -596
  149. package/src/core/frame-messaging.js +189 -138
  150. package/src/core/frame-scan.js +94 -82
  151. package/src/core/rollup-composites.js +94 -102
  152. package/src/core/rule-meta.js +71 -35
  153. package/src/core.js +37939 -29121
  154. package/src/i18n/en.js +1194 -887
  155. package/src/i18n/fr.js +1136 -793
  156. package/src/policy/contracts.js +13 -13
  157. package/src/policy/resolvePolicy.js +48 -44
  158. package/src/report.js +502 -0
  159. package/src/sarif.js +175 -0
  160. package/surea11y.browser.js +36042 -0
@@ -11,20 +11,26 @@
11
11
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
12
  */
13
13
 
14
- const id = "area-alt-quality";
14
+ const id = 'area-alt-quality';
15
15
 
16
16
  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.",
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
19
  i18n: {
20
- titleKey: "area_altQuality_title",
21
- descriptionKey: "area_altQuality_description"
20
+ titleKey: 'area_altQuality_title',
21
+ descriptionKey: 'area_altQuality_description'
22
22
  },
23
23
  helpUrl: null,
24
- tags: ["wcag2a", "wcag111", "nontext", "images", "imagemap", "manual", "atomic"],
24
+ tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'imagemap', 'manual', 'atomic'],
25
25
  wcagSc: ['1.1.1'],
26
26
  normativeMappings: [
27
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
27
+ {
28
+ standard: 'WCAG',
29
+ version: '2.2',
30
+ requirement: '1.1.1',
31
+ title: 'Non-text Content',
32
+ conformanceLevel: 'A'
33
+ }
28
34
  ],
29
35
  defaultSeverity: 'minor',
30
36
  category: 'perceivable',
@@ -38,167 +44,202 @@ const meta = {
38
44
  };
39
45
 
40
46
  function runInPage(ctx) {
47
+ const { document, root, helpers, rule } = ctx;
48
+ const safeRoot = root || document;
49
+
50
+ const queryAllSmart =
51
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
52
+ const queryAll =
53
+ helpers && typeof helpers.queryAll === 'function'
54
+ ? helpers.queryAll
55
+ : (sel) => {
56
+ try {
57
+ return safeRoot && safeRoot.querySelectorAll
58
+ ? Array.from(safeRoot.querySelectorAll(sel))
59
+ : [];
60
+ } catch {
61
+ return [];
62
+ }
63
+ };
41
64
 
42
- const { document, root, helpers, rule } = ctx;
43
- const safeRoot = root || document;
65
+ const getEligibilityInfo =
66
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
44
67
 
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
- };
68
+ const isAccTreeEligible =
69
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
52
70
 
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;
71
+ const __accEligCache = new WeakMap();
72
+ function accEligibleCached(node) {
73
+ if (!isAccTreeEligible) return { eligible: true, reasons: [] };
74
+ if (!node || typeof node !== 'object') return { eligible: true, reasons: [] };
75
+ const c = __accEligCache.get(node);
76
+ if (c) return c;
77
+ let r;
78
+ try {
79
+ r = isAccTreeEligible(node, ctx);
80
+ } catch {
81
+ r = { eligible: true, reasons: [] };
72
82
  }
83
+ r = r && typeof r === 'object' ? r : { eligible: !!r, reasons: [] };
84
+ __accEligCache.set(node, r);
85
+ return r;
86
+ }
73
87
 
74
- // --- image-map semantics (rule-local; match automatic <area> applicability) ---
75
- function normUsemap(val) {
88
+ // --- image-map semantics (rule-local; match automatic <area> applicability) ---
89
+ function normUsemap(val) {
76
90
  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) {
91
+ const t = String(val || '').trim();
92
+ if (!t) return '';
93
+ return t[0] === '#' ? t.slice(1).trim().toLowerCase() : t.toLowerCase();
94
+ } catch {
95
+ return '';
96
+ }
97
+ }
98
+ function getMapName(mapEl) {
83
99
  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
- }
100
+ if (!mapEl || !mapEl.getAttribute) return '';
101
+ const n = String(mapEl.getAttribute('name') || mapEl.getAttribute('id') || '').trim();
102
+ return n ? n.toLowerCase() : '';
103
+ } catch {
104
+ return '';
105
+ }
106
+ }
107
+
108
+ const getFocusableInfo =
109
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
110
+
111
+ function isRolePresentationExcluded(el) {
112
+ const role = (() => {
113
+ try {
114
+ return String(el.getAttribute('role') || '')
115
+ .trim()
116
+ .toLowerCase();
117
+ } catch {
118
+ return '';
119
+ }
120
+ })();
121
+ if (role !== 'presentation' && role !== 'none') return false;
89
122
 
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()));
123
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
124
+ let focusable;
125
+ if (getFocusableInfo) {
126
+ const fi = (() => {
127
+ try {
128
+ return getFocusableInfo(el, ctx);
129
+ } catch {
130
+ return null;
109
131
  }
110
- return !focusable;
132
+ })();
133
+ focusable = !!(fi && fi.focusable);
134
+ } else {
135
+ const tabindex = el.getAttribute('tabindex');
136
+ focusable =
137
+ tabindex != null &&
138
+ String(tabindex).trim() !== '' &&
139
+ !Number.isNaN(Number(String(tabindex).trim()));
111
140
  }
141
+ return !focusable;
142
+ }
112
143
 
144
+ const els = (() => {
145
+ try {
146
+ return Array.from((queryAllSmart ? queryAllSmart('area') : queryAll('area')) || []);
147
+ } catch {
148
+ return queryAll('area');
149
+ }
150
+ })();
113
151
 
114
- const els = (() => {
115
- try { return Array.from((queryAllSmart ? queryAllSmart('area') : queryAll('area')) || []); }
116
- catch { return queryAll('area'); }
117
- })();
152
+ if (!els.length) {
153
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
154
+ }
118
155
 
119
- if (!els.length) {
120
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
156
+ const occurrences = [];
157
+ let applicableCount = 0;
158
+
159
+ const __usemapIndex = new Map(); // mapName -> img (first in document order)
160
+ try {
161
+ const imgs = Array.from(document.querySelectorAll('img[usemap]'));
162
+ for (const img of imgs) {
163
+ const u = normUsemap(img.getAttribute('usemap'));
164
+ if (!u) continue;
165
+ if (!__usemapIndex.has(u)) __usemapIndex.set(u, img);
121
166
  }
167
+ } catch {}
122
168
 
123
- const occurrences = [];
124
- let applicableCount = 0;
169
+ for (const el of els) {
170
+ if (!el || !el.getAttribute) continue;
125
171
 
126
- const __usemapIndex = new Map(); // mapName -> img (first in document order)
172
+ // Must belong to a *used* image map (referenced by an <img usemap>). If unused, not applicable.
173
+ let img;
127
174
  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 {}
175
+ const map = el.closest && el.closest('map');
176
+ const mapName = map ? getMapName(map) : '';
177
+ img = mapName ? __usemapIndex.get(mapName) || null : null;
178
+ } catch {
179
+ img = null;
180
+ }
181
+ if (!img) continue;
135
182
 
136
- for (const el of els) {
137
- if (!el || !el.getAttribute) continue;
183
+ // The referencing <img> must be eligible in the accessibility tree.
184
+ if (isAccTreeEligible) {
185
+ const imgElig = accEligibleCached(img);
186
+ if (imgElig && imgElig.eligible === false) continue;
187
+ }
138
188
 
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;
189
+ if (isAccTreeEligible) {
190
+ const elig = accEligibleCached(el);
191
+ if (elig && elig.eligible === false) continue;
192
+ }
149
193
 
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
- }
194
+ if (isRolePresentationExcluded(el)) continue;
155
195
 
156
- if (isAccTreeEligible) {
157
- const elig = accEligibleCached(el);
158
- if (elig && elig.eligible === false) continue;
159
- }
196
+ // Rule-specific applicability (only elements that already have a text alternative mechanism)
197
+ let alt;
198
+ try {
199
+ alt = el.getAttribute('alt');
200
+ } catch {
201
+ alt = null;
202
+ }
203
+ if (alt === null) continue;
204
+ if (String(alt).trim() === '') continue; // only non-empty alt is applicable here
160
205
 
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
- };
206
+ applicableCount += 1;
189
207
 
190
- if (helpers && typeof helpers.reportOccurrence === 'function') {
191
- occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
192
- } else {
193
- occurrences.push({ selector: '', html: '', ...baseOccurrence });
194
- }
208
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
209
+
210
+ let altVal;
211
+ try {
212
+ altVal = String(el.getAttribute('alt') || '');
213
+ } catch {
214
+ altVal = '';
195
215
  }
196
216
 
197
- if (applicableCount === 0) {
198
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
217
+ const baseOccurrence = {
218
+ summary: 'Review alt text on <area> for accuracy and appropriateness.',
219
+ hint: 'Ensure the alt text identifies the destination/action of the image map area in context.',
220
+ i18n: {
221
+ summaryKey: 'area_altQuality_summary_cantTell',
222
+ hintKey: 'area_altQuality_hint_cantTell',
223
+ params: { element: (el.tagName || '').toLowerCase() }
224
+ },
225
+ data: {
226
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
227
+ details: { alt: altVal.trim() } // optional but useful for manual review
228
+ }
229
+ };
230
+
231
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
232
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
233
+ } else {
234
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
199
235
  }
236
+ }
237
+
238
+ if (applicableCount === 0) {
239
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
240
+ }
200
241
 
201
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
242
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
202
243
  }
203
244
 
204
245
  module.exports = { id, meta, runInPage };
@@ -52,7 +52,8 @@ const id = 'aria-checked-state-mismatch';
52
52
 
53
53
  const meta = {
54
54
  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.',
55
+ description:
56
+ '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
57
  i18n: {
57
58
  titleKey: 'ariaCheckedStateMismatch_title',
58
59
  descriptionKey: 'ariaCheckedStateMismatch_description'
@@ -61,7 +62,13 @@ const meta = {
61
62
  tags: ['wcag2a', 'wcag412', 'aria', 'forms', 'atomic', 'manual'],
62
63
  wcagSc: ['4.1.2'],
63
64
  normativeMappings: [
64
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
65
+ {
66
+ standard: 'WCAG',
67
+ version: '2.2',
68
+ requirement: '4.1.2',
69
+ title: 'Name, Role, Value',
70
+ conformanceLevel: 'A'
71
+ }
65
72
  ],
66
73
  defaultSeverity: 'moderate',
67
74
  category: 'robust',
@@ -71,12 +78,16 @@ const meta = {
71
78
  };
72
79
 
73
80
  function runInPage(ctx) {
74
- const { document, helpers, rule } = ctx;
81
+ const { helpers, rule } = ctx;
75
82
 
76
- function trim(v) { return (v == null ? '' : String(v)).trim(); }
83
+ function trim(v) {
84
+ return (v == null ? '' : String(v)).trim();
85
+ }
77
86
 
78
87
  const nodes = helpers.queryAllSmart
79
- ? helpers.queryAllSmart('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]')
88
+ ? helpers.queryAllSmart(
89
+ 'input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]'
90
+ )
80
91
  : helpers.queryAll('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]');
81
92
 
82
93
  const occurrences = [];
@@ -97,9 +108,10 @@ function runInPage(ctx) {
97
108
 
98
109
  let normalizedAriaChecked;
99
110
  if (isCheckbox) {
100
- normalizedAriaChecked = (rawAriaChecked === 'mixed' || rawAriaChecked === 'true') ? rawAriaChecked : 'false';
111
+ normalizedAriaChecked =
112
+ rawAriaChecked === 'mixed' || rawAriaChecked === 'true' ? rawAriaChecked : 'false';
101
113
  } else {
102
- normalizedAriaChecked = (rawAriaChecked === 'true') ? 'true' : 'false';
114
+ normalizedAriaChecked = rawAriaChecked === 'true' ? 'true' : 'false';
103
115
  }
104
116
 
105
117
  let actualState;
@@ -112,12 +124,13 @@ function runInPage(ctx) {
112
124
  if (normalizedAriaChecked === actualState) continue;
113
125
 
114
126
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
115
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
127
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
116
128
 
117
129
  occurrences.push({
118
130
  selector: stableSelector,
119
131
  html,
120
- summary: 'This element’s aria-checked value does not match its actual checked/indeterminate state.',
132
+ summary:
133
+ 'This element’s aria-checked value does not match its actual checked/indeterminate state.',
121
134
  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
135
  i18n: {
123
136
  summaryKey: 'ariaCheckedStateMismatch_summary_cantTell',
@@ -125,7 +138,12 @@ function runInPage(ctx) {
125
138
  params: { ariaChecked: normalizedAriaChecked, actualState, type }
126
139
  },
127
140
  data: {
128
- details: { reasonCode: 'ARIA_CHECKED_STATE_MISMATCH', ariaChecked: normalizedAriaChecked, actualState, type }
141
+ details: {
142
+ reasonCode: 'ARIA_CHECKED_STATE_MISMATCH',
143
+ ariaChecked: normalizedAriaChecked,
144
+ actualState,
145
+ type
146
+ }
129
147
  }
130
148
  });
131
149
  }
@@ -134,7 +152,12 @@ function runInPage(ctx) {
134
152
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
135
153
  }
136
154
  if (occurrences.length) {
137
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'moderate', occurrences };
155
+ return {
156
+ ruleId: rule.ruleId,
157
+ outcome: 'cantTell',
158
+ severity: rule.defaultSeverity || 'moderate',
159
+ occurrences
160
+ };
138
161
  }
139
162
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
140
163
  }
@@ -29,7 +29,8 @@ const id = 'aria-text';
29
29
 
30
30
  const meta = {
31
31
  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).',
32
+ description:
33
+ 'Checks that elements with role="text" contain no focusable descendant (link, button, form control, tabindex, iframe, or contenteditable).',
33
34
  i18n: {
34
35
  titleKey: 'ariaText_title',
35
36
  descriptionKey: 'ariaText_description'
@@ -46,12 +47,14 @@ const meta = {
46
47
  };
47
48
 
48
49
  function runInPage(ctx) {
49
- const { document, helpers, rule } = ctx;
50
+ const { helpers, rule } = ctx;
50
51
 
51
52
  const FOCUSABLE_DESCENDANT_SELECTOR =
52
53
  'a[href], button, input, select, textarea, [tabindex], iframe, [contenteditable]:not([contenteditable="false"])';
53
54
 
54
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role="text"]') : helpers.queryAll('[role="text"]');
55
+ const nodes = helpers.queryAllSmart
56
+ ? helpers.queryAllSmart('[role="text"]')
57
+ : helpers.queryAll('[role="text"]');
55
58
 
56
59
  const occurrences = [];
57
60
  let applicableCount = 0;
@@ -61,7 +64,7 @@ function runInPage(ctx) {
61
64
 
62
65
  applicableCount += 1;
63
66
 
64
- let focusableDescendant = null;
67
+ let focusableDescendant;
65
68
  try {
66
69
  focusableDescendant = el.querySelector(FOCUSABLE_DESCENDANT_SELECTOR);
67
70
  } catch {
@@ -70,7 +73,7 @@ function runInPage(ctx) {
70
73
  if (!focusableDescendant) continue;
71
74
 
72
75
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
73
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
76
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
74
77
 
75
78
  const baseOccurrence = {
76
79
  selector: stableSelector,
@@ -99,7 +102,12 @@ function runInPage(ctx) {
99
102
  }
100
103
 
101
104
  if (occurrences.length) {
102
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
105
+ return {
106
+ ruleId: rule.ruleId,
107
+ outcome: 'cantTell',
108
+ severity: rule.defaultSeverity || 'minor',
109
+ occurrences
110
+ };
103
111
  }
104
112
 
105
113
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };