@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
@@ -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 };