@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
@@ -34,126 +34,134 @@
34
34
  const id = 'html-lang-attr-present';
35
35
 
36
36
  const meta = {
37
- title: 'Page language is declared',
38
- description: 'Checks that the default language of the page is programmatically declared.',
39
- i18n: {
40
- titleKey: 'html_lang_attr_title',
41
- descriptionKey: 'html_lang_attr_description'
42
- },
43
- helpUrl: null,
44
- tags: ['wcag2a', 'wcag311', 'structure', 'language', 'automatic', 'atomic'],
45
- wcagSc: ['3.1.1'],
46
- normativeMappings: [
47
- {
48
- standard: 'WCAG',
49
- version: '2.2',
50
- requirement: '3.1.1',
51
- title: 'Language of Page',
52
- conformanceLevel: 'A'
53
- }
54
- ],
55
- defaultSeverity: 'serious',
56
- category: 'understandable',
57
- type: 'automatic',
58
- defaultConfidence: 'high',
59
- coverage: {
60
- facetsBySc: {
61
- '3.1.1': ['html-lang-attr-present']
62
- }
37
+ title: 'Page language is declared',
38
+ description: 'Checks that the default language of the page is programmatically declared.',
39
+ i18n: {
40
+ titleKey: 'html_lang_attr_title',
41
+ descriptionKey: 'html_lang_attr_description'
42
+ },
43
+ helpUrl: null,
44
+ tags: ['wcag2a', 'wcag311', 'structure', 'language', 'automatic', 'atomic'],
45
+ wcagSc: ['3.1.1'],
46
+ normativeMappings: [
47
+ {
48
+ standard: 'WCAG',
49
+ version: '2.2',
50
+ requirement: '3.1.1',
51
+ title: 'Language of Page',
52
+ conformanceLevel: 'A'
63
53
  }
54
+ ],
55
+ defaultSeverity: 'serious',
56
+ category: 'understandable',
57
+ type: 'automatic',
58
+ defaultConfidence: 'high',
59
+ coverage: {
60
+ facetsBySc: {
61
+ '3.1.1': ['html-lang-attr-present']
62
+ }
63
+ }
64
64
  };
65
65
 
66
+ // This check is inherently whole-document (does the PAGE have this
67
+ // property?), not evaluable per-subtree -- notApplicable when contextSelector
68
+ // scoped this run narrower than the whole document, or when
69
+ // engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
70
+ function applicability(ctx) {
71
+ return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
72
+ }
73
+
66
74
  function runInPage(ctx) {
67
- const { document, rule, helpers } = ctx;
68
- const html = document && document.documentElement;
69
-
70
- const tag = (html && html.tagName) ? String(html.tagName).toLowerCase() : '';
71
- if (!html || tag !== 'html') {
72
- return {
73
- ruleId: rule.ruleId,
74
- outcome: 'notApplicable',
75
- severity: 'minor',
76
- occurrences: []
77
- };
78
- }
75
+ const { document, rule, helpers } = ctx;
76
+ const html = document && document.documentElement;
79
77
 
80
- const visibilityFilter = { targetSet: 'acc', accEligible: true, reasons: [] };
81
-
82
- function pushFail(summaryKey, hintKey, params, details) {
83
- const baseOccurrence = {
84
- summary: '',
85
- hint: '',
86
- i18n: {
87
- summaryKey,
88
- hintKey,
89
- params: params && typeof params === 'object' ? params : {}
90
- },
91
- data: {
92
- visibilityFilter,
93
- details: details && typeof details === 'object' ? details : {}
94
- }
95
- };
96
-
97
- if (helpers && typeof helpers.reportOccurrence === 'function') {
98
- return [helpers.reportOccurrence(html, baseOccurrence)];
99
- }
100
-
101
- // Never compute selector/snippet in the rule.
102
- return [{ selector: '', html: '', ...baseOccurrence }];
103
- }
78
+ const tag = html && html.tagName ? String(html.tagName).toLowerCase() : '';
79
+ if (!html || tag !== 'html') {
80
+ return {
81
+ ruleId: rule.ruleId,
82
+ outcome: 'notApplicable',
83
+ severity: 'minor',
84
+ occurrences: []
85
+ };
86
+ }
104
87
 
105
- const rawLang = html.getAttribute('lang'); // null if missing
106
- const lang = (rawLang || '').trim();
107
-
108
- if (rawLang === null) {
109
- return {
110
- ruleId: rule.ruleId,
111
- outcome: 'fail',
112
- severity: rule.defaultSeverity,
113
- occurrences: pushFail(
114
- 'html_lang_attr_missing_absent',
115
- 'html_lang_attr_hint_missing_absent',
116
- {},
117
- { reasonCode: 'lang-missing', location: 'html' }
118
- )
119
- };
120
- }
88
+ const visibilityFilter = { targetSet: 'acc', accEligible: true, reasons: [] };
121
89
 
122
- if (lang.length === 0) {
123
- return {
124
- ruleId: rule.ruleId,
125
- outcome: 'fail',
126
- severity: rule.defaultSeverity,
127
- occurrences: pushFail(
128
- 'html_lang_attr_missing_empty',
129
- 'html_lang_attr_hint_missing_empty',
130
- {},
131
- { reasonCode: 'lang-empty', location: 'html' }
132
- )
133
- };
134
- }
90
+ function pushFail(summaryKey, hintKey, params, details) {
91
+ const baseOccurrence = {
92
+ summary: '',
93
+ hint: '',
94
+ i18n: {
95
+ summaryKey,
96
+ hintKey,
97
+ params: params && typeof params === 'object' ? params : {}
98
+ },
99
+ data: {
100
+ visibilityFilter,
101
+ details: details && typeof details === 'object' ? details : {}
102
+ }
103
+ };
135
104
 
136
- // Minimal BCP47 primary subtag check
137
- if (!/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(lang)) {
138
- return {
139
- ruleId: rule.ruleId,
140
- outcome: 'fail',
141
- severity: rule.defaultSeverity,
142
- occurrences: pushFail(
143
- 'html_lang_attr_invalid',
144
- 'html_lang_attr_hint_invalid',
145
- { lang },
146
- { reasonCode: 'lang-invalid-bcp47', lang }
147
- )
148
- };
105
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
106
+ return [helpers.reportOccurrence(html, baseOccurrence)];
149
107
  }
150
108
 
109
+ // Never compute selector/snippet in the rule.
110
+ return [{ selector: '', html: '', ...baseOccurrence }];
111
+ }
112
+
113
+ const rawLang = html.getAttribute('lang'); // null if missing
114
+ const lang = (rawLang || '').trim();
115
+
116
+ if (rawLang === null) {
117
+ return {
118
+ ruleId: rule.ruleId,
119
+ outcome: 'fail',
120
+ severity: rule.defaultSeverity,
121
+ occurrences: pushFail(
122
+ 'html_lang_attr_missing_absent',
123
+ 'html_lang_attr_hint_missing_absent',
124
+ {},
125
+ { reasonCode: 'lang-missing', location: 'html' }
126
+ )
127
+ };
128
+ }
129
+
130
+ if (lang.length === 0) {
151
131
  return {
152
- ruleId: rule.ruleId,
153
- outcome: 'pass',
154
- severity: 'minor',
155
- occurrences: []
132
+ ruleId: rule.ruleId,
133
+ outcome: 'fail',
134
+ severity: rule.defaultSeverity,
135
+ occurrences: pushFail(
136
+ 'html_lang_attr_missing_empty',
137
+ 'html_lang_attr_hint_missing_empty',
138
+ {},
139
+ { reasonCode: 'lang-empty', location: 'html' }
140
+ )
156
141
  };
142
+ }
143
+
144
+ // Minimal BCP47 primary subtag check
145
+ if (!/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(lang)) {
146
+ return {
147
+ ruleId: rule.ruleId,
148
+ outcome: 'fail',
149
+ severity: rule.defaultSeverity,
150
+ occurrences: pushFail(
151
+ 'html_lang_attr_invalid',
152
+ 'html_lang_attr_hint_invalid',
153
+ { lang },
154
+ { reasonCode: 'lang-invalid-bcp47', lang }
155
+ )
156
+ };
157
+ }
158
+
159
+ return {
160
+ ruleId: rule.ruleId,
161
+ outcome: 'pass',
162
+ severity: 'minor',
163
+ occurrences: []
164
+ };
157
165
  }
158
166
 
159
- module.exports = { id, meta, runInPage };
167
+ module.exports = { id, meta, runInPage, applicability };
@@ -47,7 +47,8 @@
47
47
  const id = 'link-in-text-block';
48
48
 
49
49
  const meta = {
50
- title: 'Links in text blocks must be distinguishable from surrounding text without relying on color alone',
50
+ title:
51
+ 'Links in text blocks must be distinguishable from surrounding text without relying on color alone',
51
52
  description:
52
53
  'Checks that a link inside a run of text is visually distinguishable from the surrounding text by underline, a font-weight/style difference, or a sufficient (>=3:1) color-contrast difference — not by color alone.',
53
54
  i18n: {
@@ -55,10 +56,26 @@ const meta = {
55
56
  descriptionKey: 'linkInTextBlock_description'
56
57
  },
57
58
  helpUrl: null,
58
- tags: ['wcag2a', 'wcag141', 'navigation', 'color', 'links', 'contrast', 'atomic', 'automatic', 'dom'],
59
+ tags: [
60
+ 'wcag2a',
61
+ 'wcag141',
62
+ 'navigation',
63
+ 'color',
64
+ 'links',
65
+ 'contrast',
66
+ 'atomic',
67
+ 'automatic',
68
+ 'dom'
69
+ ],
59
70
  wcagSc: ['1.4.1'],
60
71
  normativeMappings: [
61
- { standard: 'WCAG', version: '2.2', requirement: '1.4.1', title: 'Use of Color', conformanceLevel: 'A' }
72
+ {
73
+ standard: 'WCAG',
74
+ version: '2.2',
75
+ requirement: '1.4.1',
76
+ title: 'Use of Color',
77
+ conformanceLevel: 'A'
78
+ }
62
79
  ],
63
80
  defaultSeverity: 'serious',
64
81
  category: 'perceivable',
@@ -68,7 +85,7 @@ const meta = {
68
85
  };
69
86
 
70
87
  function runInPage(ctx) {
71
- const { document, helpers, rule, engineOptions } = ctx;
88
+ const { helpers, rule, engineOptions } = ctx;
72
89
 
73
90
  function safeComputedStyle(el) {
74
91
  try {
@@ -77,14 +94,16 @@ function runInPage(ctx) {
77
94
  const cs = helpers.computedStyle(el);
78
95
  if (cs) return cs;
79
96
  }
80
- const view = (el.ownerDocument && el.ownerDocument.defaultView) ? el.ownerDocument.defaultView : null;
97
+ const view =
98
+ el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : null;
81
99
  if (view && typeof view.getComputedStyle === 'function') return view.getComputedStyle(el);
82
100
  } catch {}
83
101
  return null;
84
102
  }
85
103
 
86
104
  function decorationTokens(cs) {
87
- const raw = `${(cs && cs.textDecorationLine) || ''} ${(cs && cs.textDecoration) || ''}`.toLowerCase();
105
+ const raw =
106
+ `${(cs && cs.textDecorationLine) || ''} ${(cs && cs.textDecoration) || ''}`.toLowerCase();
88
107
  return raw.split(/\s+/).filter(Boolean);
89
108
  }
90
109
 
@@ -102,9 +121,10 @@ function runInPage(ctx) {
102
121
  return false;
103
122
  }
104
123
 
105
- const contrastOpts = engineOptions && typeof engineOptions.contrast === 'object' && engineOptions.contrast
106
- ? engineOptions.contrast
107
- : {};
124
+ const contrastOpts =
125
+ engineOptions && typeof engineOptions.contrast === 'object' && engineOptions.contrast
126
+ ? engineOptions.contrast
127
+ : {};
108
128
  const mode = contrastOpts.mode === 'auditorAssist' ? 'auditorAssist' : 'strictConformance';
109
129
  const rootCanvasFallback =
110
130
  typeof contrastOpts.rootCanvasFallback === 'string' && contrastOpts.rootCanvasFallback.trim()
@@ -114,7 +134,9 @@ function runInPage(ctx) {
114
134
  const c = helpers && helpers.contrast ? helpers.contrast : null;
115
135
 
116
136
  const selector = 'a[href]';
117
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
137
+ const nodes = helpers.queryAllSmart
138
+ ? helpers.queryAllSmart(selector)
139
+ : helpers.queryAll(selector);
118
140
 
119
141
  const occurrences = [];
120
142
  let applicableCount = 0;
@@ -123,7 +145,8 @@ function runInPage(ctx) {
123
145
  if (!el || !el.getAttribute) continue;
124
146
 
125
147
  const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
126
- const eligible = typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
148
+ const eligible =
149
+ typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
127
150
  if (!eligible) continue;
128
151
 
129
152
  const parent = el.parentElement;
@@ -156,13 +179,22 @@ function runInPage(ctx) {
156
179
  if (blocker && blocker.ok === false) {
157
180
  // Not confidently computable — skip (benefit of the doubt).
158
181
  } else {
159
- const bg = c.computeEffectiveBackground(el, { contrast: { mode, rootCanvasFallback }, collectStack: false });
182
+ const bg = c.computeEffectiveBackground(el, {
183
+ contrast: { mode, rootCanvasFallback },
184
+ collectStack: false
185
+ });
160
186
  const fgLink = c.computeEffectiveForeground(el);
161
187
  const fgParent = c.computeEffectiveForeground(parent);
162
188
 
163
189
  if (bg && bg.ok && bg.rgba && fgLink && fgLink.rgba && fgParent && fgParent.rgba) {
164
- const fgLinkOpaque = fgLink.rgba.a < 1 ? c.compositeRgba(fgLink.rgba, bg.rgba) : { r: fgLink.rgba.r, g: fgLink.rgba.g, b: fgLink.rgba.b, a: 1 };
165
- const fgParentOpaque = fgParent.rgba.a < 1 ? c.compositeRgba(fgParent.rgba, bg.rgba) : { r: fgParent.rgba.r, g: fgParent.rgba.g, b: fgParent.rgba.b, a: 1 };
190
+ const fgLinkOpaque =
191
+ fgLink.rgba.a < 1
192
+ ? c.compositeRgba(fgLink.rgba, bg.rgba)
193
+ : { r: fgLink.rgba.r, g: fgLink.rgba.g, b: fgLink.rgba.b, a: 1 };
194
+ const fgParentOpaque =
195
+ fgParent.rgba.a < 1
196
+ ? c.compositeRgba(fgParent.rgba, bg.rgba)
197
+ : { r: fgParent.rgba.r, g: fgParent.rgba.g, b: fgParent.rgba.b, a: 1 };
166
198
 
167
199
  ratio = c.contrastRatio(fgLinkOpaque, fgParentOpaque);
168
200
  fgLinkHex = c.rgbToHex ? c.rgbToHex(fgLinkOpaque) : '';
@@ -178,16 +210,19 @@ function runInPage(ctx) {
178
210
 
179
211
  if (!flagged) continue;
180
212
 
181
- const eligInfo = helpers.getEligibilityInfo ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
213
+ const eligInfo = helpers.getEligibilityInfo
214
+ ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
215
+ : null;
182
216
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
183
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
217
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
184
218
  const tag = (el.tagName || '').toLowerCase();
185
219
  const ratioStr = c.round2 ? c.round2(ratio) : String(ratio);
186
220
 
187
221
  occurrences.push({
188
222
  selector: stableSelector,
189
223
  html,
190
- summary: 'This link in a block of text relies on color alone to be distinguished from the surrounding text.',
224
+ summary:
225
+ 'This link in a block of text relies on color alone to be distinguished from the surrounding text.',
191
226
  hint: 'Add an underline, a font-weight/style difference, or increase the color contrast between the link and surrounding text to at least 3:1.',
192
227
  i18n: {
193
228
  summaryKey: 'linkInTextBlock_summary_fail',
@@ -209,9 +244,14 @@ function runInPage(ctx) {
209
244
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
210
245
  }
211
246
  if (occurrences.length) {
212
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
247
+ return {
248
+ ruleId: rule.ruleId,
249
+ outcome: 'fail',
250
+ severity: rule.defaultSeverity || 'serious',
251
+ occurrences
252
+ };
213
253
  }
214
254
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
215
255
  }
216
256
 
217
- module.exports = { id, meta, runInPage };
257
+ module.exports = { id, meta, runInPage };
@@ -13,7 +13,13 @@ const meta = {
13
13
  tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'links', '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,7 +30,7 @@ const meta = {
24
30
  };
25
31
 
26
32
  function runInPage(ctx) {
27
- const { document, helpers, rule } = ctx;
33
+ const { helpers, rule } = ctx;
28
34
 
29
35
  const occurrences = [];
30
36
  let applicableCount = 0;
@@ -40,48 +46,68 @@ function runInPage(ctx) {
40
46
  const info = helpers.getContentNameInfo(container, ctx);
41
47
  return info && info.present ? info.value : '';
42
48
  }
43
- const t = (container && container.textContent) ? String(container.textContent) : '';
49
+ const t = container && container.textContent ? String(container.textContent) : '';
44
50
  return t.replace(/\s+/g, ' ').trim();
45
51
  }
46
52
 
47
53
  const selector = 'a[href], area[href], [role="link"]';
48
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
54
+ const nodes = helpers.queryAllSmart
55
+ ? helpers.queryAllSmart(selector)
56
+ : helpers.queryAll(selector);
49
57
 
50
58
  for (const el of nodes) {
51
59
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
52
60
  const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
53
- const eligible = typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
61
+ const eligible =
62
+ typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
54
63
  if (!eligible) continue;
55
64
 
56
65
  applicableCount += 1;
57
66
 
58
67
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
59
- const programmaticName = (nameInfo && typeof nameInfo.value === 'string') ? nameInfo.value : '';
68
+ const programmaticName = nameInfo && typeof nameInfo.value === 'string' ? nameInfo.value : '';
60
69
 
61
70
  // A native <a href>/<area href> (or [role="link"]) whose role has been
62
71
  // overridden to one of these roles is no longer semantically a link —
63
72
  // per the WAI-ARIA Accessible Name and Description Computation spec
64
73
  // these roles are name-from-author-only, and their rendered content
65
- // represents a VALUE, not a NAME (mirrors axe-core's controlValueRoles,
66
- // verified against its source; see button-name-present.js's identical
74
+ // represents a VALUE, not a NAME (see button-name-present.js's identical
67
75
  // fix for a real-world example — <button role="combobox">List</button> —
68
76
  // of the same class of bug on a different host tag).
69
77
  const role = el.getAttribute ? el.getAttribute('role') : null;
70
- const roleNorm = String(role || '').replace(/\s+/g, ' ').trim().toLowerCase();
71
- const VALUE_ROLES = ['textbox', 'progressbar', 'scrollbar', 'slider', 'spinbutton', 'combobox', 'listbox'];
78
+ const roleNorm = String(role || '')
79
+ .replace(/\s+/g, ' ')
80
+ .trim()
81
+ .toLowerCase();
82
+ const VALUE_ROLES = [
83
+ 'textbox',
84
+ 'progressbar',
85
+ 'scrollbar',
86
+ 'slider',
87
+ 'spinbutton',
88
+ 'combobox',
89
+ 'listbox'
90
+ ];
72
91
  const isContentNameCandidate = !VALUE_ROLES.includes(roleNorm);
73
92
 
74
- const contentName = (programmaticName.trim().length === 0 && isContentNameCandidate) ? getConservativeSubtreeText(el) : '';
93
+ const contentName =
94
+ programmaticName.trim().length === 0 && isContentNameCandidate
95
+ ? getConservativeSubtreeText(el)
96
+ : '';
75
97
  const finalName = (programmaticName.trim().length ? programmaticName : contentName).trim();
76
98
 
77
99
  if (finalName.length === 0) {
78
100
  // Only compute the richer eligibility-info payload (used solely for
79
101
  // the occurrence's visibilityFilter) once we know an occurrence is
80
102
  // actually being built, rather than for every applicable element.
81
- const eligInfo = helpers.getEligibilityInfo ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
103
+ const eligInfo = helpers.getEligibilityInfo
104
+ ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
105
+ : null;
82
106
 
83
107
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
84
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
108
+ const html = helpers.getOuterHtmlSnippet
109
+ ? helpers.getOuterHtmlSnippet(el)
110
+ : el.outerHTML || '';
85
111
  const tag = (el.tagName || '').toLowerCase();
86
112
 
87
113
  occurrences.push({
@@ -118,7 +144,12 @@ function runInPage(ctx) {
118
144
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
119
145
  }
120
146
  if (occurrences.length) {
121
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
147
+ return {
148
+ ruleId: rule.ruleId,
149
+ outcome: 'fail',
150
+ severity: rule.defaultSeverity || 'minor',
151
+ occurrences
152
+ };
122
153
  }
123
154
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
124
155
  }
@@ -52,7 +52,8 @@ const id = 'list-children-valid';
52
52
 
53
53
  const meta = {
54
54
  title: 'Lists must only directly contain list items',
55
- description: 'Checks that <ul>/<ol> elements only have <li>, <script>, or <template> as direct children.',
55
+ description:
56
+ 'Checks that <ul>/<ol> elements only have <li>, <script>, or <template> as direct children.',
56
57
  i18n: {
57
58
  titleKey: 'listChildrenValid_title',
58
59
  descriptionKey: 'listChildrenValid_description'
@@ -61,7 +62,13 @@ const meta = {
61
62
  tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
62
63
  wcagSc: ['1.3.1'],
63
64
  normativeMappings: [
64
- { standard: 'WCAG', version: '2.2', requirement: '1.3.1', title: 'Info and Relationships', conformanceLevel: 'A' }
65
+ {
66
+ standard: 'WCAG',
67
+ version: '2.2',
68
+ requirement: '1.3.1',
69
+ title: 'Info and Relationships',
70
+ conformanceLevel: 'A'
71
+ }
65
72
  ],
66
73
  defaultSeverity: 'serious',
67
74
  category: 'perceivable',
@@ -71,15 +78,18 @@ const meta = {
71
78
  };
72
79
 
73
80
  function runInPage(ctx) {
74
- const { document, helpers, rule } = ctx;
81
+ const { helpers, rule } = ctx;
75
82
 
76
83
  // Declared inside runInPage — see scripts/build-core.js header
77
84
  // ("runInPage MUST be self-contained").
78
85
  const ALLOWED_CHILD_TAGS = new Set(['li', 'script', 'template']);
79
86
 
80
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('ul, ol') : helpers.queryAll('ul, ol');
87
+ const nodes = helpers.queryAllSmart
88
+ ? helpers.queryAllSmart('ul, ol')
89
+ : helpers.queryAll('ul, ol');
81
90
 
82
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
91
+ const isAccTreeEligible =
92
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
83
93
 
84
94
  function isExposedToAt(el) {
85
95
  if (!isAccTreeEligible) return true;
@@ -119,9 +129,11 @@ function runInPage(ctx) {
119
129
 
120
130
  if (!invalidTags.length) continue;
121
131
 
132
+ const dedupedInvalidTags = [...new Set(invalidTags)];
133
+
122
134
  const tag = el.tagName.toLowerCase();
123
135
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
124
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
136
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
125
137
 
126
138
  occurrences.push({
127
139
  selector: stableSelector,
@@ -131,7 +143,7 @@ function runInPage(ctx) {
131
143
  i18n: {
132
144
  summaryKey: 'listChildrenValid_summary_fail',
133
145
  hintKey: 'listChildrenValid_hint_fail',
134
- params: { element: tag, invalidChildren: invalidTags.join(', ') }
146
+ params: { element: tag, invalidChildren: dedupedInvalidTags.join(', ') }
135
147
  },
136
148
  data: {
137
149
  details: { reasonCode: 'LIST_INVALID_CHILD', element: tag, invalidChildren: invalidTags }
@@ -143,9 +155,14 @@ function runInPage(ctx) {
143
155
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
144
156
  }
145
157
  if (occurrences.length) {
146
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
158
+ return {
159
+ ruleId: rule.ruleId,
160
+ outcome: 'fail',
161
+ severity: rule.defaultSeverity || 'serious',
162
+ occurrences
163
+ };
147
164
  }
148
165
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
149
166
  }
150
167
 
151
- module.exports = { id, meta, runInPage };
168
+ module.exports = { id, meta, runInPage };