@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
  /**
@@ -27,8 +29,8 @@
27
29
  * mechanism (e.g. a screen reader's "jump by heading" command).
28
30
  * @implementation-notes
29
31
  * - This rule intentionally checks presence, not position, for the
30
- * same-page-anchor condition (b): a widely-used reference engine's real bypass algorithm is
31
- * heuristic (see ROADMAP.md's Tier 1a note on why this rule was
32
+ * same-page-anchor condition (b): a full bypass algorithm is heuristic
33
+ * (see ROADMAP.md's Tier 1a note on why this rule was
32
34
  * deferred from the rest of that batch), and getting DOM-order /
33
35
  * keyboard-focus-order positioning exactly right without introducing
34
36
  * false positives is materially harder than the rest of Tier 1a. Being
@@ -49,7 +51,8 @@ const id = 'bypass-blocks-present';
49
51
 
50
52
  const meta = {
51
53
  title: 'Page must provide a way to bypass repeated blocks',
52
- description: 'Checks that the page has at least one recognized WCAG 2.4.1 bypass-blocks mechanism: a main landmark, a working same-page anchor link, or a heading.',
54
+ description:
55
+ 'Checks that the page has at least one recognized WCAG 2.4.1 bypass-blocks mechanism: a main landmark, a working same-page anchor link, or a heading.',
53
56
  i18n: {
54
57
  titleKey: 'bypassBlocksPresent_title',
55
58
  descriptionKey: 'bypassBlocksPresent_description'
@@ -58,7 +61,13 @@ const meta = {
58
61
  tags: ['wcag2a', 'wcag241', 'navigation', 'atomic', 'automatic'],
59
62
  wcagSc: ['2.4.1'],
60
63
  normativeMappings: [
61
- { standard: 'WCAG', version: '2.2', requirement: '2.4.1', title: 'Bypass Blocks', conformanceLevel: 'A' }
64
+ {
65
+ standard: 'WCAG',
66
+ version: '2.2',
67
+ requirement: '2.4.1',
68
+ title: 'Bypass Blocks',
69
+ conformanceLevel: 'A'
70
+ }
62
71
  ],
63
72
  defaultSeverity: 'serious',
64
73
  category: 'operable',
@@ -83,7 +92,8 @@ function runInPage(ctx) {
83
92
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
84
93
  }
85
94
 
86
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
95
+ const isAccTreeEligible =
96
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
87
97
 
88
98
  function isExposedToAt(el) {
89
99
  if (!isAccTreeEligible) return true;
@@ -107,12 +117,12 @@ function runInPage(ctx) {
107
117
  }
108
118
 
109
119
  // Filters through isAccTreeEligible (hidden/aria-hidden/display:none/inert
110
- // don't count as "a bypass mechanism is present") -- see page-has-heading-
111
- // one-manual.js's identical fix for the real-world trigger (CDC's flu
112
- // page, 2026-07-30: its only <h1> sits inside a display:none ancestor,
120
+ // don't count as "a bypass mechanism is present") -- see
121
+ // page-has-heading-one-manual.js for the identical real-world trigger
122
+ // (e.g. a page whose only <h1> sits inside a display:none ancestor,
113
123
  // unreachable by sighted and screen reader users alike). A fully
114
- // non-rendered <main>/heading was previously credited here too, wrongly
115
- // returning `pass` for a page with zero actual bypass mechanisms.
124
+ // non-rendered <main>/heading must not be credited here, since that would
125
+ // wrongly return `pass` for a page with zero actual bypass mechanisms.
116
126
  function hasMainLandmark() {
117
127
  for (const el of queryAll('main, [role="main"]')) {
118
128
  if (el && isExposedToAt(el)) return true;
@@ -121,7 +131,7 @@ function runInPage(ctx) {
121
131
  }
122
132
 
123
133
  function hasWorkingAnchorLink() {
124
- let links = [];
134
+ let links;
125
135
  try {
126
136
  links = document.querySelectorAll('a[href]');
127
137
  } catch {
@@ -140,7 +150,7 @@ function runInPage(ctx) {
140
150
  fragment = fragment.trim();
141
151
  if (!fragment) continue;
142
152
 
143
- let target = null;
153
+ let target;
144
154
  try {
145
155
  target = document.getElementById(fragment);
146
156
  } catch {
@@ -167,32 +177,41 @@ function runInPage(ctx) {
167
177
 
168
178
  const mainLandmark = hasMainLandmark();
169
179
  const anchorLink = mainLandmark ? false : hasWorkingAnchorLink();
170
- const heading = (mainLandmark || anchorLink) ? false : hasHeading();
180
+ const heading = mainLandmark || anchorLink ? false : hasHeading();
171
181
 
172
182
  if (mainLandmark || anchorLink || heading) {
173
183
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
174
184
  }
175
185
 
176
186
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
177
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(body) : (body.outerHTML || '').slice(0, 200);
178
-
179
- const occurrences = [{
180
- selector: stableSelector,
181
- html,
182
- summary: 'This page has no recognized way to bypass repeated blocks of content.',
183
- hint: 'Add a main landmark (<main> or role="main"), a working "skip to content" link, or heading elements that assistive technology can use to jump past repeated content.',
184
- i18n: {
185
- summaryKey: 'bypassBlocksPresent_summary_fail',
186
- hintKey: 'bypassBlocksPresent_hint_fail',
187
- params: {}
188
- },
189
- data: {
190
- details: { reasonCode: 'BYPASS_MECHANISM_ABSENT' },
191
- visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] }
187
+ const html = helpers.getOuterHtmlSnippet
188
+ ? helpers.getOuterHtmlSnippet(body)
189
+ : (body.outerHTML || '').slice(0, 200);
190
+
191
+ const occurrences = [
192
+ {
193
+ selector: stableSelector,
194
+ html,
195
+ summary: 'This page has no recognized way to bypass repeated blocks of content.',
196
+ hint: 'Add a main landmark (<main> or role="main"), a working "skip to content" link, or heading elements that assistive technology can use to jump past repeated content.',
197
+ i18n: {
198
+ summaryKey: 'bypassBlocksPresent_summary_fail',
199
+ hintKey: 'bypassBlocksPresent_hint_fail',
200
+ params: {}
201
+ },
202
+ data: {
203
+ details: { reasonCode: 'BYPASS_MECHANISM_ABSENT' },
204
+ visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] }
205
+ }
192
206
  }
193
- }];
194
-
195
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
207
+ ];
208
+
209
+ return {
210
+ ruleId: rule.ruleId,
211
+ outcome: 'fail',
212
+ severity: rule.defaultSeverity || 'serious',
213
+ occurrences
214
+ };
196
215
  }
197
216
 
198
- module.exports = { id, meta, runInPage, applicability };
217
+ module.exports = { id, meta, runInPage, applicability };
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -18,7 +20,8 @@ const id = 'canvas-text-alternative-present';
18
20
 
19
21
  const meta = {
20
22
  title: '<canvas> must provide a text alternative',
21
- description: 'Checks that <canvas> elements provide a text alternative via fallback content or an accessible name.',
23
+ description:
24
+ 'Checks that <canvas> elements provide a text alternative via fallback content or an accessible name.',
22
25
  i18n: {
23
26
  titleKey: 'canvas_textAltPresent_title',
24
27
  descriptionKey: 'canvas_textAltPresent_description'
@@ -27,7 +30,13 @@ const meta = {
27
30
  tags: ['wcag2a', 'wcag111', 'nontext', 'canvas', 'atomic', 'automatic'],
28
31
  wcagSc: ['1.1.1'],
29
32
  normativeMappings: [
30
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
33
+ {
34
+ standard: 'WCAG',
35
+ version: '2.2',
36
+ requirement: '1.1.1',
37
+ title: 'Non-text Content',
38
+ conformanceLevel: 'A'
39
+ }
31
40
  ],
32
41
  defaultSeverity: 'serious',
33
42
  category: 'perceivable',
@@ -44,29 +53,38 @@ function runInPage(ctx) {
44
53
  const { document, root, helpers, rule } = ctx;
45
54
  const safeRoot = root || document;
46
55
 
47
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
48
- const queryAll = helpers && typeof helpers.queryAll === 'function'
49
- ? helpers.queryAll
50
- : (sel) => {
51
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
52
- catch { return []; }
53
- };
54
-
55
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
56
- ? helpers.getEligibilityInfo
57
- : null;
58
-
59
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
60
- ? helpers.isAccTreeEligible
61
- : null;
62
-
63
- const getTextAlternativeInfo = helpers && typeof helpers.getTextAlternativeInfo === 'function'
64
- ? helpers.getTextAlternativeInfo
65
- : null;
56
+ const queryAllSmart =
57
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
58
+ const queryAll =
59
+ helpers && typeof helpers.queryAll === 'function'
60
+ ? helpers.queryAll
61
+ : (sel) => {
62
+ try {
63
+ return safeRoot && safeRoot.querySelectorAll
64
+ ? Array.from(safeRoot.querySelectorAll(sel))
65
+ : [];
66
+ } catch {
67
+ return [];
68
+ }
69
+ };
70
+
71
+ const getEligibilityInfo =
72
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
73
+
74
+ const isAccTreeEligible =
75
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
76
+
77
+ const getTextAlternativeInfo =
78
+ helpers && typeof helpers.getTextAlternativeInfo === 'function'
79
+ ? helpers.getTextAlternativeInfo
80
+ : null;
66
81
 
67
82
  const canvases = (() => {
68
- try { return Array.from((queryAllSmart ? queryAllSmart('canvas') : queryAll('canvas')) || []); }
69
- catch { return queryAll('canvas'); }
83
+ try {
84
+ return Array.from((queryAllSmart ? queryAllSmart('canvas') : queryAll('canvas')) || []);
85
+ } catch {
86
+ return queryAll('canvas');
87
+ }
70
88
  })();
71
89
 
72
90
  if (!canvases.length) {
@@ -82,7 +100,11 @@ function runInPage(ctx) {
82
100
  // Applicability: eligible in the accessibility tree (with focusable/IDREF exceptions handled by helper).
83
101
  if (isAccTreeEligible) {
84
102
  const elig = (() => {
85
- try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
103
+ try {
104
+ return isAccTreeEligible(el, ctx);
105
+ } catch {
106
+ return { eligible: true, reasons: [] };
107
+ }
86
108
  })();
87
109
  if (elig && elig.eligible === false) continue;
88
110
  }
@@ -91,7 +113,13 @@ function runInPage(ctx) {
91
113
 
92
114
  // Expectation: must provide a text alternative.
93
115
  const ti = getTextAlternativeInfo
94
- ? (() => { try { return getTextAlternativeInfo(el, ctx); } catch { return null; } })()
116
+ ? (() => {
117
+ try {
118
+ return getTextAlternativeInfo(el, ctx);
119
+ } catch {
120
+ return null;
121
+ }
122
+ })()
95
123
  : null;
96
124
 
97
125
  const hasTextAlt = !!(ti && ti.present);
@@ -134,7 +162,12 @@ function runInPage(ctx) {
134
162
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
135
163
  }
136
164
 
137
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
165
+ return {
166
+ ruleId: rule.ruleId,
167
+ outcome: 'fail',
168
+ severity: rule.defaultSeverity || 'minor',
169
+ occurrences
170
+ };
138
171
  }
139
172
 
140
173
  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
  const id = 'combobox-name-present';
@@ -10,10 +12,16 @@ const meta = {
10
12
  descriptionKey: 'comboboxNamePresent_description'
11
13
  },
12
14
  helpUrl: null,
13
- tags: ['wcag2a','wcag412','forms','atomic','automatic','name','combobox'],
15
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'combobox'],
14
16
  wcagSc: ['4.1.2'],
15
17
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
18
+ {
19
+ standard: 'WCAG',
20
+ version: '2.2',
21
+ requirement: '4.1.2',
22
+ title: 'Name, Role, Value',
23
+ conformanceLevel: 'A'
24
+ }
17
25
  ],
18
26
  defaultSeverity: 'serious',
19
27
  category: 'robust',
@@ -24,38 +32,22 @@ const meta = {
24
32
 
25
33
  function runInPage(ctx) {
26
34
  const { document, helpers, rule } = ctx;
27
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
29
- : null;
30
-
35
+ const getEligibilityInfo =
36
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
31
37
 
32
38
  function normalizeWs(s) {
33
- return String(s || '').replace(/\s+/g, ' ').trim();
39
+ return String(s || '')
40
+ .replace(/\s+/g, ' ')
41
+ .trim();
34
42
  }
35
43
 
36
44
  function getAttr(el, name) {
37
45
  try {
38
46
  if (!el || !el.getAttribute) return '';
39
47
  return normalizeWs(el.getAttribute(name));
40
- } catch { return ''; }
41
- }
42
-
43
- function hasAttr(el, name) {
44
- try { return !!(el && el.hasAttribute && el.hasAttribute(name)); } catch { return false; }
45
- }
46
-
47
- function isExplicitProgrammatic(el) {
48
- // Programmatic name mechanisms we treat as authoritative (presence-only):
49
- // - aria-label (non-empty)
50
- // - aria-labelledby (non-empty)
51
- // - title (non-empty) [weak but allowed for presence]
52
- const al = getAttr(el, 'aria-label');
53
- if (al) return true;
54
- const alb = getAttr(el, 'aria-labelledby');
55
- if (alb) return true;
56
- const t = getAttr(el, 'title');
57
- if (t) return true;
58
- return false;
48
+ } catch {
49
+ return '';
50
+ }
59
51
  }
60
52
 
61
53
  function buildLabelForMap(doc) {
@@ -85,7 +77,7 @@ function runInPage(ctx) {
85
77
  const info = helpers.getContentNameInfo(container, ctx);
86
78
  return info && info.present ? info.value : '';
87
79
  }
88
- const t = (container && container.textContent) ? String(container.textContent) : '';
80
+ const t = container && container.textContent ? String(container.textContent) : '';
89
81
  return t.replace(/\s+/g, ' ').trim();
90
82
  }
91
83
 
@@ -132,17 +124,9 @@ function runInPage(ctx) {
132
124
  return '';
133
125
  }
134
126
 
135
- function getInputValueName(el) {
136
- try {
137
- const type = getAttr(el, 'type').toLowerCase();
138
- if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
139
- const v = getAttr(el, 'value');
140
- return v;
141
- } catch { return ''; }
142
- }
143
-
144
127
  function isEligibleAcc(helpers, el, ctx) {
145
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
128
+ const fn =
129
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
146
130
  if (!fn) return true;
147
131
  try {
148
132
  const r = fn(el, ctx);
@@ -153,12 +137,13 @@ function runInPage(ctx) {
153
137
  }
154
138
  }
155
139
 
156
-
157
140
  const occurrences = [];
158
141
  let applicableCount = 0;
159
142
 
160
143
  const selector = '[role="combobox"]';
161
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
144
+ const nodes = helpers.queryAllSmart
145
+ ? helpers.queryAllSmart(selector)
146
+ : helpers.queryAll(selector);
162
147
 
163
148
  // Precompute label[for] map for combobox elements that are labelable
164
149
  // native form controls (e.g. <input role="combobox">).
@@ -231,16 +216,22 @@ function runInPage(ctx) {
231
216
  if (res.ok) continue;
232
217
 
233
218
  const eligInfo = getEligibilityInfo
234
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
235
- : null;
219
+ ? (() => {
220
+ try {
221
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
222
+ } catch {
223
+ return null;
224
+ }
225
+ })()
226
+ : null;
236
227
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
237
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
228
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
238
229
 
239
230
  occurrences.push({
240
231
  selector: stableSelector,
241
232
  html,
242
233
  summary: 'This combobox has no accessible name.',
243
- hint: 'Provide aria-label or aria-labelledby (preferred), or ensure the element has visible text that is not hidden from assistive technologies.',
234
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this combobox's accessible name.",
244
235
  i18n: {
245
236
  summaryKey: 'comboboxNamePresent_summary_fail',
246
237
  hintKey: 'comboboxNamePresent_hint_fail',
@@ -253,12 +244,16 @@ function runInPage(ctx) {
253
244
  });
254
245
  }
255
246
 
256
-
257
247
  if (applicableCount === 0) {
258
248
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
259
249
  }
260
250
  if (occurrences.length) {
261
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
251
+ return {
252
+ ruleId: rule.ruleId,
253
+ outcome: 'fail',
254
+ severity: rule.defaultSeverity || 'minor',
255
+ occurrences
256
+ };
262
257
  }
263
258
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
264
259
  }