@surea11y/core 1.2.0 → 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 (157) hide show
  1. package/CHANGELOG.md +37 -7
  2. package/LICENSE +373 -21
  3. package/README.md +67 -1
  4. package/bin/core.js +144 -19
  5. package/docs/API_STABILITY.md +1 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/CLI.md +54 -1
  9. package/docs/ENGINE_OPTIONS.md +2 -0
  10. package/docs/INTEGRATION.md +18 -0
  11. package/docs/OUTPUT_SCHEMA.md +1 -1
  12. package/docs/RULE_CATALOG.md +1 -1
  13. package/docs/SARIF.md +59 -0
  14. package/package.json +15 -4
  15. package/src/baseline.js +0 -0
  16. package/src/catalogs/composites.wcag.js +414 -450
  17. package/src/checks/automatic/area-alt-present.js +59 -25
  18. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  19. package/src/checks/automatic/aria-allowed-role.js +21 -7
  20. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  21. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  22. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  23. package/src/checks/automatic/aria-hidden-body.js +42 -19
  24. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  25. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  26. package/src/checks/automatic/aria-prohibited-children.js +55 -16
  27. package/src/checks/automatic/aria-required-attr.js +23 -8
  28. package/src/checks/automatic/aria-required-children.js +37 -14
  29. package/src/checks/automatic/aria-required-parent.js +48 -14
  30. package/src/checks/automatic/aria-role-name-present.js +47 -21
  31. package/src/checks/automatic/aria-roles-valid.js +22 -12
  32. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  33. package/src/checks/automatic/aria-valid-attr.js +17 -5
  34. package/src/checks/automatic/autocomplete-valid.js +74 -16
  35. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  36. package/src/checks/automatic/binary-control-name-present.js +60 -50
  37. package/src/checks/automatic/button-name-present.js +48 -18
  38. package/src/checks/automatic/bypass-blocks-present.js +42 -25
  39. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  40. package/src/checks/automatic/combobox-name-present.js +38 -45
  41. package/src/checks/automatic/contrast-computable.js +361 -341
  42. package/src/checks/automatic/contrast-enhanced.js +487 -466
  43. package/src/checks/automatic/contrast-minimum.js +486 -465
  44. package/src/checks/automatic/css-orientation-lock.js +30 -8
  45. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  46. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  47. package/src/checks/automatic/dialog-name-present.js +37 -75
  48. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  49. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  50. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  51. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  52. package/src/checks/automatic/form-control-single-label.js +47 -10
  53. package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
  54. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  55. package/src/checks/automatic/iframe-name-present.js +31 -9
  56. package/src/checks/automatic/iframe-title-unique.js +29 -8
  57. package/src/checks/automatic/img-alt-present.js +47 -43
  58. package/src/checks/automatic/input-image-alt-present.js +143 -112
  59. package/src/checks/automatic/label-in-name.js +50 -22
  60. package/src/checks/automatic/language-page-present.js +109 -109
  61. package/src/checks/automatic/link-in-text-block.js +59 -19
  62. package/src/checks/automatic/link-name-present.js +45 -14
  63. package/src/checks/automatic/list-children-valid.js +26 -9
  64. package/src/checks/automatic/listbox-name-present.js +39 -19
  65. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  66. package/src/checks/automatic/menuitem-name-present.js +39 -61
  67. package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
  68. package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
  69. package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
  70. package/src/checks/automatic/meter-name-present.js +36 -33
  71. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  72. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  73. package/src/checks/automatic/option-name-present.js +38 -21
  74. package/src/checks/automatic/page-title-present.js +17 -6
  75. package/src/checks/automatic/progressbar-name-present.js +41 -34
  76. package/src/checks/automatic/role-img-alt-present.js +209 -157
  77. package/src/checks/automatic/searchbox-name-present.js +39 -19
  78. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  79. package/src/checks/automatic/slider-name-present.js +40 -47
  80. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  81. package/src/checks/automatic/summary-name-present.js +37 -17
  82. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  83. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  84. package/src/checks/automatic/tab-name-present.js +37 -60
  85. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  86. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  87. package/src/checks/automatic/target-size-minimum.js +118 -48
  88. package/src/checks/automatic/td-has-header.js +29 -11
  89. package/src/checks/automatic/textbox-name-present.js +39 -19
  90. package/src/checks/automatic/tooltip-name-present.js +37 -18
  91. package/src/checks/automatic/treeitem-name-present.js +38 -21
  92. package/src/checks/automatic/valid-lang.js +20 -6
  93. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  94. package/src/checks/manual/accesskeys-manual.js +14 -5
  95. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  96. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  97. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  98. package/src/checks/manual/aria-text-manual.js +14 -6
  99. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  100. package/src/checks/manual/css-hidden-focus.js +196 -165
  101. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  102. package/src/checks/manual/empty-heading-manual.js +24 -7
  103. package/src/checks/manual/empty-table-header-manual.js +17 -6
  104. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  105. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  106. package/src/checks/manual/heading-order-manual.js +22 -7
  107. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  108. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  109. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  110. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  111. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  112. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  113. package/src/checks/manual/label-title-only-manual.js +14 -5
  114. package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
  115. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
  116. package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
  117. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
  118. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
  119. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  120. package/src/checks/manual/landmark-one-main-manual.js +26 -20
  121. package/src/checks/manual/landmark-unique-manual.js +41 -15
  122. package/src/checks/manual/link-name-quality-manual.js +43 -12
  123. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  124. package/src/checks/manual/meta-viewport-large-manual.js +16 -5
  125. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  126. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  127. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  128. package/src/checks/manual/p-as-heading-manual.js +22 -7
  129. package/src/checks/manual/page-has-heading-one-manual.js +31 -22
  130. package/src/checks/manual/page-title-patterns-manual.js +78 -50
  131. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  132. package/src/checks/manual/region-manual.js +241 -48
  133. package/src/checks/manual/scope-attr-valid-manual.js +10 -3
  134. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  135. package/src/checks/manual/skip-link-manual.js +35 -12
  136. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  137. package/src/checks/manual/tabindex-manual.js +10 -3
  138. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  139. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  140. package/src/checks/manual/video-caption-manual.js +15 -4
  141. package/src/checks/manual-review.js +56 -12
  142. package/src/core/aria-helpers.js +1127 -886
  143. package/src/core/contrast-helpers.js +1217 -1062
  144. package/src/core/dom-helpers.js +4175 -3917
  145. package/src/core/dom-runner.js +720 -604
  146. package/src/core/frame-messaging.js +189 -138
  147. package/src/core/frame-scan.js +94 -82
  148. package/src/core/rollup-composites.js +94 -102
  149. package/src/core/rule-meta.js +58 -41
  150. package/src/core.js +36552 -29111
  151. package/src/i18n/en.js +1194 -889
  152. package/src/i18n/fr.js +1136 -795
  153. package/src/policy/contracts.js +13 -13
  154. package/src/policy/resolvePolicy.js +48 -44
  155. package/src/report.js +63 -43
  156. package/src/sarif.js +175 -0
  157. package/surea11y.browser.js +36042 -0
@@ -25,7 +25,8 @@ const id = 'server-side-image-map-absent';
25
25
 
26
26
  const meta = {
27
27
  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).',
28
+ description:
29
+ 'Checks that <img> elements do not carry the ismap attribute (server-side image maps have no keyboard-operable equivalent).',
29
30
  i18n: {
30
31
  titleKey: 'serverSideImageMapAbsent_title',
31
32
  descriptionKey: 'serverSideImageMapAbsent_description'
@@ -34,7 +35,13 @@ const meta = {
34
35
  tags: ['wcag2a', 'wcag211', 'structure', 'atomic', 'automatic', 'keyboard'],
35
36
  wcagSc: ['2.1.1'],
36
37
  normativeMappings: [
37
- { standard: 'WCAG', version: '2.2', requirement: '2.1.1', title: 'Keyboard', conformanceLevel: 'A' }
38
+ {
39
+ standard: 'WCAG',
40
+ version: '2.2',
41
+ requirement: '2.1.1',
42
+ title: 'Keyboard',
43
+ conformanceLevel: 'A'
44
+ }
38
45
  ],
39
46
  defaultSeverity: 'serious',
40
47
  category: 'operable',
@@ -44,9 +51,11 @@ 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
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[ismap]') : helpers.queryAll('img[ismap]');
56
+ const nodes = helpers.queryAllSmart
57
+ ? helpers.queryAllSmart('img[ismap]')
58
+ : helpers.queryAll('img[ismap]');
50
59
 
51
60
  const occurrences = [];
52
61
  let applicableCount = 0;
@@ -57,12 +66,13 @@ function runInPage(ctx) {
57
66
  applicableCount += 1;
58
67
 
59
68
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
60
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
69
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
61
70
 
62
71
  occurrences.push({
63
72
  selector: stableSelector,
64
73
  html,
65
- summary: 'This image uses a server-side image map, which has no keyboard-operable equivalent.',
74
+ summary:
75
+ 'This image uses a server-side image map, which has no keyboard-operable equivalent.',
66
76
  hint: 'Replace the server-side image map (ismap) with a client-side image map (<map>/<area>) or separate accessible links/buttons.',
67
77
  i18n: {
68
78
  summaryKey: 'serverSideImageMapAbsent_summary_fail',
@@ -79,9 +89,14 @@ function runInPage(ctx) {
79
89
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
80
90
  }
81
91
  if (occurrences.length) {
82
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
92
+ return {
93
+ ruleId: rule.ruleId,
94
+ outcome: 'fail',
95
+ severity: rule.defaultSeverity || 'serious',
96
+ occurrences
97
+ };
83
98
  }
84
99
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
85
100
  }
86
101
 
87
- module.exports = { id, meta, runInPage };
102
+ module.exports = { id, meta, runInPage };
@@ -4,16 +4,23 @@ const id = 'slider-name-present';
4
4
 
5
5
  const meta = {
6
6
  title: 'Sliders have an accessible name',
7
- description: 'Checks that sliders (input[type="range"] and role="slider") expose a non-empty accessible name.',
7
+ description:
8
+ 'Checks that sliders (input[type="range"] and role="slider") expose a non-empty accessible name.',
8
9
  i18n: {
9
10
  titleKey: 'sliderNamePresent_title',
10
11
  descriptionKey: 'sliderNamePresent_description'
11
12
  },
12
13
  helpUrl: null,
13
- tags: ['wcag2a','wcag412','forms','atomic','automatic','name','slider'],
14
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'slider'],
14
15
  wcagSc: ['4.1.2'],
15
16
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
17
+ {
18
+ standard: 'WCAG',
19
+ version: '2.2',
20
+ requirement: '4.1.2',
21
+ title: 'Name, Role, Value',
22
+ conformanceLevel: 'A'
23
+ }
17
24
  ],
18
25
  defaultSeverity: 'serious',
19
26
  category: 'robust',
@@ -24,38 +31,22 @@ const meta = {
24
31
 
25
32
  function runInPage(ctx) {
26
33
  const { document, helpers, rule } = ctx;
27
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
29
- : null;
30
-
34
+ const getEligibilityInfo =
35
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
31
36
 
32
37
  function normalizeWs(s) {
33
- return String(s || '').replace(/\s+/g, ' ').trim();
38
+ return String(s || '')
39
+ .replace(/\s+/g, ' ')
40
+ .trim();
34
41
  }
35
42
 
36
43
  function getAttr(el, name) {
37
44
  try {
38
45
  if (!el || !el.getAttribute) return '';
39
46
  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;
47
+ } catch {
48
+ return '';
49
+ }
59
50
  }
60
51
 
61
52
  function buildLabelForMap(doc) {
@@ -85,7 +76,7 @@ function runInPage(ctx) {
85
76
  const info = helpers.getContentNameInfo(container, ctx);
86
77
  return info && info.present ? info.value : '';
87
78
  }
88
- const t = (container && container.textContent) ? String(container.textContent) : '';
79
+ const t = container && container.textContent ? String(container.textContent) : '';
89
80
  return t.replace(/\s+/g, ' ').trim();
90
81
  }
91
82
 
@@ -132,17 +123,9 @@ function runInPage(ctx) {
132
123
  return '';
133
124
  }
134
125
 
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
126
  function isEligibleAcc(helpers, el, ctx) {
145
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
127
+ const fn =
128
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
146
129
  if (!fn) return true;
147
130
  try {
148
131
  const r = fn(el, ctx);
@@ -153,13 +136,13 @@ function runInPage(ctx) {
153
136
  }
154
137
  }
155
138
 
156
-
157
139
  const occurrences = [];
158
140
  let applicableCount = 0;
159
141
 
160
142
  const selector = 'input[type="range"], [role="slider"]';
161
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
162
-
143
+ const nodes = helpers.queryAllSmart
144
+ ? helpers.queryAllSmart(selector)
145
+ : helpers.queryAll(selector);
163
146
 
164
147
  // Precompute label[for] map for native range inputs.
165
148
  const labelForMap = buildLabelForMap(document);
@@ -229,7 +212,7 @@ function runInPage(ctx) {
229
212
  const type = getAttr(el, 'type').toLowerCase();
230
213
  const role = getAttr(el, 'role').toLowerCase();
231
214
 
232
- let kind = '';
215
+ let kind;
233
216
  if (tag === 'input' && type === 'range') kind = 'native-slider';
234
217
  else if (role === 'slider') kind = 'aria-slider';
235
218
  else continue;
@@ -240,10 +223,16 @@ function runInPage(ctx) {
240
223
  if (res.ok) continue;
241
224
 
242
225
  const eligInfo = getEligibilityInfo
243
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
244
- : null;
226
+ ? (() => {
227
+ try {
228
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
229
+ } catch {
230
+ return null;
231
+ }
232
+ })()
233
+ : null;
245
234
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
246
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
235
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
247
236
 
248
237
  occurrences.push({
249
238
  selector: stableSelector,
@@ -262,12 +251,16 @@ function runInPage(ctx) {
262
251
  });
263
252
  }
264
253
 
265
-
266
254
  if (applicableCount === 0) {
267
255
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
268
256
  }
269
257
  if (occurrences.length) {
270
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
258
+ return {
259
+ ruleId: rule.ruleId,
260
+ outcome: 'fail',
261
+ severity: rule.defaultSeverity || 'minor',
262
+ occurrences
263
+ };
271
264
  }
272
265
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
273
266
  }
@@ -10,10 +10,16 @@ const meta = {
10
10
  descriptionKey: 'spinbuttonNamePresent_description'
11
11
  },
12
12
  helpUrl: null,
13
- tags: ["wcag2a", "wcag412", "forms", "atomic", "automatic", "name", "spinbutton"],
13
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'spinbutton'],
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,20 +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 ''; }
46
+ } catch {
47
+ return '';
48
+ }
41
49
  }
42
50
 
43
51
  function getConservativeSubtreeText(document, container) {
@@ -52,7 +60,7 @@ function runInPage(ctx) {
52
60
  const info = helpers.getContentNameInfo(container, ctx);
53
61
  return info && info.present ? info.value : '';
54
62
  }
55
- const t = (container && container.textContent) ? String(container.textContent) : '';
63
+ const t = container && container.textContent ? String(container.textContent) : '';
56
64
  return t.replace(/\s+/g, ' ').trim();
57
65
  }
58
66
 
@@ -100,7 +108,8 @@ function runInPage(ctx) {
100
108
  }
101
109
 
102
110
  function isEligibleAcc(helpers, el, ctx) {
103
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
111
+ const fn =
112
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
104
113
  if (!fn) return true;
105
114
  try {
106
115
  const r = fn(el, ctx);
@@ -126,12 +135,13 @@ function runInPage(ctx) {
126
135
  return map;
127
136
  }
128
137
 
129
-
130
138
  const occurrences = [];
131
139
  let applicableCount = 0;
132
140
 
133
- const selector = "[role=\"spinbutton\"]";
134
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
141
+ const selector = '[role="spinbutton"]';
142
+ const nodes = helpers.queryAllSmart
143
+ ? helpers.queryAllSmart(selector)
144
+ : helpers.queryAll(selector);
135
145
 
136
146
  // Precompute label[for] map for spinbutton elements that are labelable
137
147
  // native form controls (e.g. <input role="spinbutton">).
@@ -200,16 +210,22 @@ function runInPage(ctx) {
200
210
  if (res.ok) continue;
201
211
 
202
212
  const eligInfo = getEligibilityInfo
203
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
204
- : null;
213
+ ? (() => {
214
+ try {
215
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
216
+ } catch {
217
+ return null;
218
+ }
219
+ })()
220
+ : null;
205
221
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
206
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
222
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
207
223
 
208
224
  occurrences.push({
209
225
  selector: stableSelector,
210
226
  html,
211
227
  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.',
228
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this spinbutton's accessible name.",
213
229
  i18n: {
214
230
  summaryKey: 'spinbuttonNamePresent_summary_fail',
215
231
  hintKey: 'spinbuttonNamePresent_hint_fail',
@@ -222,12 +238,16 @@ function runInPage(ctx) {
222
238
  });
223
239
  }
224
240
 
225
-
226
241
  if (applicableCount === 0) {
227
242
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
228
243
  }
229
244
  if (occurrences.length) {
230
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
245
+ return {
246
+ ruleId: rule.ruleId,
247
+ outcome: 'fail',
248
+ severity: rule.defaultSeverity || 'minor',
249
+ occurrences
250
+ };
231
251
  }
232
252
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
233
253
  }
@@ -13,7 +13,13 @@ const meta = {
13
13
  tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name'],
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,20 +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 ''; }
46
+ } catch {
47
+ return '';
48
+ }
41
49
  }
42
50
 
43
51
  function getConservativeSubtreeText(document, container) {
@@ -52,7 +60,7 @@ function runInPage(ctx) {
52
60
  const info = helpers.getContentNameInfo(container, ctx);
53
61
  return info && info.present ? info.value : '';
54
62
  }
55
- const t = (container && container.textContent) ? String(container.textContent) : '';
63
+ const t = container && container.textContent ? String(container.textContent) : '';
56
64
  return t.replace(/\s+/g, ' ').trim();
57
65
  }
58
66
 
@@ -74,7 +82,8 @@ function runInPage(ctx) {
74
82
  }
75
83
 
76
84
  function isEligibleAcc(helpers, el, ctx) {
77
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
85
+ const fn =
86
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
78
87
  if (!fn) return true;
79
88
  try {
80
89
  const r = fn(el, ctx);
@@ -85,11 +94,12 @@ function runInPage(ctx) {
85
94
  }
86
95
  }
87
96
 
88
-
89
97
  const occurrences = [];
90
98
  let applicableCount = 0;
91
99
 
92
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('summary') : helpers.queryAll('summary');
100
+ const nodes = helpers.queryAllSmart
101
+ ? helpers.queryAllSmart('summary')
102
+ : helpers.queryAll('summary');
93
103
 
94
104
  function evaluate(el) {
95
105
  const ariaLabel = getAttr(el, 'aria-label');
@@ -117,10 +127,16 @@ function runInPage(ctx) {
117
127
  if (res.ok) continue;
118
128
 
119
129
  const eligInfo = getEligibilityInfo
120
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
121
- : null;
130
+ ? (() => {
131
+ try {
132
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
133
+ } catch {
134
+ return null;
135
+ }
136
+ })()
137
+ : null;
122
138
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
123
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
139
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
124
140
 
125
141
  occurrences.push({
126
142
  selector: stableSelector,
@@ -139,14 +155,18 @@ function runInPage(ctx) {
139
155
  });
140
156
  }
141
157
 
142
-
143
158
  if (applicableCount === 0) {
144
159
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
145
160
  }
146
161
  if (occurrences.length) {
147
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
162
+ return {
163
+ ruleId: rule.ruleId,
164
+ outcome: 'fail',
165
+ severity: rule.defaultSeverity || 'minor',
166
+ occurrences
167
+ };
148
168
  }
149
169
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
150
170
  }
151
171
 
152
- module.exports = { id, meta, runInPage };
172
+ module.exports = { id, meta, runInPage };