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