@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
@@ -50,7 +50,8 @@ const id = 'aria-required-attr';
50
50
 
51
51
  const meta = {
52
52
  title: 'Roles with a required ARIA state/property must carry it',
53
- description: 'Checks that elements with an explicit role carry every unambiguous, context-independent required aria-* state/property for that role (e.g. role="checkbox" must have aria-checked).',
53
+ description:
54
+ 'Checks that elements with an explicit role carry every unambiguous, context-independent required aria-* state/property for that role (e.g. role="checkbox" must have aria-checked).',
54
55
  i18n: {
55
56
  titleKey: 'ariaRequiredAttr_title',
56
57
  descriptionKey: 'ariaRequiredAttr_description'
@@ -59,7 +60,13 @@ const meta = {
59
60
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
60
61
  wcagSc: ['4.1.2'],
61
62
  normativeMappings: [
62
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
63
+ {
64
+ standard: 'WCAG',
65
+ version: '2.2',
66
+ requirement: '4.1.2',
67
+ title: 'Name, Role, Value',
68
+ conformanceLevel: 'A'
69
+ }
63
70
  ],
64
71
  defaultSeverity: 'serious',
65
72
  category: 'robust',
@@ -69,7 +76,7 @@ const meta = {
69
76
  };
70
77
 
71
78
  function runInPage(ctx) {
72
- const { document, helpers, rule } = ctx;
79
+ const { helpers, rule } = ctx;
73
80
 
74
81
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
75
82
  if (!ariaHelpers) {
@@ -77,7 +84,8 @@ function runInPage(ctx) {
77
84
  }
78
85
 
79
86
  function isEligibleAcc(el) {
80
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
87
+ const fn =
88
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
81
89
  if (!fn) return true;
82
90
  try {
83
91
  const r = fn(el, ctx);
@@ -93,7 +101,9 @@ function runInPage(ctx) {
93
101
  return v != null && String(v).trim().toLowerCase() === 'true';
94
102
  }
95
103
 
96
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
104
+ const nodes = helpers.queryAllSmart
105
+ ? helpers.queryAllSmart('[role]')
106
+ : helpers.queryAll('[role]');
97
107
 
98
108
  const occurrences = [];
99
109
  let applicableCount = 0;
@@ -121,7 +131,7 @@ function runInPage(ctx) {
121
131
  if (!missing.length) continue;
122
132
 
123
133
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
124
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
134
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
125
135
 
126
136
  for (const attr of missing) {
127
137
  occurrences.push({
@@ -145,9 +155,14 @@ function runInPage(ctx) {
145
155
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
146
156
  }
147
157
  if (occurrences.length) {
148
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
158
+ return {
159
+ ruleId: rule.ruleId,
160
+ outcome: 'fail',
161
+ severity: rule.defaultSeverity || 'serious',
162
+ occurrences
163
+ };
149
164
  }
150
165
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
151
166
  }
152
167
 
153
- module.exports = { id, meta, runInPage };
168
+ module.exports = { id, meta, runInPage };
@@ -61,7 +61,8 @@ const id = 'aria-required-children';
61
61
 
62
62
  const meta = {
63
63
  title: 'Container roles must own at least one required child role',
64
- description: 'Checks that container roles with a documented "required owned elements" entry (list, listbox, menu, radiogroup, table, grid, tablist, tree, row, ...) contain at least one descendant or aria-owns-referenced element with an acceptable owned role.',
64
+ description:
65
+ 'Checks that container roles with a documented "required owned elements" entry (list, listbox, menu, radiogroup, table, grid, tablist, tree, row, ...) contain at least one descendant or aria-owns-referenced element with an acceptable owned role.',
65
66
  i18n: {
66
67
  titleKey: 'ariaRequiredChildren_title',
67
68
  descriptionKey: 'ariaRequiredChildren_description'
@@ -70,7 +71,13 @@ const meta = {
70
71
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
71
72
  wcagSc: ['4.1.2'],
72
73
  normativeMappings: [
73
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
74
+ {
75
+ standard: 'WCAG',
76
+ version: '2.2',
77
+ requirement: '4.1.2',
78
+ title: 'Name, Role, Value',
79
+ conformanceLevel: 'A'
80
+ }
74
81
  ],
75
82
  defaultSeverity: 'moderate',
76
83
  category: 'robust',
@@ -80,7 +87,7 @@ const meta = {
80
87
  };
81
88
 
82
89
  function runInPage(ctx) {
83
- const { document, helpers, rule } = ctx;
90
+ const { helpers, rule } = ctx;
84
91
 
85
92
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
86
93
  if (!ariaHelpers) {
@@ -88,7 +95,8 @@ function runInPage(ctx) {
88
95
  }
89
96
 
90
97
  function isEligibleAcc(el) {
91
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
98
+ const fn =
99
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
92
100
  if (!fn) return true;
93
101
  try {
94
102
  const r = fn(el, ctx);
@@ -109,9 +117,12 @@ function runInPage(ctx) {
109
117
  // (kept in sync with aria-helpers.js NATIVE_CONTAINMENT_ROLE_BY_ELEMENT).
110
118
  // Declared inside runInPage — see scripts/build-core.js header
111
119
  // ("runInPage MUST be self-contained").
112
- const CANDIDATE_SELECTOR = '[role], li, option, tr, td, th, thead, tbody, tfoot, ul, ol, table, select, input[type="radio"]';
120
+ const CANDIDATE_SELECTOR =
121
+ '[role], li, option, tr, td, th, thead, tbody, tfoot, ul, ol, table, select, input[type="radio"]';
113
122
 
114
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
123
+ const nodes = helpers.queryAllSmart
124
+ ? helpers.queryAllSmart('[role]')
125
+ : helpers.queryAll('[role]');
115
126
 
116
127
  const occurrences = [];
117
128
  let applicableCount = 0;
@@ -139,8 +150,11 @@ function runInPage(ctx) {
139
150
  if (out.length >= limit) return;
140
151
  if (seen.has(child)) continue;
141
152
 
142
- if ((child.tagName || '').toLowerCase() === 'slot' && typeof child.assignedElements === 'function') {
143
- let assigned = [];
153
+ if (
154
+ (child.tagName || '').toLowerCase() === 'slot' &&
155
+ typeof child.assignedElements === 'function'
156
+ ) {
157
+ let assigned;
144
158
  try {
145
159
  assigned = child.assignedElements({ flatten: true }) || [];
146
160
  } catch {
@@ -184,7 +198,7 @@ function runInPage(ctx) {
184
198
  // selector, exactly as before this fix — covers the overwhelming
185
199
  // majority of containers (no shadow DOM involved at all) with zero
186
200
  // added cost.
187
- let descendants = [];
201
+ let descendants;
188
202
  try {
189
203
  descendants = el.querySelectorAll(CANDIDATE_SELECTOR);
190
204
  } catch {
@@ -202,7 +216,7 @@ function runInPage(ctx) {
202
216
  // <slot> somewhere in the subtree to expand — bounds the extra cost to
203
217
  // exactly the containers that could possibly need it.
204
218
  if (!found) {
205
- let hasSlot = false;
219
+ let hasSlot;
206
220
  try {
207
221
  hasSlot = !!el.querySelector('slot');
208
222
  } catch {
@@ -243,7 +257,7 @@ function runInPage(ctx) {
243
257
  if (found) continue;
244
258
 
245
259
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
246
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
260
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
247
261
 
248
262
  occurrences.push({
249
263
  selector: stableSelector,
@@ -256,7 +270,11 @@ function runInPage(ctx) {
256
270
  params: { role, requiredRoles: requiredOwned.join(', ') }
257
271
  },
258
272
  data: {
259
- details: { reasonCode: 'ARIA_REQUIRED_CHILD_MISSING', role, requiredOwnedRoles: requiredOwned }
273
+ details: {
274
+ reasonCode: 'ARIA_REQUIRED_CHILD_MISSING',
275
+ role,
276
+ requiredOwnedRoles: requiredOwned
277
+ }
260
278
  }
261
279
  });
262
280
  }
@@ -265,9 +283,14 @@ function runInPage(ctx) {
265
283
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
266
284
  }
267
285
  if (occurrences.length) {
268
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
286
+ return {
287
+ ruleId: rule.ruleId,
288
+ outcome: 'fail',
289
+ severity: rule.defaultSeverity || 'moderate',
290
+ occurrences
291
+ };
269
292
  }
270
293
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
271
294
  }
272
295
 
273
- module.exports = { id, meta, runInPage };
296
+ module.exports = { id, meta, runInPage };
@@ -47,7 +47,8 @@ const id = 'aria-required-parent';
47
47
 
48
48
  const meta = {
49
49
  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.',
50
+ description:
51
+ '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
52
  i18n: {
52
53
  titleKey: 'ariaRequiredParent_title',
53
54
  descriptionKey: 'ariaRequiredParent_description'
@@ -56,7 +57,13 @@ const meta = {
56
57
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
57
58
  wcagSc: ['4.1.2'],
58
59
  normativeMappings: [
59
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
60
+ {
61
+ standard: 'WCAG',
62
+ version: '2.2',
63
+ requirement: '4.1.2',
64
+ title: 'Name, Role, Value',
65
+ conformanceLevel: 'A'
66
+ }
60
67
  ],
61
68
  defaultSeverity: 'moderate',
62
69
  category: 'robust',
@@ -66,7 +73,7 @@ const meta = {
66
73
  };
67
74
 
68
75
  function runInPage(ctx) {
69
- const { document, helpers, rule } = ctx;
76
+ const { helpers, rule } = ctx;
70
77
 
71
78
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
72
79
  if (!ariaHelpers) {
@@ -74,7 +81,8 @@ function runInPage(ctx) {
74
81
  }
75
82
 
76
83
  function isEligibleAcc(el) {
77
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
84
+ const fn =
85
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
78
86
  if (!fn) return true;
79
87
  try {
80
88
  const r = fn(el, ctx);
@@ -120,14 +128,25 @@ function runInPage(ctx) {
120
128
  // non-Element node (a ShadowRoot, nodeType 11) when climbing out of a
121
129
  // shadow tree that has no further light-DOM parent — skip those and keep
122
130
  // 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; };
131
+ const getComposedParent =
132
+ helpers && typeof helpers.composedParent === 'function'
133
+ ? helpers.composedParent
134
+ : function (n) {
135
+ return n && n.parentElement ? n.parentElement : null;
136
+ };
126
137
 
127
138
  function hasAcceptableAncestorContext(el, acceptableRoles, ownRole) {
128
139
  const allowsGroup = acceptableRoles.has('group');
129
140
  let cur = getComposedParent(el);
130
141
  let guard = 0;
142
+ // Mutable working copy: passing a transparent "group" ancestor also
143
+ // makes the element's OWN role an acceptable context from that point on
144
+ // (a nested treeitem-under-group-under-treeitem chain is a normal,
145
+ // arbitrarily-deep ARIA tree/list, not just one level) — mirroring that
146
+ // reference engine's getMissingContext, which pushes explicitRole into
147
+ // reqContext at the same point. Cloned lazily so the caller's Set (built
148
+ // once per element in runInPage) is never mutated.
149
+ let roles = acceptableRoles;
131
150
  while (cur && guard++ < 200) {
132
151
  if (cur.nodeType !== 1) {
133
152
  cur = getComposedParent(cur);
@@ -139,10 +158,12 @@ function runInPage(ctx) {
139
158
  continue;
140
159
  }
141
160
  if (role === 'group' && allowsGroup && GROUP_TRANSPARENT_FOR_ROLES.has(ownRole)) {
161
+ if (roles === acceptableRoles) roles = new Set(acceptableRoles);
162
+ roles.add(ownRole);
142
163
  cur = getComposedParent(cur);
143
164
  continue;
144
165
  }
145
- return acceptableRoles.has(role);
166
+ return roles.has(role);
146
167
  }
147
168
  return false;
148
169
  }
@@ -152,7 +173,9 @@ function runInPage(ctx) {
152
173
  const idTok = elId && String(elId).trim();
153
174
  if (!idTok) return false;
154
175
 
155
- const owners = helpers.queryAllSmart ? helpers.queryAllSmart('[aria-owns]') : helpers.queryAll('[aria-owns]');
176
+ const owners = helpers.queryAllSmart
177
+ ? helpers.queryAllSmart('[aria-owns]')
178
+ : helpers.queryAll('[aria-owns]');
156
179
  for (const owner of owners) {
157
180
  if (!owner || !owner.getAttribute) continue;
158
181
  const ownsAttr = owner.getAttribute('aria-owns') || '';
@@ -165,7 +188,9 @@ function runInPage(ctx) {
165
188
  return false;
166
189
  }
167
190
 
168
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
191
+ const nodes = helpers.queryAllSmart
192
+ ? helpers.queryAllSmart('[role]')
193
+ : helpers.queryAll('[role]');
169
194
 
170
195
  const occurrences = [];
171
196
  let applicableCount = 0;
@@ -191,7 +216,7 @@ function runInPage(ctx) {
191
216
  if (hasContext) continue;
192
217
 
193
218
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
194
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
219
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
195
220
 
196
221
  occurrences.push({
197
222
  selector: stableSelector,
@@ -204,7 +229,11 @@ function runInPage(ctx) {
204
229
  params: { role, requiredRoles: requiredContext.join(', ') }
205
230
  },
206
231
  data: {
207
- details: { reasonCode: 'ARIA_REQUIRED_PARENT_MISSING', role, requiredContextRoles: requiredContext }
232
+ details: {
233
+ reasonCode: 'ARIA_REQUIRED_PARENT_MISSING',
234
+ role,
235
+ requiredContextRoles: requiredContext
236
+ }
208
237
  }
209
238
  });
210
239
  }
@@ -213,9 +242,14 @@ function runInPage(ctx) {
213
242
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
214
243
  }
215
244
  if (occurrences.length) {
216
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
245
+ return {
246
+ ruleId: rule.ruleId,
247
+ outcome: 'fail',
248
+ severity: rule.defaultSeverity || 'moderate',
249
+ occurrences
250
+ };
217
251
  }
218
252
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
219
253
  }
220
254
 
221
- module.exports = { id, meta, runInPage };
255
+ module.exports = { id, meta, runInPage };
@@ -15,7 +15,8 @@ const id = 'aria-role-name-present';
15
15
 
16
16
  const meta = {
17
17
  title: 'ARIA widget/container roles have an accessible name',
18
- description: 'Checks that selected ARIA widget/container roles expose a non-empty accessible name.',
18
+ description:
19
+ 'Checks that selected ARIA widget/container roles expose a non-empty accessible name.',
19
20
  i18n: {
20
21
  titleKey: 'ariaRoleNamePresent_title',
21
22
  descriptionKey: 'ariaRoleNamePresent_description'
@@ -24,7 +25,13 @@ const meta = {
24
25
  tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name'],
25
26
  wcagSc: ['4.1.2'],
26
27
  normativeMappings: [
27
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
28
+ {
29
+ standard: 'WCAG',
30
+ version: '2.2',
31
+ requirement: '4.1.2',
32
+ title: 'Name, Role, Value',
33
+ conformanceLevel: 'A'
34
+ }
28
35
  ],
29
36
  defaultSeverity: 'serious',
30
37
  category: 'robust',
@@ -41,17 +48,19 @@ function runInPage(ctx) {
41
48
  const { document, root, helpers, rule } = ctx;
42
49
  const safeRoot = root || document;
43
50
 
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
- };
51
+ const queryAllSmart =
52
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
53
+ const queryAll =
54
+ helpers && typeof helpers.queryAll === 'function'
55
+ ? helpers.queryAll
56
+ : (sel, rt) => {
57
+ try {
58
+ const scope = rt || safeRoot;
59
+ return scope && scope.querySelectorAll ? Array.from(scope.querySelectorAll(sel)) : [];
60
+ } catch {
61
+ return [];
62
+ }
63
+ };
55
64
 
56
65
  const isAccTreeEligible =
57
66
  helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
@@ -60,9 +69,14 @@ function runInPage(ctx) {
60
69
  helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
61
70
 
62
71
  const getAriaLabelledByInfo =
63
- helpers && typeof helpers.getAriaLabelledByInfo === 'function' ? helpers.getAriaLabelledByInfo : null;
72
+ helpers && typeof helpers.getAriaLabelledByInfo === 'function'
73
+ ? helpers.getAriaLabelledByInfo
74
+ : null;
64
75
 
65
- const normalizeWs = (s) => String(s || '').replace(/\s+/g, ' ').trim();
76
+ const normalizeWs = (s) =>
77
+ String(s || '')
78
+ .replace(/\s+/g, ' ')
79
+ .trim();
66
80
 
67
81
  const getAttr = (el, name) => {
68
82
  try {
@@ -73,7 +87,6 @@ function runInPage(ctx) {
73
87
  }
74
88
  };
75
89
 
76
-
77
90
  // aria-labelledby resolution: delegate to the shared helper (used by
78
91
  // button-name-present et al.), which correctly includes hidden/aria-hidden
79
92
  // referenced nodes per the Accessible Name and Description Computation
@@ -120,7 +133,9 @@ function runInPage(ctx) {
120
133
 
121
134
  const nodes = (() => {
122
135
  try {
123
- return (queryAllSmart ? queryAllSmart(selector, safeRoot) : queryAll(selector, safeRoot)) || [];
136
+ return (
137
+ (queryAllSmart ? queryAllSmart(selector, safeRoot) : queryAll(selector, safeRoot)) || []
138
+ );
124
139
  } catch {
125
140
  return queryAll(selector, safeRoot);
126
141
  }
@@ -158,9 +173,15 @@ function runInPage(ctx) {
158
173
  const ok = !!(ariaLabel || labelled || title);
159
174
  if (ok) continue;
160
175
 
161
- const eligInfo = getEligibilityInfo ? (() => {
162
- try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; }
163
- })() : null;
176
+ const eligInfo = getEligibilityInfo
177
+ ? (() => {
178
+ try {
179
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
180
+ } catch {
181
+ return null;
182
+ }
183
+ })()
184
+ : null;
164
185
 
165
186
  const baseOccurrence = {
166
187
  summary: 'This element has no accessible name.',
@@ -195,7 +216,12 @@ function runInPage(ctx) {
195
216
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
196
217
  }
197
218
 
198
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
219
+ return {
220
+ ruleId: rule.ruleId,
221
+ outcome: 'fail',
222
+ severity: rule.defaultSeverity || 'minor',
223
+ occurrences
224
+ };
199
225
  }
200
226
 
201
227
  module.exports = { id, meta, runInPage };
@@ -25,7 +25,8 @@ const id = 'aria-roles-valid';
25
25
 
26
26
  const meta = {
27
27
  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.',
28
+ description:
29
+ 'Checks that an explicit role="" attribute resolves to a real, non-abstract WAI-ARIA role.',
29
30
  i18n: {
30
31
  titleKey: 'ariaRolesValid_title',
31
32
  descriptionKey: 'ariaRolesValid_description'
@@ -34,7 +35,13 @@ const meta = {
34
35
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
35
36
  wcagSc: ['4.1.2'],
36
37
  normativeMappings: [
37
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
38
+ {
39
+ standard: 'WCAG',
40
+ version: '2.2',
41
+ requirement: '4.1.2',
42
+ title: 'Name, Role, Value',
43
+ conformanceLevel: 'A'
44
+ }
38
45
  ],
39
46
  defaultSeverity: 'serious',
40
47
  category: 'robust',
@@ -44,14 +51,16 @@ const meta = {
44
51
  };
45
52
 
46
53
  function runInPage(ctx) {
47
- const { document, helpers, rule } = ctx;
54
+ const { helpers, rule } = ctx;
48
55
 
49
56
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
50
57
  if (!ariaHelpers) {
51
58
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
52
59
  }
53
60
 
54
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
61
+ const nodes = helpers.queryAllSmart
62
+ ? helpers.queryAllSmart('[role]')
63
+ : helpers.queryAll('[role]');
55
64
 
56
65
  const occurrences = [];
57
66
  let applicableCount = 0;
@@ -71,7 +80,7 @@ function runInPage(ctx) {
71
80
 
72
81
  const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
73
82
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
74
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
83
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
75
84
 
76
85
  occurrences.push({
77
86
  selector: stableSelector,
@@ -83,12 +92,8 @@ function runInPage(ctx) {
83
92
  ? 'Use a valid ARIA role token, or remove the role attribute if none applies.'
84
93
  : 'Replace this abstract role with a concrete role appropriate for the widget/structure.',
85
94
  i18n: {
86
- summaryKey: !isKnown
87
- ? 'ariaRolesValid_summary_invalid'
88
- : 'ariaRolesValid_summary_abstract',
89
- hintKey: !isKnown
90
- ? 'ariaRolesValid_hint_invalid'
91
- : 'ariaRolesValid_hint_abstract',
95
+ summaryKey: !isKnown ? 'ariaRolesValid_summary_invalid' : 'ariaRolesValid_summary_abstract',
96
+ hintKey: !isKnown ? 'ariaRolesValid_hint_invalid' : 'ariaRolesValid_hint_abstract',
92
97
  params: { role }
93
98
  },
94
99
  data: {
@@ -101,7 +106,12 @@ function runInPage(ctx) {
101
106
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
102
107
  }
103
108
  if (occurrences.length) {
104
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
109
+ return {
110
+ ruleId: rule.ruleId,
111
+ outcome: 'fail',
112
+ severity: rule.defaultSeverity || 'serious',
113
+ occurrences
114
+ };
105
115
  }
106
116
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
107
117
  }
@@ -38,7 +38,8 @@ const id = 'aria-valid-attr-value';
38
38
 
39
39
  const meta = {
40
40
  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).',
41
+ description:
42
+ '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
43
  i18n: {
43
44
  titleKey: 'ariaValidAttrValue_title',
44
45
  descriptionKey: 'ariaValidAttrValue_description'
@@ -47,7 +48,13 @@ const meta = {
47
48
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
48
49
  wcagSc: ['4.1.2'],
49
50
  normativeMappings: [
50
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
51
+ {
52
+ standard: 'WCAG',
53
+ version: '2.2',
54
+ requirement: '4.1.2',
55
+ title: 'Name, Role, Value',
56
+ conformanceLevel: 'A'
57
+ }
51
58
  ],
52
59
  defaultSeverity: 'serious',
53
60
  category: 'robust',
@@ -57,7 +64,7 @@ const meta = {
57
64
  };
58
65
 
59
66
  function runInPage(ctx) {
60
- const { document, helpers, rule } = ctx;
67
+ const { helpers, rule } = ctx;
61
68
 
62
69
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
63
70
  if (!ariaHelpers) {
@@ -85,14 +92,18 @@ function runInPage(ctx) {
85
92
  const result = ariaHelpers.validateAttrValue(name, rawValue);
86
93
  if (!result.valid) {
87
94
  if (!invalid) invalid = [];
88
- invalid.push({ name, value: rawValue == null ? '' : String(rawValue), reason: result.reason });
95
+ invalid.push({
96
+ name,
97
+ value: rawValue == null ? '' : String(rawValue),
98
+ reason: result.reason
99
+ });
89
100
  }
90
101
  }
91
102
 
92
103
  if (!invalid || !invalid.length) continue;
93
104
 
94
105
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
95
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
106
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
96
107
 
97
108
  for (const item of invalid) {
98
109
  occurrences.push({
@@ -106,7 +117,12 @@ function runInPage(ctx) {
106
117
  params: { attr: item.name, value: item.value }
107
118
  },
108
119
  data: {
109
- details: { reasonCode: 'ARIA_ATTR_VALUE_INVALID', attr: item.name, value: item.value, valueReason: item.reason }
120
+ details: {
121
+ reasonCode: 'ARIA_ATTR_VALUE_INVALID',
122
+ attr: item.name,
123
+ value: item.value,
124
+ valueReason: item.reason
125
+ }
110
126
  }
111
127
  });
112
128
  }
@@ -116,7 +132,12 @@ function runInPage(ctx) {
116
132
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
117
133
  }
118
134
  if (occurrences.length) {
119
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
135
+ return {
136
+ ruleId: rule.ruleId,
137
+ outcome: 'fail',
138
+ severity: rule.defaultSeverity || 'serious',
139
+ occurrences
140
+ };
120
141
  }
121
142
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
122
143
  }