@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
  const id = 'searchbox-name-present';
@@ -10,10 +12,16 @@ const meta = {
10
12
  descriptionKey: 'searchboxNamePresent_description'
11
13
  },
12
14
  helpUrl: null,
13
- tags: ["wcag2a", "wcag412", "forms", "atomic", "automatic", "name", "searchbox"],
15
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'searchbox'],
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,20 +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 ''; }
48
+ } catch {
49
+ return '';
50
+ }
41
51
  }
42
52
 
43
53
  function getConservativeSubtreeText(document, container) {
@@ -52,7 +62,7 @@ function runInPage(ctx) {
52
62
  const info = helpers.getContentNameInfo(container, ctx);
53
63
  return info && info.present ? info.value : '';
54
64
  }
55
- const t = (container && container.textContent) ? String(container.textContent) : '';
65
+ const t = container && container.textContent ? String(container.textContent) : '';
56
66
  return t.replace(/\s+/g, ' ').trim();
57
67
  }
58
68
 
@@ -100,7 +110,8 @@ function runInPage(ctx) {
100
110
  }
101
111
 
102
112
  function isEligibleAcc(helpers, el, ctx) {
103
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
113
+ const fn =
114
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
104
115
  if (!fn) return true;
105
116
  try {
106
117
  const r = fn(el, ctx);
@@ -126,12 +137,13 @@ function runInPage(ctx) {
126
137
  return map;
127
138
  }
128
139
 
129
-
130
140
  const occurrences = [];
131
141
  let applicableCount = 0;
132
142
 
133
- const selector = "[role=\"searchbox\"]";
134
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
143
+ const selector = '[role="searchbox"]';
144
+ const nodes = helpers.queryAllSmart
145
+ ? helpers.queryAllSmart(selector)
146
+ : helpers.queryAll(selector);
135
147
 
136
148
  // Precompute label[for] map for searchbox elements that are labelable
137
149
  // native form controls (e.g. <input role="searchbox">).
@@ -200,16 +212,22 @@ function runInPage(ctx) {
200
212
  if (res.ok) continue;
201
213
 
202
214
  const eligInfo = getEligibilityInfo
203
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
204
- : null;
215
+ ? (() => {
216
+ try {
217
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
218
+ } catch {
219
+ return null;
220
+ }
221
+ })()
222
+ : null;
205
223
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
206
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
224
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
207
225
 
208
226
  occurrences.push({
209
227
  selector: stableSelector,
210
228
  html,
211
229
  summary: 'This element has no accessible name.',
212
- hint: 'Provide aria-label or aria-labelledby (preferred), or provide visible text that is not hidden from assistive technologies.',
230
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this searchbox's accessible name.",
213
231
  i18n: {
214
232
  summaryKey: 'searchboxNamePresent_summary_fail',
215
233
  hintKey: 'searchboxNamePresent_hint_fail',
@@ -222,12 +240,16 @@ function runInPage(ctx) {
222
240
  });
223
241
  }
224
242
 
225
-
226
243
  if (applicableCount === 0) {
227
244
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
228
245
  }
229
246
  if (occurrences.length) {
230
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
247
+ return {
248
+ ruleId: rule.ruleId,
249
+ outcome: 'fail',
250
+ severity: rule.defaultSeverity || 'minor',
251
+ occurrences
252
+ };
231
253
  }
232
254
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
233
255
  }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -16,16 +18,16 @@
16
18
  * users. Client-side image maps (<map>/<area>, each with real href/alt)
17
19
  * are the accessible alternative and are not flagged by this rule.
18
20
  * @implementation-notes
19
- * - Matches a widely-used reference engine's server-side-image-map: presence of ismap is itself
20
- * the violation (there is no automatable way to verify a "usable
21
- * alternative" exists elsewhere on the page).
21
+ * - Presence of ismap is itself the violation (there is no automatable way
22
+ * to verify a "usable alternative" exists elsewhere on the page).
22
23
  */
23
24
 
24
25
  const id = 'server-side-image-map-absent';
25
26
 
26
27
  const meta = {
27
28
  title: 'Images must not use a server-side image map',
28
- description: 'Checks that <img> elements do not carry the ismap attribute (server-side image maps have no keyboard-operable equivalent).',
29
+ description:
30
+ 'Checks that <img> elements do not carry the ismap attribute (server-side image maps have no keyboard-operable equivalent).',
29
31
  i18n: {
30
32
  titleKey: 'serverSideImageMapAbsent_title',
31
33
  descriptionKey: 'serverSideImageMapAbsent_description'
@@ -34,7 +36,13 @@ const meta = {
34
36
  tags: ['wcag2a', 'wcag211', 'structure', 'atomic', 'automatic', 'keyboard'],
35
37
  wcagSc: ['2.1.1'],
36
38
  normativeMappings: [
37
- { standard: 'WCAG', version: '2.2', requirement: '2.1.1', title: 'Keyboard', conformanceLevel: 'A' }
39
+ {
40
+ standard: 'WCAG',
41
+ version: '2.2',
42
+ requirement: '2.1.1',
43
+ title: 'Keyboard',
44
+ conformanceLevel: 'A'
45
+ }
38
46
  ],
39
47
  defaultSeverity: 'serious',
40
48
  category: 'operable',
@@ -44,9 +52,11 @@ const meta = {
44
52
  };
45
53
 
46
54
  function runInPage(ctx) {
47
- const { document, helpers, rule } = ctx;
55
+ const { helpers, rule } = ctx;
48
56
 
49
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[ismap]') : helpers.queryAll('img[ismap]');
57
+ const nodes = helpers.queryAllSmart
58
+ ? helpers.queryAllSmart('img[ismap]')
59
+ : helpers.queryAll('img[ismap]');
50
60
 
51
61
  const occurrences = [];
52
62
  let applicableCount = 0;
@@ -57,12 +67,13 @@ function runInPage(ctx) {
57
67
  applicableCount += 1;
58
68
 
59
69
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
60
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
70
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
61
71
 
62
72
  occurrences.push({
63
73
  selector: stableSelector,
64
74
  html,
65
- summary: 'This image uses a server-side image map, which has no keyboard-operable equivalent.',
75
+ summary:
76
+ 'This image uses a server-side image map, which has no keyboard-operable equivalent.',
66
77
  hint: 'Replace the server-side image map (ismap) with a client-side image map (<map>/<area>) or separate accessible links/buttons.',
67
78
  i18n: {
68
79
  summaryKey: 'serverSideImageMapAbsent_summary_fail',
@@ -79,9 +90,14 @@ function runInPage(ctx) {
79
90
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
80
91
  }
81
92
  if (occurrences.length) {
82
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
93
+ return {
94
+ ruleId: rule.ruleId,
95
+ outcome: 'fail',
96
+ severity: rule.defaultSeverity || 'serious',
97
+ occurrences
98
+ };
83
99
  }
84
100
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
85
101
  }
86
102
 
87
- module.exports = { id, meta, runInPage };
103
+ module.exports = { id, meta, runInPage };
@@ -1,19 +1,28 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'slider-name-present';
4
6
 
5
7
  const meta = {
6
8
  title: 'Sliders have an accessible name',
7
- description: 'Checks that sliders (input[type="range"] and role="slider") expose a non-empty accessible name.',
9
+ description:
10
+ 'Checks that sliders (input[type="range"] and role="slider") expose a non-empty accessible name.',
8
11
  i18n: {
9
12
  titleKey: 'sliderNamePresent_title',
10
13
  descriptionKey: 'sliderNamePresent_description'
11
14
  },
12
15
  helpUrl: null,
13
- tags: ['wcag2a','wcag412','forms','atomic','automatic','name','slider'],
16
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'slider'],
14
17
  wcagSc: ['4.1.2'],
15
18
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
19
+ {
20
+ standard: 'WCAG',
21
+ version: '2.2',
22
+ requirement: '4.1.2',
23
+ title: 'Name, Role, Value',
24
+ conformanceLevel: 'A'
25
+ }
17
26
  ],
18
27
  defaultSeverity: 'serious',
19
28
  category: 'robust',
@@ -24,38 +33,22 @@ const meta = {
24
33
 
25
34
  function runInPage(ctx) {
26
35
  const { document, helpers, rule } = ctx;
27
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
29
- : null;
30
-
36
+ const getEligibilityInfo =
37
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
31
38
 
32
39
  function normalizeWs(s) {
33
- return String(s || '').replace(/\s+/g, ' ').trim();
40
+ return String(s || '')
41
+ .replace(/\s+/g, ' ')
42
+ .trim();
34
43
  }
35
44
 
36
45
  function getAttr(el, name) {
37
46
  try {
38
47
  if (!el || !el.getAttribute) return '';
39
48
  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;
49
+ } catch {
50
+ return '';
51
+ }
59
52
  }
60
53
 
61
54
  function buildLabelForMap(doc) {
@@ -85,7 +78,7 @@ function runInPage(ctx) {
85
78
  const info = helpers.getContentNameInfo(container, ctx);
86
79
  return info && info.present ? info.value : '';
87
80
  }
88
- const t = (container && container.textContent) ? String(container.textContent) : '';
81
+ const t = container && container.textContent ? String(container.textContent) : '';
89
82
  return t.replace(/\s+/g, ' ').trim();
90
83
  }
91
84
 
@@ -132,17 +125,9 @@ function runInPage(ctx) {
132
125
  return '';
133
126
  }
134
127
 
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
128
  function isEligibleAcc(helpers, el, ctx) {
145
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
129
+ const fn =
130
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
146
131
  if (!fn) return true;
147
132
  try {
148
133
  const r = fn(el, ctx);
@@ -153,13 +138,13 @@ function runInPage(ctx) {
153
138
  }
154
139
  }
155
140
 
156
-
157
141
  const occurrences = [];
158
142
  let applicableCount = 0;
159
143
 
160
144
  const selector = 'input[type="range"], [role="slider"]';
161
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
162
-
145
+ const nodes = helpers.queryAllSmart
146
+ ? helpers.queryAllSmart(selector)
147
+ : helpers.queryAll(selector);
163
148
 
164
149
  // Precompute label[for] map for native range inputs.
165
150
  const labelForMap = buildLabelForMap(document);
@@ -229,7 +214,7 @@ function runInPage(ctx) {
229
214
  const type = getAttr(el, 'type').toLowerCase();
230
215
  const role = getAttr(el, 'role').toLowerCase();
231
216
 
232
- let kind = '';
217
+ let kind;
233
218
  if (tag === 'input' && type === 'range') kind = 'native-slider';
234
219
  else if (role === 'slider') kind = 'aria-slider';
235
220
  else continue;
@@ -240,10 +225,16 @@ function runInPage(ctx) {
240
225
  if (res.ok) continue;
241
226
 
242
227
  const eligInfo = getEligibilityInfo
243
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
244
- : null;
228
+ ? (() => {
229
+ try {
230
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
231
+ } catch {
232
+ return null;
233
+ }
234
+ })()
235
+ : null;
245
236
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
246
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
237
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
247
238
 
248
239
  occurrences.push({
249
240
  selector: stableSelector,
@@ -262,12 +253,16 @@ function runInPage(ctx) {
262
253
  });
263
254
  }
264
255
 
265
-
266
256
  if (applicableCount === 0) {
267
257
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
268
258
  }
269
259
  if (occurrences.length) {
270
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
260
+ return {
261
+ ruleId: rule.ruleId,
262
+ outcome: 'fail',
263
+ severity: rule.defaultSeverity || 'minor',
264
+ occurrences
265
+ };
271
266
  }
272
267
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
273
268
  }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'spinbutton-name-present';
@@ -10,10 +12,16 @@ const meta = {
10
12
  descriptionKey: 'spinbuttonNamePresent_description'
11
13
  },
12
14
  helpUrl: null,
13
- tags: ["wcag2a", "wcag412", "forms", "atomic", "automatic", "name", "spinbutton"],
15
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'spinbutton'],
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,20 +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 ''; }
48
+ } catch {
49
+ return '';
50
+ }
41
51
  }
42
52
 
43
53
  function getConservativeSubtreeText(document, container) {
@@ -52,7 +62,7 @@ function runInPage(ctx) {
52
62
  const info = helpers.getContentNameInfo(container, ctx);
53
63
  return info && info.present ? info.value : '';
54
64
  }
55
- const t = (container && container.textContent) ? String(container.textContent) : '';
65
+ const t = container && container.textContent ? String(container.textContent) : '';
56
66
  return t.replace(/\s+/g, ' ').trim();
57
67
  }
58
68
 
@@ -100,7 +110,8 @@ function runInPage(ctx) {
100
110
  }
101
111
 
102
112
  function isEligibleAcc(helpers, el, ctx) {
103
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
113
+ const fn =
114
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
104
115
  if (!fn) return true;
105
116
  try {
106
117
  const r = fn(el, ctx);
@@ -126,12 +137,13 @@ function runInPage(ctx) {
126
137
  return map;
127
138
  }
128
139
 
129
-
130
140
  const occurrences = [];
131
141
  let applicableCount = 0;
132
142
 
133
- const selector = "[role=\"spinbutton\"]";
134
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
143
+ const selector = '[role="spinbutton"]';
144
+ const nodes = helpers.queryAllSmart
145
+ ? helpers.queryAllSmart(selector)
146
+ : helpers.queryAll(selector);
135
147
 
136
148
  // Precompute label[for] map for spinbutton elements that are labelable
137
149
  // native form controls (e.g. <input role="spinbutton">).
@@ -200,16 +212,22 @@ function runInPage(ctx) {
200
212
  if (res.ok) continue;
201
213
 
202
214
  const eligInfo = getEligibilityInfo
203
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
204
- : null;
215
+ ? (() => {
216
+ try {
217
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
218
+ } catch {
219
+ return null;
220
+ }
221
+ })()
222
+ : null;
205
223
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
206
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
224
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
207
225
 
208
226
  occurrences.push({
209
227
  selector: stableSelector,
210
228
  html,
211
229
  summary: 'This element has no accessible name.',
212
- hint: 'Provide aria-label or aria-labelledby (preferred), or provide visible text that is not hidden from assistive technologies.',
230
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this spinbutton's accessible name.",
213
231
  i18n: {
214
232
  summaryKey: 'spinbuttonNamePresent_summary_fail',
215
233
  hintKey: 'spinbuttonNamePresent_hint_fail',
@@ -222,12 +240,16 @@ function runInPage(ctx) {
222
240
  });
223
241
  }
224
242
 
225
-
226
243
  if (applicableCount === 0) {
227
244
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
228
245
  }
229
246
  if (occurrences.length) {
230
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
247
+ return {
248
+ ruleId: rule.ruleId,
249
+ outcome: 'fail',
250
+ severity: rule.defaultSeverity || 'minor',
251
+ occurrences
252
+ };
231
253
  }
232
254
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
233
255
  }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'summary-name-present';
@@ -13,7 +15,13 @@ const meta = {
13
15
  tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name'],
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,20 +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 ''; }
48
+ } catch {
49
+ return '';
50
+ }
41
51
  }
42
52
 
43
53
  function getConservativeSubtreeText(document, container) {
@@ -52,7 +62,7 @@ function runInPage(ctx) {
52
62
  const info = helpers.getContentNameInfo(container, ctx);
53
63
  return info && info.present ? info.value : '';
54
64
  }
55
- const t = (container && container.textContent) ? String(container.textContent) : '';
65
+ const t = container && container.textContent ? String(container.textContent) : '';
56
66
  return t.replace(/\s+/g, ' ').trim();
57
67
  }
58
68
 
@@ -74,7 +84,8 @@ function runInPage(ctx) {
74
84
  }
75
85
 
76
86
  function isEligibleAcc(helpers, el, ctx) {
77
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
87
+ const fn =
88
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
78
89
  if (!fn) return true;
79
90
  try {
80
91
  const r = fn(el, ctx);
@@ -85,11 +96,12 @@ function runInPage(ctx) {
85
96
  }
86
97
  }
87
98
 
88
-
89
99
  const occurrences = [];
90
100
  let applicableCount = 0;
91
101
 
92
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('summary') : helpers.queryAll('summary');
102
+ const nodes = helpers.queryAllSmart
103
+ ? helpers.queryAllSmart('summary')
104
+ : helpers.queryAll('summary');
93
105
 
94
106
  function evaluate(el) {
95
107
  const ariaLabel = getAttr(el, 'aria-label');
@@ -117,10 +129,16 @@ function runInPage(ctx) {
117
129
  if (res.ok) continue;
118
130
 
119
131
  const eligInfo = getEligibilityInfo
120
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
121
- : null;
132
+ ? (() => {
133
+ try {
134
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
135
+ } catch {
136
+ return null;
137
+ }
138
+ })()
139
+ : null;
122
140
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
123
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
141
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
124
142
 
125
143
  occurrences.push({
126
144
  selector: stableSelector,
@@ -139,14 +157,18 @@ function runInPage(ctx) {
139
157
  });
140
158
  }
141
159
 
142
-
143
160
  if (applicableCount === 0) {
144
161
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
145
162
  }
146
163
  if (occurrences.length) {
147
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
164
+ return {
165
+ ruleId: rule.ruleId,
166
+ outcome: 'fail',
167
+ severity: rule.defaultSeverity || 'minor',
168
+ occurrences
169
+ };
148
170
  }
149
171
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
150
172
  }
151
173
 
152
- module.exports = { id, meta, runInPage };
174
+ module.exports = { id, meta, runInPage };