@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
@@ -49,7 +49,8 @@ const id = 'bypass-blocks-present';
49
49
 
50
50
  const meta = {
51
51
  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.',
52
+ description:
53
+ '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
54
  i18n: {
54
55
  titleKey: 'bypassBlocksPresent_title',
55
56
  descriptionKey: 'bypassBlocksPresent_description'
@@ -58,7 +59,13 @@ const meta = {
58
59
  tags: ['wcag2a', 'wcag241', 'navigation', 'atomic', 'automatic'],
59
60
  wcagSc: ['2.4.1'],
60
61
  normativeMappings: [
61
- { standard: 'WCAG', version: '2.2', requirement: '2.4.1', title: 'Bypass Blocks', conformanceLevel: 'A' }
62
+ {
63
+ standard: 'WCAG',
64
+ version: '2.2',
65
+ requirement: '2.4.1',
66
+ title: 'Bypass Blocks',
67
+ conformanceLevel: 'A'
68
+ }
62
69
  ],
63
70
  defaultSeverity: 'serious',
64
71
  category: 'operable',
@@ -67,6 +74,14 @@ const meta = {
67
74
  coverage: { facetsBySc: { '2.4.1': ['bypass-blocks-present'] } }
68
75
  };
69
76
 
77
+ // This check is inherently whole-document (does the PAGE have this
78
+ // property?), not evaluable per-subtree -- notApplicable when contextSelector
79
+ // scoped this run narrower than the whole document, or when
80
+ // engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
81
+ function applicability(ctx) {
82
+ return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
83
+ }
84
+
70
85
  function runInPage(ctx) {
71
86
  const { document, helpers, rule } = ctx;
72
87
 
@@ -75,7 +90,8 @@ function runInPage(ctx) {
75
90
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
76
91
  }
77
92
 
78
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
93
+ const isAccTreeEligible =
94
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
79
95
 
80
96
  function isExposedToAt(el) {
81
97
  if (!isAccTreeEligible) return true;
@@ -113,7 +129,7 @@ function runInPage(ctx) {
113
129
  }
114
130
 
115
131
  function hasWorkingAnchorLink() {
116
- let links = [];
132
+ let links;
117
133
  try {
118
134
  links = document.querySelectorAll('a[href]');
119
135
  } catch {
@@ -132,7 +148,7 @@ function runInPage(ctx) {
132
148
  fragment = fragment.trim();
133
149
  if (!fragment) continue;
134
150
 
135
- let target = null;
151
+ let target;
136
152
  try {
137
153
  target = document.getElementById(fragment);
138
154
  } catch {
@@ -159,32 +175,41 @@ function runInPage(ctx) {
159
175
 
160
176
  const mainLandmark = hasMainLandmark();
161
177
  const anchorLink = mainLandmark ? false : hasWorkingAnchorLink();
162
- const heading = (mainLandmark || anchorLink) ? false : hasHeading();
178
+ const heading = mainLandmark || anchorLink ? false : hasHeading();
163
179
 
164
180
  if (mainLandmark || anchorLink || heading) {
165
181
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
166
182
  }
167
183
 
168
184
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
169
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(body) : (body.outerHTML || '').slice(0, 200);
170
-
171
- const occurrences = [{
172
- selector: stableSelector,
173
- html,
174
- summary: 'This page has no recognized way to bypass repeated blocks of content.',
175
- 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.',
176
- i18n: {
177
- summaryKey: 'bypassBlocksPresent_summary_fail',
178
- hintKey: 'bypassBlocksPresent_hint_fail',
179
- params: {}
180
- },
181
- data: {
182
- details: { reasonCode: 'BYPASS_MECHANISM_ABSENT' },
183
- visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] }
185
+ const html = helpers.getOuterHtmlSnippet
186
+ ? helpers.getOuterHtmlSnippet(body)
187
+ : (body.outerHTML || '').slice(0, 200);
188
+
189
+ const occurrences = [
190
+ {
191
+ selector: stableSelector,
192
+ html,
193
+ summary: 'This page has no recognized way to bypass repeated blocks of content.',
194
+ 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.',
195
+ i18n: {
196
+ summaryKey: 'bypassBlocksPresent_summary_fail',
197
+ hintKey: 'bypassBlocksPresent_hint_fail',
198
+ params: {}
199
+ },
200
+ data: {
201
+ details: { reasonCode: 'BYPASS_MECHANISM_ABSENT' },
202
+ visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] }
203
+ }
184
204
  }
185
- }];
186
-
187
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
205
+ ];
206
+
207
+ return {
208
+ ruleId: rule.ruleId,
209
+ outcome: 'fail',
210
+ severity: rule.defaultSeverity || 'serious',
211
+ occurrences
212
+ };
188
213
  }
189
214
 
190
- module.exports = { id, meta, runInPage };
215
+ module.exports = { id, meta, runInPage, applicability };
@@ -18,7 +18,8 @@ const id = 'canvas-text-alternative-present';
18
18
 
19
19
  const meta = {
20
20
  title: '<canvas> must provide a text alternative',
21
- description: 'Checks that <canvas> elements provide a text alternative via fallback content or an accessible name.',
21
+ description:
22
+ 'Checks that <canvas> elements provide a text alternative via fallback content or an accessible name.',
22
23
  i18n: {
23
24
  titleKey: 'canvas_textAltPresent_title',
24
25
  descriptionKey: 'canvas_textAltPresent_description'
@@ -27,7 +28,13 @@ const meta = {
27
28
  tags: ['wcag2a', 'wcag111', 'nontext', 'canvas', 'atomic', 'automatic'],
28
29
  wcagSc: ['1.1.1'],
29
30
  normativeMappings: [
30
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
31
+ {
32
+ standard: 'WCAG',
33
+ version: '2.2',
34
+ requirement: '1.1.1',
35
+ title: 'Non-text Content',
36
+ conformanceLevel: 'A'
37
+ }
31
38
  ],
32
39
  defaultSeverity: 'serious',
33
40
  category: 'perceivable',
@@ -44,29 +51,38 @@ function runInPage(ctx) {
44
51
  const { document, root, helpers, rule } = ctx;
45
52
  const safeRoot = root || document;
46
53
 
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;
54
+ const queryAllSmart =
55
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
56
+ const queryAll =
57
+ helpers && typeof helpers.queryAll === 'function'
58
+ ? helpers.queryAll
59
+ : (sel) => {
60
+ try {
61
+ return safeRoot && safeRoot.querySelectorAll
62
+ ? Array.from(safeRoot.querySelectorAll(sel))
63
+ : [];
64
+ } catch {
65
+ return [];
66
+ }
67
+ };
68
+
69
+ const getEligibilityInfo =
70
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
71
+
72
+ const isAccTreeEligible =
73
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
74
+
75
+ const getTextAlternativeInfo =
76
+ helpers && typeof helpers.getTextAlternativeInfo === 'function'
77
+ ? helpers.getTextAlternativeInfo
78
+ : null;
66
79
 
67
80
  const canvases = (() => {
68
- try { return Array.from((queryAllSmart ? queryAllSmart('canvas') : queryAll('canvas')) || []); }
69
- catch { return queryAll('canvas'); }
81
+ try {
82
+ return Array.from((queryAllSmart ? queryAllSmart('canvas') : queryAll('canvas')) || []);
83
+ } catch {
84
+ return queryAll('canvas');
85
+ }
70
86
  })();
71
87
 
72
88
  if (!canvases.length) {
@@ -82,7 +98,11 @@ function runInPage(ctx) {
82
98
  // Applicability: eligible in the accessibility tree (with focusable/IDREF exceptions handled by helper).
83
99
  if (isAccTreeEligible) {
84
100
  const elig = (() => {
85
- try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
101
+ try {
102
+ return isAccTreeEligible(el, ctx);
103
+ } catch {
104
+ return { eligible: true, reasons: [] };
105
+ }
86
106
  })();
87
107
  if (elig && elig.eligible === false) continue;
88
108
  }
@@ -91,7 +111,13 @@ function runInPage(ctx) {
91
111
 
92
112
  // Expectation: must provide a text alternative.
93
113
  const ti = getTextAlternativeInfo
94
- ? (() => { try { return getTextAlternativeInfo(el, ctx); } catch { return null; } })()
114
+ ? (() => {
115
+ try {
116
+ return getTextAlternativeInfo(el, ctx);
117
+ } catch {
118
+ return null;
119
+ }
120
+ })()
95
121
  : null;
96
122
 
97
123
  const hasTextAlt = !!(ti && ti.present);
@@ -134,7 +160,12 @@ function runInPage(ctx) {
134
160
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
135
161
  }
136
162
 
137
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
163
+ return {
164
+ ruleId: rule.ruleId,
165
+ outcome: 'fail',
166
+ severity: rule.defaultSeverity || 'minor',
167
+ occurrences
168
+ };
138
169
  }
139
170
 
140
171
  module.exports = { id, meta, runInPage };
@@ -10,10 +10,16 @@ const meta = {
10
10
  descriptionKey: 'comboboxNamePresent_description'
11
11
  },
12
12
  helpUrl: null,
13
- tags: ['wcag2a','wcag412','forms','atomic','automatic','name','combobox'],
13
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'combobox'],
14
14
  wcagSc: ['4.1.2'],
15
15
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
16
+ {
17
+ standard: 'WCAG',
18
+ version: '2.2',
19
+ requirement: '4.1.2',
20
+ title: 'Name, Role, Value',
21
+ conformanceLevel: 'A'
22
+ }
17
23
  ],
18
24
  defaultSeverity: 'serious',
19
25
  category: 'robust',
@@ -24,38 +30,22 @@ const meta = {
24
30
 
25
31
  function runInPage(ctx) {
26
32
  const { document, helpers, rule } = ctx;
27
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
29
- : null;
30
-
33
+ const getEligibilityInfo =
34
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
31
35
 
32
36
  function normalizeWs(s) {
33
- return String(s || '').replace(/\s+/g, ' ').trim();
37
+ return String(s || '')
38
+ .replace(/\s+/g, ' ')
39
+ .trim();
34
40
  }
35
41
 
36
42
  function getAttr(el, name) {
37
43
  try {
38
44
  if (!el || !el.getAttribute) return '';
39
45
  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;
46
+ } catch {
47
+ return '';
48
+ }
59
49
  }
60
50
 
61
51
  function buildLabelForMap(doc) {
@@ -85,7 +75,7 @@ function runInPage(ctx) {
85
75
  const info = helpers.getContentNameInfo(container, ctx);
86
76
  return info && info.present ? info.value : '';
87
77
  }
88
- const t = (container && container.textContent) ? String(container.textContent) : '';
78
+ const t = container && container.textContent ? String(container.textContent) : '';
89
79
  return t.replace(/\s+/g, ' ').trim();
90
80
  }
91
81
 
@@ -132,17 +122,9 @@ function runInPage(ctx) {
132
122
  return '';
133
123
  }
134
124
 
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
125
  function isEligibleAcc(helpers, el, ctx) {
145
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
126
+ const fn =
127
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
146
128
  if (!fn) return true;
147
129
  try {
148
130
  const r = fn(el, ctx);
@@ -153,12 +135,13 @@ function runInPage(ctx) {
153
135
  }
154
136
  }
155
137
 
156
-
157
138
  const occurrences = [];
158
139
  let applicableCount = 0;
159
140
 
160
141
  const selector = '[role="combobox"]';
161
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
142
+ const nodes = helpers.queryAllSmart
143
+ ? helpers.queryAllSmart(selector)
144
+ : helpers.queryAll(selector);
162
145
 
163
146
  // Precompute label[for] map for combobox elements that are labelable
164
147
  // native form controls (e.g. <input role="combobox">).
@@ -231,16 +214,22 @@ function runInPage(ctx) {
231
214
  if (res.ok) continue;
232
215
 
233
216
  const eligInfo = getEligibilityInfo
234
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
235
- : null;
217
+ ? (() => {
218
+ try {
219
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
220
+ } catch {
221
+ return null;
222
+ }
223
+ })()
224
+ : null;
236
225
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
237
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
226
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
238
227
 
239
228
  occurrences.push({
240
229
  selector: stableSelector,
241
230
  html,
242
231
  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.',
232
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this combobox's accessible name.",
244
233
  i18n: {
245
234
  summaryKey: 'comboboxNamePresent_summary_fail',
246
235
  hintKey: 'comboboxNamePresent_hint_fail',
@@ -253,12 +242,16 @@ function runInPage(ctx) {
253
242
  });
254
243
  }
255
244
 
256
-
257
245
  if (applicableCount === 0) {
258
246
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
259
247
  }
260
248
  if (occurrences.length) {
261
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
249
+ return {
250
+ ruleId: rule.ruleId,
251
+ outcome: 'fail',
252
+ severity: rule.defaultSeverity || 'minor',
253
+ occurrences
254
+ };
262
255
  }
263
256
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
264
257
  }