@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
  /**
@@ -47,7 +49,8 @@ const id = 'aria-required-parent';
47
49
 
48
50
  const meta = {
49
51
  title: 'Roles requiring a specific context role must be in that context',
50
- description: 'Checks that roles with a documented "required context role" entry (listitem, option, tab, treeitem, row, cell, ...) have an ancestor or aria-owns owner with an acceptable context role.',
52
+ description:
53
+ 'Checks that roles with a documented "required context role" entry (listitem, option, tab, treeitem, row, cell, ...) have an ancestor or aria-owns owner with an acceptable context role.',
51
54
  i18n: {
52
55
  titleKey: 'ariaRequiredParent_title',
53
56
  descriptionKey: 'ariaRequiredParent_description'
@@ -56,7 +59,13 @@ const meta = {
56
59
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
57
60
  wcagSc: ['4.1.2'],
58
61
  normativeMappings: [
59
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
62
+ {
63
+ standard: 'WCAG',
64
+ version: '2.2',
65
+ requirement: '4.1.2',
66
+ title: 'Name, Role, Value',
67
+ conformanceLevel: 'A'
68
+ }
60
69
  ],
61
70
  defaultSeverity: 'moderate',
62
71
  category: 'robust',
@@ -66,7 +75,7 @@ const meta = {
66
75
  };
67
76
 
68
77
  function runInPage(ctx) {
69
- const { document, helpers, rule } = ctx;
78
+ const { helpers, rule } = ctx;
70
79
 
71
80
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
72
81
  if (!ariaHelpers) {
@@ -74,7 +83,8 @@ function runInPage(ctx) {
74
83
  }
75
84
 
76
85
  function isEligibleAcc(el) {
77
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
86
+ const fn =
87
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
78
88
  if (!fn) return true;
79
89
  try {
80
90
  const r = fn(el, ctx);
@@ -86,24 +96,20 @@ function runInPage(ctx) {
86
96
  }
87
97
 
88
98
  // Roles that may host a nested listitem/treeitem group without breaking
89
- // the required-context chain (verified against a widely-used reference
90
- // engine's own getMissingContext, which special-cases exactly these two
91
- // roles via its ownGroupRoles option).
99
+ // the required-context chain — the group role is transparent for exactly
100
+ // these two roles.
92
101
  const GROUP_TRANSPARENT_FOR_ROLES = new Set(['listitem', 'treeitem']);
93
102
 
94
103
  // A real ancestor role — not "no role at all" and not the two roles that
95
104
  // strip an element from the accessibility tree's parent/child chain
96
- // entirely (presentation/none) — stops the search, matching that reference
97
- // engine's getMissingContext. This is stricter than "any ancestor with the right
98
- // role anywhere up the tree": the required-context relationship is about
99
- // the accessibility tree's actual PARENT, so an intervening ancestor with
100
- // its OWN distinct real role (e.g. a plain <li>'s native "listitem" role)
101
- // blocks the search even if a further-up ancestor has the correct role.
102
- // Found via a real page — Le Monde's review-carousel tablist, where each
103
- // <button role="tab"> sits inside a plain <li> (native listitem) inside
104
- // <ul role="tablist">: that reference engine correctly fails this (the tablist is never the
105
- // tab's accessible-tree parent, listitem is), which the old "walk every
106
- // ancestor" version here missed entirely.
105
+ // entirely (presentation/none) — stops the search. This is stricter than
106
+ // "any ancestor with the right role anywhere up the tree": the
107
+ // required-context relationship is about the accessibility tree's actual
108
+ // PARENT, so an intervening ancestor with its OWN distinct real role
109
+ // (e.g. a plain <li>'s native "listitem" role) blocks the search even if
110
+ // a further-up ancestor has the correct role. E.g. a <button role="tab">
111
+ // inside a plain <li> (native listitem) inside <ul role="tablist"> fails:
112
+ // the tablist is never the tab's accessible-tree parent, the listitem is.
107
113
  function getRealContextRole(el) {
108
114
  const role = ariaHelpers.getContainmentRole(el);
109
115
  if (!role || role === 'presentation' || role === 'none') return '';
@@ -113,21 +119,29 @@ function runInPage(ctx) {
113
119
  // Flat-tree ancestor walk (ctx.helpers.composedParent — assignedSlot wins
114
120
  // over parentNode, then shadow host). A slotted light-DOM element's real
115
121
  // rendered ancestor is whatever the shadow tree wraps its <slot> in (e.g.
116
- // a role="list" container), not its own light-DOM parentElement — found
117
- // via Adobe Spectrum Web Components' <sp-sidenav-item role="listitem">,
118
- // distributed via slot="descendant" into its parent's shadow root, which
119
- // wraps that slot in a <div role="list">. composedParent can return a
120
- // non-Element node (a ShadowRoot, nodeType 11) when climbing out of a
121
- // shadow tree that has no further light-DOM parent — skip those and keep
122
- // climbing rather than treating them as a (roleless) context.
123
- const getComposedParent = helpers && typeof helpers.composedParent === 'function'
124
- ? helpers.composedParent
125
- : function (n) { return n && n.parentElement ? n.parentElement : null; };
122
+ // a role="list" container), not its own light-DOM parentElement.
123
+ // composedParent can return a non-Element node (a ShadowRoot, nodeType
124
+ // 11) when climbing out of a shadow tree that has no further light-DOM
125
+ // parent — skip those and keep climbing rather than treating them as a
126
+ // (roleless) context.
127
+ const getComposedParent =
128
+ helpers && typeof helpers.composedParent === 'function'
129
+ ? helpers.composedParent
130
+ : function (n) {
131
+ return n && n.parentElement ? n.parentElement : null;
132
+ };
126
133
 
127
134
  function hasAcceptableAncestorContext(el, acceptableRoles, ownRole) {
128
135
  const allowsGroup = acceptableRoles.has('group');
129
136
  let cur = getComposedParent(el);
130
137
  let guard = 0;
138
+ // Mutable working copy: passing a transparent "group" ancestor also
139
+ // makes the element's OWN role an acceptable context from that point on
140
+ // (a nested treeitem-under-group-under-treeitem chain is a normal,
141
+ // arbitrarily-deep ARIA tree/list, not just one level). Cloned lazily
142
+ // so the caller's Set (built once per element in runInPage) is never
143
+ // mutated.
144
+ let roles = acceptableRoles;
131
145
  while (cur && guard++ < 200) {
132
146
  if (cur.nodeType !== 1) {
133
147
  cur = getComposedParent(cur);
@@ -139,10 +153,12 @@ function runInPage(ctx) {
139
153
  continue;
140
154
  }
141
155
  if (role === 'group' && allowsGroup && GROUP_TRANSPARENT_FOR_ROLES.has(ownRole)) {
156
+ if (roles === acceptableRoles) roles = new Set(acceptableRoles);
157
+ roles.add(ownRole);
142
158
  cur = getComposedParent(cur);
143
159
  continue;
144
160
  }
145
- return acceptableRoles.has(role);
161
+ return roles.has(role);
146
162
  }
147
163
  return false;
148
164
  }
@@ -152,7 +168,9 @@ function runInPage(ctx) {
152
168
  const idTok = elId && String(elId).trim();
153
169
  if (!idTok) return false;
154
170
 
155
- const owners = helpers.queryAllSmart ? helpers.queryAllSmart('[aria-owns]') : helpers.queryAll('[aria-owns]');
171
+ const owners = helpers.queryAllSmart
172
+ ? helpers.queryAllSmart('[aria-owns]')
173
+ : helpers.queryAll('[aria-owns]');
156
174
  for (const owner of owners) {
157
175
  if (!owner || !owner.getAttribute) continue;
158
176
  const ownsAttr = owner.getAttribute('aria-owns') || '';
@@ -165,7 +183,9 @@ function runInPage(ctx) {
165
183
  return false;
166
184
  }
167
185
 
168
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
186
+ const nodes = helpers.queryAllSmart
187
+ ? helpers.queryAllSmart('[role]')
188
+ : helpers.queryAll('[role]');
169
189
 
170
190
  const occurrences = [];
171
191
  let applicableCount = 0;
@@ -191,7 +211,7 @@ function runInPage(ctx) {
191
211
  if (hasContext) continue;
192
212
 
193
213
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
194
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
214
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
195
215
 
196
216
  occurrences.push({
197
217
  selector: stableSelector,
@@ -204,7 +224,11 @@ function runInPage(ctx) {
204
224
  params: { role, requiredRoles: requiredContext.join(', ') }
205
225
  },
206
226
  data: {
207
- details: { reasonCode: 'ARIA_REQUIRED_PARENT_MISSING', role, requiredContextRoles: requiredContext }
227
+ details: {
228
+ reasonCode: 'ARIA_REQUIRED_PARENT_MISSING',
229
+ role,
230
+ requiredContextRoles: requiredContext
231
+ }
208
232
  }
209
233
  });
210
234
  }
@@ -213,9 +237,14 @@ function runInPage(ctx) {
213
237
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
214
238
  }
215
239
  if (occurrences.length) {
216
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
240
+ return {
241
+ ruleId: rule.ruleId,
242
+ outcome: 'fail',
243
+ severity: rule.defaultSeverity || 'moderate',
244
+ occurrences
245
+ };
217
246
  }
218
247
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
219
248
  }
220
249
 
221
- module.exports = { id, meta, runInPage };
250
+ 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
  /**
@@ -15,7 +17,8 @@ const id = 'aria-role-name-present';
15
17
 
16
18
  const meta = {
17
19
  title: 'ARIA widget/container roles have an accessible name',
18
- description: 'Checks that selected ARIA widget/container roles expose a non-empty accessible name.',
20
+ description:
21
+ 'Checks that selected ARIA widget/container roles expose a non-empty accessible name.',
19
22
  i18n: {
20
23
  titleKey: 'ariaRoleNamePresent_title',
21
24
  descriptionKey: 'ariaRoleNamePresent_description'
@@ -24,7 +27,13 @@ const meta = {
24
27
  tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name'],
25
28
  wcagSc: ['4.1.2'],
26
29
  normativeMappings: [
27
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
30
+ {
31
+ standard: 'WCAG',
32
+ version: '2.2',
33
+ requirement: '4.1.2',
34
+ title: 'Name, Role, Value',
35
+ conformanceLevel: 'A'
36
+ }
28
37
  ],
29
38
  defaultSeverity: 'serious',
30
39
  category: 'robust',
@@ -41,17 +50,19 @@ function runInPage(ctx) {
41
50
  const { document, root, helpers, rule } = ctx;
42
51
  const safeRoot = root || document;
43
52
 
44
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
45
- const queryAll = helpers && typeof helpers.queryAll === 'function'
46
- ? helpers.queryAll
47
- : (sel, rt) => {
48
- try {
49
- const scope = rt || safeRoot;
50
- return scope && scope.querySelectorAll ? Array.from(scope.querySelectorAll(sel)) : [];
51
- } catch {
52
- return [];
53
- }
54
- };
53
+ const queryAllSmart =
54
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
55
+ const queryAll =
56
+ helpers && typeof helpers.queryAll === 'function'
57
+ ? helpers.queryAll
58
+ : (sel, rt) => {
59
+ try {
60
+ const scope = rt || safeRoot;
61
+ return scope && scope.querySelectorAll ? Array.from(scope.querySelectorAll(sel)) : [];
62
+ } catch {
63
+ return [];
64
+ }
65
+ };
55
66
 
56
67
  const isAccTreeEligible =
57
68
  helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
@@ -60,9 +71,14 @@ function runInPage(ctx) {
60
71
  helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
61
72
 
62
73
  const getAriaLabelledByInfo =
63
- helpers && typeof helpers.getAriaLabelledByInfo === 'function' ? helpers.getAriaLabelledByInfo : null;
74
+ helpers && typeof helpers.getAriaLabelledByInfo === 'function'
75
+ ? helpers.getAriaLabelledByInfo
76
+ : null;
64
77
 
65
- const normalizeWs = (s) => String(s || '').replace(/\s+/g, ' ').trim();
78
+ const normalizeWs = (s) =>
79
+ String(s || '')
80
+ .replace(/\s+/g, ' ')
81
+ .trim();
66
82
 
67
83
  const getAttr = (el, name) => {
68
84
  try {
@@ -73,7 +89,6 @@ function runInPage(ctx) {
73
89
  }
74
90
  };
75
91
 
76
-
77
92
  // aria-labelledby resolution: delegate to the shared helper (used by
78
93
  // button-name-present et al.), which correctly includes hidden/aria-hidden
79
94
  // referenced nodes per the Accessible Name and Description Computation
@@ -120,7 +135,9 @@ function runInPage(ctx) {
120
135
 
121
136
  const nodes = (() => {
122
137
  try {
123
- return (queryAllSmart ? queryAllSmart(selector, safeRoot) : queryAll(selector, safeRoot)) || [];
138
+ return (
139
+ (queryAllSmart ? queryAllSmart(selector, safeRoot) : queryAll(selector, safeRoot)) || []
140
+ );
124
141
  } catch {
125
142
  return queryAll(selector, safeRoot);
126
143
  }
@@ -158,9 +175,15 @@ function runInPage(ctx) {
158
175
  const ok = !!(ariaLabel || labelled || title);
159
176
  if (ok) continue;
160
177
 
161
- const eligInfo = getEligibilityInfo ? (() => {
162
- try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; }
163
- })() : null;
178
+ const eligInfo = getEligibilityInfo
179
+ ? (() => {
180
+ try {
181
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
182
+ } catch {
183
+ return null;
184
+ }
185
+ })()
186
+ : null;
164
187
 
165
188
  const baseOccurrence = {
166
189
  summary: 'This element has no accessible name.',
@@ -195,7 +218,12 @@ function runInPage(ctx) {
195
218
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
196
219
  }
197
220
 
198
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
221
+ return {
222
+ ruleId: rule.ruleId,
223
+ outcome: 'fail',
224
+ severity: rule.defaultSeverity || 'minor',
225
+ occurrences
226
+ };
199
227
  }
200
228
 
201
229
  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
  /**
@@ -25,7 +27,8 @@ const id = 'aria-roles-valid';
25
27
 
26
28
  const meta = {
27
29
  title: 'role attribute must be a valid, non-abstract ARIA role',
28
- description: 'Checks that an explicit role="" attribute resolves to a real, non-abstract WAI-ARIA role.',
30
+ description:
31
+ 'Checks that an explicit role="" attribute resolves to a real, non-abstract WAI-ARIA role.',
29
32
  i18n: {
30
33
  titleKey: 'ariaRolesValid_title',
31
34
  descriptionKey: 'ariaRolesValid_description'
@@ -34,7 +37,13 @@ const meta = {
34
37
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
35
38
  wcagSc: ['4.1.2'],
36
39
  normativeMappings: [
37
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
40
+ {
41
+ standard: 'WCAG',
42
+ version: '2.2',
43
+ requirement: '4.1.2',
44
+ title: 'Name, Role, Value',
45
+ conformanceLevel: 'A'
46
+ }
38
47
  ],
39
48
  defaultSeverity: 'serious',
40
49
  category: 'robust',
@@ -44,14 +53,16 @@ const meta = {
44
53
  };
45
54
 
46
55
  function runInPage(ctx) {
47
- const { document, helpers, rule } = ctx;
56
+ const { helpers, rule } = ctx;
48
57
 
49
58
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
50
59
  if (!ariaHelpers) {
51
60
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
52
61
  }
53
62
 
54
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
63
+ const nodes = helpers.queryAllSmart
64
+ ? helpers.queryAllSmart('[role]')
65
+ : helpers.queryAll('[role]');
55
66
 
56
67
  const occurrences = [];
57
68
  let applicableCount = 0;
@@ -71,7 +82,7 @@ function runInPage(ctx) {
71
82
 
72
83
  const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
73
84
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
74
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
85
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
75
86
 
76
87
  occurrences.push({
77
88
  selector: stableSelector,
@@ -83,12 +94,8 @@ function runInPage(ctx) {
83
94
  ? 'Use a valid ARIA role token, or remove the role attribute if none applies.'
84
95
  : 'Replace this abstract role with a concrete role appropriate for the widget/structure.',
85
96
  i18n: {
86
- summaryKey: !isKnown
87
- ? 'ariaRolesValid_summary_invalid'
88
- : 'ariaRolesValid_summary_abstract',
89
- hintKey: !isKnown
90
- ? 'ariaRolesValid_hint_invalid'
91
- : 'ariaRolesValid_hint_abstract',
97
+ summaryKey: !isKnown ? 'ariaRolesValid_summary_invalid' : 'ariaRolesValid_summary_abstract',
98
+ hintKey: !isKnown ? 'ariaRolesValid_hint_invalid' : 'ariaRolesValid_hint_abstract',
92
99
  params: { role }
93
100
  },
94
101
  data: {
@@ -101,7 +108,12 @@ function runInPage(ctx) {
101
108
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
102
109
  }
103
110
  if (occurrences.length) {
104
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
111
+ return {
112
+ ruleId: rule.ruleId,
113
+ outcome: 'fail',
114
+ severity: rule.defaultSeverity || 'serious',
115
+ occurrences
116
+ };
105
117
  }
106
118
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
107
119
  }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -13,32 +15,34 @@
13
15
  * @expectation
14
16
  * Each attribute's value conforms to its WAI-ARIA-declared value type:
15
17
  * boolean ("true"/"false"), tristate ("true"/"false"/"mixed"), a token
16
- * from a fixed enumerated set, an integer, a real number, or a non-empty
17
- * ID reference (list) that resolves to an existing element in the
18
- * document.
18
+ * from a fixed enumerated set, an integer, a real number, or an empty
19
+ * value or ID reference (list) that resolves to an existing element in
20
+ * the document.
19
21
  * @implementation-notes
20
22
  * - Not rule-gated on isAccTreeEligible: this remains a static-markup
21
23
  * property, while engine-level hidden-subtree filtering still applies
22
24
  * unless engineOptions.includeHiddenElements is true.
23
- * - ID-reference resolution (added 2026-07-20, see aria-helpers.js's
24
- * idExists) only flags idref-list attributes (aria-labelledby,
25
- * aria-describedby, aria-controls, aria-owns, etc.) when NONE of the
26
- * space-separated ids resolve — a partially-dangling list (some ids
27
- * exist, some don't) is left unflagged. Verified 2026-07-21 directly
28
- * against a widely-used reference engine's own `validateAttrValue` source:
29
- * this is not a conservative guess, it's an exact match for that engine's
30
- * own behavior (`idrefs(vNode, attr).some(node => !!node)` — that engine
31
- * itself only invalidates when every token fails to resolve). Single-idref
32
- * attributes (aria-activedescendant, aria-errormessage) are flagged
33
- * whenever their one id doesn't resolve, also matching that engine's `idref`
34
- * case exactly.
25
+ * - ID-reference resolution (see aria-helpers.js's idExists) only flags
26
+ * idref-list attributes (aria-labelledby, aria-describedby,
27
+ * aria-controls, aria-owns, etc.) when NONE of the space-separated ids
28
+ * resolve — a partially-dangling list (some ids exist, some don't) is
29
+ * left unflagged (only invalidate when every token fails to resolve).
30
+ * Single-idref attributes (aria-activedescendant, aria-errormessage) are
31
+ * flagged whenever their one id doesn't resolve.
32
+ * - An explicitly-EMPTY idref/idref-list value (e.g.
33
+ * `aria-describedby=""`) is valid, not a violation — `allowEmpty` holds
34
+ * for every idref/idref-list ARIA attribute. A common, deliberate pattern
35
+ * in templated markup (e.g. React conditionally rendering
36
+ * `aria-describedby={hasError ? errorId : ''}`); flagging it would be a
37
+ * false positive.
35
38
  */
36
39
 
37
40
  const id = 'aria-valid-attr-value';
38
41
 
39
42
  const meta = {
40
43
  title: 'aria-* attribute values must match their declared type',
41
- description: 'Checks that every recognized aria-* attribute has a value conforming to its WAI-ARIA-declared value type (boolean, tristate, token, integer, number, or ID reference).',
44
+ description:
45
+ 'Checks that every recognized aria-* attribute has a value conforming to its WAI-ARIA-declared value type (boolean, tristate, token, integer, number, or ID reference).',
42
46
  i18n: {
43
47
  titleKey: 'ariaValidAttrValue_title',
44
48
  descriptionKey: 'ariaValidAttrValue_description'
@@ -47,7 +51,13 @@ const meta = {
47
51
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
48
52
  wcagSc: ['4.1.2'],
49
53
  normativeMappings: [
50
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
54
+ {
55
+ standard: 'WCAG',
56
+ version: '2.2',
57
+ requirement: '4.1.2',
58
+ title: 'Name, Role, Value',
59
+ conformanceLevel: 'A'
60
+ }
51
61
  ],
52
62
  defaultSeverity: 'serious',
53
63
  category: 'robust',
@@ -57,7 +67,7 @@ const meta = {
57
67
  };
58
68
 
59
69
  function runInPage(ctx) {
60
- const { document, helpers, rule } = ctx;
70
+ const { helpers, rule } = ctx;
61
71
 
62
72
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
63
73
  if (!ariaHelpers) {
@@ -85,14 +95,18 @@ function runInPage(ctx) {
85
95
  const result = ariaHelpers.validateAttrValue(name, rawValue);
86
96
  if (!result.valid) {
87
97
  if (!invalid) invalid = [];
88
- invalid.push({ name, value: rawValue == null ? '' : String(rawValue), reason: result.reason });
98
+ invalid.push({
99
+ name,
100
+ value: rawValue == null ? '' : String(rawValue),
101
+ reason: result.reason
102
+ });
89
103
  }
90
104
  }
91
105
 
92
106
  if (!invalid || !invalid.length) continue;
93
107
 
94
108
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
95
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
109
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
96
110
 
97
111
  for (const item of invalid) {
98
112
  occurrences.push({
@@ -106,7 +120,12 @@ function runInPage(ctx) {
106
120
  params: { attr: item.name, value: item.value }
107
121
  },
108
122
  data: {
109
- details: { reasonCode: 'ARIA_ATTR_VALUE_INVALID', attr: item.name, value: item.value, valueReason: item.reason }
123
+ details: {
124
+ reasonCode: 'ARIA_ATTR_VALUE_INVALID',
125
+ attr: item.name,
126
+ value: item.value,
127
+ valueReason: item.reason
128
+ }
110
129
  }
111
130
  });
112
131
  }
@@ -116,7 +135,12 @@ function runInPage(ctx) {
116
135
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
117
136
  }
118
137
  if (occurrences.length) {
119
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
138
+ return {
139
+ ruleId: rule.ruleId,
140
+ outcome: 'fail',
141
+ severity: rule.defaultSeverity || 'serious',
142
+ occurrences
143
+ };
120
144
  }
121
145
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
122
146
  }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -27,7 +29,8 @@ const id = 'aria-valid-attr';
27
29
 
28
30
  const meta = {
29
31
  title: 'aria-* attributes must be real, defined ARIA attributes',
30
- description: 'Checks that every aria-* attribute name present in the DOM is a real attribute defined by the WAI-ARIA specification.',
32
+ description:
33
+ 'Checks that every aria-* attribute name present in the DOM is a real attribute defined by the WAI-ARIA specification.',
31
34
  i18n: {
32
35
  titleKey: 'ariaValidAttr_title',
33
36
  descriptionKey: 'ariaValidAttr_description'
@@ -36,7 +39,13 @@ const meta = {
36
39
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
37
40
  wcagSc: ['4.1.2'],
38
41
  normativeMappings: [
39
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
42
+ {
43
+ standard: 'WCAG',
44
+ version: '2.2',
45
+ requirement: '4.1.2',
46
+ title: 'Name, Role, Value',
47
+ conformanceLevel: 'A'
48
+ }
40
49
  ],
41
50
  defaultSeverity: 'serious',
42
51
  category: 'robust',
@@ -46,7 +55,7 @@ const meta = {
46
55
  };
47
56
 
48
57
  function runInPage(ctx) {
49
- const { document, helpers, rule } = ctx;
58
+ const { helpers, rule } = ctx;
50
59
 
51
60
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
52
61
  if (!ariaHelpers) {
@@ -78,7 +87,7 @@ function runInPage(ctx) {
78
87
  if (!invalidNames || !invalidNames.length) continue;
79
88
 
80
89
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
81
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
90
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
82
91
 
83
92
  for (const name of invalidNames) {
84
93
  occurrences.push({
@@ -102,7 +111,12 @@ function runInPage(ctx) {
102
111
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
103
112
  }
104
113
  if (occurrences.length) {
105
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
114
+ return {
115
+ ruleId: rule.ruleId,
116
+ outcome: 'fail',
117
+ severity: rule.defaultSeverity || 'serious',
118
+ occurrences
119
+ };
106
120
  }
107
121
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
108
122
  }