@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
  /**
@@ -33,7 +35,8 @@ const id = 'autocomplete-valid';
33
35
 
34
36
  const meta = {
35
37
  title: 'autocomplete attribute must be a valid autofill value',
36
- description: 'Checks that a non-empty autocomplete attribute is "on"/"off" or a well-formed autofill detail token list.',
38
+ description:
39
+ 'Checks that a non-empty autocomplete attribute is "on"/"off" or a well-formed autofill detail token list.',
37
40
  i18n: {
38
41
  titleKey: 'autocompleteValid_title',
39
42
  descriptionKey: 'autocompleteValid_description'
@@ -42,7 +45,13 @@ const meta = {
42
45
  tags: ['wcag21aa', 'wcag135', 'forms', 'atomic', 'automatic'],
43
46
  wcagSc: ['1.3.5'],
44
47
  normativeMappings: [
45
- { standard: 'WCAG', version: '2.2', requirement: '1.3.5', title: 'Identify Input Purpose', conformanceLevel: 'AA' }
48
+ {
49
+ standard: 'WCAG',
50
+ version: '2.2',
51
+ requirement: '1.3.5',
52
+ title: 'Identify Input Purpose',
53
+ conformanceLevel: 'AA'
54
+ }
46
55
  ],
47
56
  defaultSeverity: 'moderate',
48
57
  category: 'perceivable',
@@ -52,20 +61,63 @@ const meta = {
52
61
  };
53
62
 
54
63
  function runInPage(ctx) {
55
- const { document, helpers, rule } = ctx;
64
+ const { helpers, rule } = ctx;
56
65
 
57
66
  // Declared inside runInPage — see scripts/build-core.js header
58
67
  // ("runInPage MUST be self-contained").
59
68
  const FIELD_NAMES = new Set([
60
- 'name', 'honorific-prefix', 'given-name', 'additional-name', 'family-name', 'honorific-suffix',
61
- 'nickname', 'username', 'new-password', 'current-password', 'one-time-code', 'organization-title',
62
- 'organization', 'street-address', 'address-line1', 'address-line2', 'address-line3',
63
- 'address-level4', 'address-level3', 'address-level2', 'address-level1', 'country', 'country-name',
64
- 'postal-code', 'cc-name', 'cc-given-name', 'cc-additional-name', 'cc-family-name', 'cc-number',
65
- 'cc-exp', 'cc-exp-month', 'cc-exp-year', 'cc-csc', 'cc-type', 'transaction-currency',
66
- 'transaction-amount', 'language', 'bday', 'bday-day', 'bday-month', 'bday-year', 'sex',
67
- 'tel', 'tel-country-code', 'tel-national', 'tel-area-code', 'tel-local', 'tel-extension',
68
- 'email', 'impp', 'url', 'photo'
69
+ 'name',
70
+ 'honorific-prefix',
71
+ 'given-name',
72
+ 'additional-name',
73
+ 'family-name',
74
+ 'honorific-suffix',
75
+ 'nickname',
76
+ 'username',
77
+ 'new-password',
78
+ 'current-password',
79
+ 'one-time-code',
80
+ 'organization-title',
81
+ 'organization',
82
+ 'street-address',
83
+ 'address-line1',
84
+ 'address-line2',
85
+ 'address-line3',
86
+ 'address-level4',
87
+ 'address-level3',
88
+ 'address-level2',
89
+ 'address-level1',
90
+ 'country',
91
+ 'country-name',
92
+ 'postal-code',
93
+ 'cc-name',
94
+ 'cc-given-name',
95
+ 'cc-additional-name',
96
+ 'cc-family-name',
97
+ 'cc-number',
98
+ 'cc-exp',
99
+ 'cc-exp-month',
100
+ 'cc-exp-year',
101
+ 'cc-csc',
102
+ 'cc-type',
103
+ 'transaction-currency',
104
+ 'transaction-amount',
105
+ 'language',
106
+ 'bday',
107
+ 'bday-day',
108
+ 'bday-month',
109
+ 'bday-year',
110
+ 'sex',
111
+ 'tel',
112
+ 'tel-country-code',
113
+ 'tel-national',
114
+ 'tel-area-code',
115
+ 'tel-local',
116
+ 'tel-extension',
117
+ 'email',
118
+ 'impp',
119
+ 'url',
120
+ 'photo'
69
121
  ]);
70
122
  const CONTACT_MODALITY = new Set(['home', 'work', 'mobile', 'fax', 'pager', 'impp']);
71
123
 
@@ -75,7 +127,8 @@ function runInPage(ctx) {
75
127
  if (tokens.length === 1 && (tokens[0] === 'on' || tokens[0] === 'off')) return true;
76
128
 
77
129
  let i = 0;
78
- if (tokens[i] && tokens[i].startsWith('section-') && tokens[i].length > 'section-'.length) i += 1;
130
+ if (tokens[i] && tokens[i].startsWith('section-') && tokens[i].length > 'section-'.length)
131
+ i += 1;
79
132
  if (tokens[i] === 'shipping' || tokens[i] === 'billing') i += 1;
80
133
  if (CONTACT_MODALITY.has(tokens[i])) i += 1;
81
134
 
@@ -87,7 +140,9 @@ function runInPage(ctx) {
87
140
  return FIELD_NAMES.has(remaining[0]);
88
141
  }
89
142
 
90
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('input, select, textarea') : helpers.queryAll('input, select, textarea');
143
+ const nodes = helpers.queryAllSmart
144
+ ? helpers.queryAllSmart('input, select, textarea')
145
+ : helpers.queryAll('input, select, textarea');
91
146
 
92
147
  const occurrences = [];
93
148
  let applicableCount = 0;
@@ -103,7 +158,7 @@ function runInPage(ctx) {
103
158
 
104
159
  const tag = el.tagName.toLowerCase();
105
160
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
106
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
161
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
107
162
 
108
163
  occurrences.push({
109
164
  selector: stableSelector,
@@ -125,7 +180,12 @@ function runInPage(ctx) {
125
180
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
126
181
  }
127
182
  if (occurrences.length) {
128
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
183
+ return {
184
+ ruleId: rule.ruleId,
185
+ outcome: 'fail',
186
+ severity: rule.defaultSeverity || 'moderate',
187
+ occurrences
188
+ };
129
189
  }
130
190
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
131
191
  }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -19,15 +21,15 @@
19
21
  * @implementation-notes
20
22
  * - Static text-based parsing of the style attribute string (regex for
21
23
  * the three property names followed by `!important`), not computed
22
- * style — matches the property list a widely-used reference engine's avoid-inline-spacing
23
- * rule checks.
24
+ * style.
24
25
  */
25
26
 
26
27
  const id = 'avoid-inline-spacing';
27
28
 
28
29
  const meta = {
29
30
  title: 'Inline style must not force text spacing with !important',
30
- description: 'Checks that inline style does not set line-height, letter-spacing, or word-spacing with !important, which blocks user text-spacing overrides.',
31
+ description:
32
+ 'Checks that inline style does not set line-height, letter-spacing, or word-spacing with !important, which blocks user text-spacing overrides.',
31
33
  i18n: {
32
34
  titleKey: 'avoidInlineSpacing_title',
33
35
  descriptionKey: 'avoidInlineSpacing_description'
@@ -36,7 +38,13 @@ const meta = {
36
38
  tags: ['wcag21aa', 'wcag1412', 'structure', 'atomic', 'automatic'],
37
39
  wcagSc: ['1.4.12'],
38
40
  normativeMappings: [
39
- { standard: 'WCAG', version: '2.2', requirement: '1.4.12', title: 'Text Spacing', conformanceLevel: 'AA' }
41
+ {
42
+ standard: 'WCAG',
43
+ version: '2.2',
44
+ requirement: '1.4.12',
45
+ title: 'Text Spacing',
46
+ conformanceLevel: 'AA'
47
+ }
40
48
  ],
41
49
  defaultSeverity: 'moderate',
42
50
  category: 'perceivable',
@@ -46,11 +54,13 @@ const meta = {
46
54
  };
47
55
 
48
56
  function runInPage(ctx) {
49
- const { document, helpers, rule } = ctx;
57
+ const { helpers, rule } = ctx;
50
58
 
51
59
  const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
52
60
 
53
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]') : helpers.queryAll('[style]');
61
+ const nodes = helpers.queryAllSmart
62
+ ? helpers.queryAllSmart('[style]')
63
+ : helpers.queryAll('[style]');
54
64
 
55
65
  const occurrences = [];
56
66
  let applicableCount = 0;
@@ -76,7 +86,7 @@ function runInPage(ctx) {
76
86
 
77
87
  const tag = el.tagName.toLowerCase();
78
88
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
79
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
89
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
80
90
 
81
91
  occurrences.push({
82
92
  selector: stableSelector,
@@ -98,7 +108,12 @@ function runInPage(ctx) {
98
108
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
99
109
  }
100
110
  if (occurrences.length) {
101
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
111
+ return {
112
+ ruleId: rule.ruleId,
113
+ outcome: 'fail',
114
+ severity: rule.defaultSeverity || 'moderate',
115
+ occurrences
116
+ };
102
117
  }
103
118
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
104
119
  }
@@ -1,61 +1,66 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'binary-control-name-present';
4
6
 
5
7
  const meta = {
6
8
  title: 'Binary controls have an accessible name',
7
- description: 'Checks that checkbox, radio, and switch controls expose a non-empty accessible name.',
9
+ description:
10
+ 'Checks that checkbox, radio, and switch controls expose a non-empty accessible name.',
8
11
  i18n: {
9
12
  titleKey: 'binaryControlNamePresent_title',
10
13
  descriptionKey: 'binaryControlNamePresent_description'
11
14
  },
12
15
  helpUrl: null,
13
- tags: ['wcag2a','wcag412','forms','atomic','automatic','name','checkbox','radio','switch'],
16
+ tags: [
17
+ 'wcag2a',
18
+ 'wcag412',
19
+ 'forms',
20
+ 'atomic',
21
+ 'automatic',
22
+ 'name',
23
+ 'checkbox',
24
+ 'radio',
25
+ 'switch'
26
+ ],
14
27
  wcagSc: ['4.1.2'],
15
28
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
29
+ {
30
+ standard: 'WCAG',
31
+ version: '2.2',
32
+ requirement: '4.1.2',
33
+ title: 'Name, Role, Value',
34
+ conformanceLevel: 'A'
35
+ }
17
36
  ],
18
37
  defaultSeverity: 'serious',
19
38
  category: 'robust',
20
39
  type: 'automatic',
21
40
  defaultConfidence: 'high',
22
- coverage: { facetsBySc: { '4.1.2': ['checkbox-name-present','radio-name-present','switch-name-present'] } }
41
+ coverage: {
42
+ facetsBySc: { '4.1.2': ['checkbox-name-present', 'radio-name-present', 'switch-name-present'] }
43
+ }
23
44
  };
24
45
 
25
46
  function runInPage(ctx) {
26
47
  const { document, helpers, rule } = ctx;
27
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
29
- : null;
30
-
48
+ const getEligibilityInfo =
49
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
31
50
 
32
51
  function normalizeWs(s) {
33
- return String(s || '').replace(/\s+/g, ' ').trim();
52
+ return String(s || '')
53
+ .replace(/\s+/g, ' ')
54
+ .trim();
34
55
  }
35
56
 
36
57
  function getAttr(el, name) {
37
58
  try {
38
59
  if (!el || !el.getAttribute) return '';
39
60
  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;
61
+ } catch {
62
+ return '';
63
+ }
59
64
  }
60
65
 
61
66
  function buildLabelForMap(doc) {
@@ -85,7 +90,7 @@ function runInPage(ctx) {
85
90
  const info = helpers.getContentNameInfo(container, ctx);
86
91
  return info && info.present ? info.value : '';
87
92
  }
88
- const t = (container && container.textContent) ? String(container.textContent) : '';
93
+ const t = container && container.textContent ? String(container.textContent) : '';
89
94
  return t.replace(/\s+/g, ' ').trim();
90
95
  }
91
96
 
@@ -132,17 +137,9 @@ function runInPage(ctx) {
132
137
  return '';
133
138
  }
134
139
 
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
140
  function isEligibleAcc(helpers, el, ctx) {
145
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
141
+ const fn =
142
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
146
143
  if (!fn) return true;
147
144
  try {
148
145
  const r = fn(el, ctx);
@@ -153,13 +150,14 @@ function runInPage(ctx) {
153
150
  }
154
151
  }
155
152
 
156
-
157
153
  const occurrences = [];
158
154
  let applicableCount = 0;
159
155
 
160
- const selector = 'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
161
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
162
-
156
+ const selector =
157
+ 'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
158
+ const nodes = helpers.queryAllSmart
159
+ ? helpers.queryAllSmart(selector)
160
+ : helpers.queryAll(selector);
163
161
 
164
162
  // Precompute label[for] associations once for speed/determinism.
165
163
  const labelForMap = buildLabelForMap(document);
@@ -223,7 +221,11 @@ function runInPage(ctx) {
223
221
  }
224
222
 
225
223
  // ARIA roles (checkbox/radio/switch) can use content as name
226
- if (controlType === 'switch' || controlType === 'aria-checkbox' || controlType === 'aria-radio') {
224
+ if (
225
+ controlType === 'switch' ||
226
+ controlType === 'aria-checkbox' ||
227
+ controlType === 'aria-radio'
228
+ ) {
227
229
  const t = getConservativeSubtreeText(document, el);
228
230
  if (t) return { ok: true, method: 'content' };
229
231
  }
@@ -240,7 +242,7 @@ function runInPage(ctx) {
240
242
  const type = getAttr(el, 'type').toLowerCase();
241
243
  const role = getAttr(el, 'role').toLowerCase();
242
244
 
243
- let controlType = '';
245
+ let controlType;
244
246
  if (tag === 'input' && type === 'checkbox') controlType = 'checkbox';
245
247
  else if (tag === 'input' && type === 'radio') controlType = 'radio';
246
248
  else if (role === 'checkbox') controlType = 'aria-checkbox';
@@ -254,10 +256,16 @@ function runInPage(ctx) {
254
256
  if (res.ok) continue;
255
257
 
256
258
  const eligInfo = getEligibilityInfo
257
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
258
- : null;
259
+ ? (() => {
260
+ try {
261
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
262
+ } catch {
263
+ return null;
264
+ }
265
+ })()
266
+ : null;
259
267
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
260
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
268
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
261
269
 
262
270
  occurrences.push({
263
271
  selector: stableSelector,
@@ -280,12 +288,16 @@ function runInPage(ctx) {
280
288
  });
281
289
  }
282
290
 
283
-
284
291
  if (applicableCount === 0) {
285
292
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
286
293
  }
287
294
  if (occurrences.length) {
288
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
295
+ return {
296
+ ruleId: rule.ruleId,
297
+ outcome: 'fail',
298
+ severity: rule.defaultSeverity || 'minor',
299
+ occurrences
300
+ };
289
301
  }
290
302
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
291
303
  }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  // NOTE: Repo ruleId contract requires ENGINE_TAG prefix in the rule id.
@@ -15,7 +17,13 @@ const meta = {
15
17
  tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'buttons', 'name'],
16
18
  wcagSc: ['4.1.2'],
17
19
  normativeMappings: [
18
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
20
+ {
21
+ standard: 'WCAG',
22
+ version: '2.2',
23
+ requirement: '4.1.2',
24
+ title: 'Name, Role, Value',
25
+ conformanceLevel: 'A'
26
+ }
19
27
  ],
20
28
  defaultSeverity: 'serious',
21
29
  category: 'robust',
@@ -25,13 +33,15 @@ const meta = {
25
33
  };
26
34
 
27
35
  function runInPage(ctx) {
28
- const { document, helpers, rule } = ctx;
36
+ const { helpers, rule } = ctx;
29
37
 
30
38
  const occurrences = [];
31
39
  let applicableCount = 0;
32
40
 
33
41
  function normalizeWs(s) {
34
- return String(s || '').replace(/\s+/g, ' ').trim();
42
+ return String(s || '')
43
+ .replace(/\s+/g, ' ')
44
+ .trim();
35
45
  }
36
46
 
37
47
  function getConservativeSubtreeText(container) {
@@ -39,14 +49,13 @@ function runInPage(ctx) {
39
49
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
40
50
  // it has one, not just literal text nodes. See getContentNameInfo's
41
51
  // header comment in src/core/dom-helpers.js for the full rationale
42
- // (this replaced a text-node-only TreeWalker that missed the common
43
- // "<a><img alt='...'></a>" logo-link / "<button><img alt='...'></button>"
44
- // icon-button pattern).
52
+ // (covers the common "<a><img alt='...'></a>" logo-link /
53
+ // "<button><img alt='...'></button>" icon-button pattern).
45
54
  if (helpers.getContentNameInfo) {
46
55
  const info = helpers.getContentNameInfo(container, ctx);
47
56
  return info && info.present ? info.value : '';
48
57
  }
49
- const t = (container && container.textContent) ? String(container.textContent) : '';
58
+ const t = container && container.textContent ? String(container.textContent) : '';
50
59
  return t.replace(/\s+/g, ' ').trim();
51
60
  }
52
61
 
@@ -55,19 +64,23 @@ function runInPage(ctx) {
55
64
  const type = normalizeWs(el.getAttribute ? el.getAttribute('type') : '').toLowerCase();
56
65
  if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
57
66
  const vAttr = el.getAttribute ? el.getAttribute('value') : '';
58
- return normalizeWs(vAttr != null ? vAttr : (typeof el.value === 'string' ? el.value : ''));
67
+ return normalizeWs(vAttr != null ? vAttr : typeof el.value === 'string' ? el.value : '');
59
68
  } catch {
60
69
  return '';
61
70
  }
62
71
  }
63
72
 
64
- const selector = 'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
65
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
73
+ const selector =
74
+ 'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
75
+ const nodes = helpers.queryAllSmart
76
+ ? helpers.queryAllSmart(selector)
77
+ : helpers.queryAll(selector);
66
78
 
67
79
  for (const el of nodes) {
68
80
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
69
81
  const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
70
- const eligible = typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
82
+ const eligible =
83
+ typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
71
84
  if (!eligible) continue;
72
85
 
73
86
  applicableCount += 1;
@@ -81,8 +94,9 @@ function runInPage(ctx) {
81
94
  // getAccessibleNameInfo only resolves programmatic mechanisms (aria-labelledby,
82
95
  // aria-label, native <label> association, title) — it never falls back to
83
96
  // subtree content — so it's safe to trust directly whenever present.
84
- const trustedProgrammaticName =
85
- normalizeWs(nameInfo && nameInfo.present && typeof nameInfo.value === 'string' ? nameInfo.value : '');
97
+ const trustedProgrammaticName = normalizeWs(
98
+ nameInfo && nameInfo.present && typeof nameInfo.value === 'string' ? nameInfo.value : ''
99
+ );
86
100
  const explicitProg = !!trustedProgrammaticName;
87
101
 
88
102
  let inputValueName = '';
@@ -94,15 +108,22 @@ function runInPage(ctx) {
94
108
  // one of these roles is no longer semantically a button — per the WAI-ARIA
95
109
  // Accessible Name and Description Computation spec these roles are
96
110
  // name-from-author-only, and their rendered content represents a VALUE,
97
- // not a NAME (mirrors axe-core's controlValueRoles, verified against its
98
- // source). Found on a real page (Spotify's "sort by" control): a
99
- // <button role="combobox">List</button> where "List" is the combobox's
100
- // currently selected value, not a label for what the combobox is —
101
- // crediting it as the accessible name masked a real missing-name bug.
102
- const VALUE_ROLES = ['textbox', 'progressbar', 'scrollbar', 'slider', 'spinbutton', 'combobox', 'listbox'];
103
- const isContentNameCandidate = (tag === 'button' || role === 'button') && !VALUE_ROLES.includes(roleNorm);
111
+ // not a NAME. E.g. <button role="combobox">List</button> where "List" is
112
+ // the combobox's currently selected value, not a label — crediting it as
113
+ // the accessible name would mask a missing name.
114
+ const VALUE_ROLES = [
115
+ 'textbox',
116
+ 'progressbar',
117
+ 'scrollbar',
118
+ 'slider',
119
+ 'spinbutton',
120
+ 'combobox',
121
+ 'listbox'
122
+ ];
123
+ const isContentNameCandidate =
124
+ (tag === 'button' || role === 'button') && !VALUE_ROLES.includes(roleNorm);
104
125
  const contentName =
105
- (!trustedProgrammaticName && !inputValueName && isContentNameCandidate)
126
+ !trustedProgrammaticName && !inputValueName && isContentNameCandidate
106
127
  ? getConservativeSubtreeText(el)
107
128
  : '';
108
129
 
@@ -112,10 +133,14 @@ function runInPage(ctx) {
112
133
  // Only compute the richer eligibility-info payload (used solely for
113
134
  // the occurrence's visibilityFilter) once we know an occurrence is
114
135
  // actually being built, rather than for every applicable element.
115
- const eligInfo = helpers.getEligibilityInfo ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
136
+ const eligInfo = helpers.getEligibilityInfo
137
+ ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
138
+ : null;
116
139
 
117
140
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
118
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
141
+ const html = helpers.getOuterHtmlSnippet
142
+ ? helpers.getOuterHtmlSnippet(el)
143
+ : el.outerHTML || '';
119
144
 
120
145
  occurrences.push({
121
146
  selector: stableSelector,
@@ -148,7 +173,12 @@ function runInPage(ctx) {
148
173
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
149
174
  }
150
175
  if (occurrences.length) {
151
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
176
+ return {
177
+ ruleId: rule.ruleId,
178
+ outcome: 'fail',
179
+ severity: rule.defaultSeverity || 'minor',
180
+ occurrences
181
+ };
152
182
  }
153
183
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
154
184
  }