@surea11y/core 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +37 -7
  2. package/LICENSE +373 -21
  3. package/README.md +67 -1
  4. package/bin/core.js +144 -19
  5. package/docs/API_STABILITY.md +1 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/CLI.md +54 -1
  9. package/docs/ENGINE_OPTIONS.md +2 -0
  10. package/docs/INTEGRATION.md +18 -0
  11. package/docs/OUTPUT_SCHEMA.md +1 -1
  12. package/docs/RULE_CATALOG.md +1 -1
  13. package/docs/SARIF.md +59 -0
  14. package/package.json +15 -4
  15. package/src/baseline.js +0 -0
  16. package/src/catalogs/composites.wcag.js +414 -450
  17. package/src/checks/automatic/area-alt-present.js +59 -25
  18. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  19. package/src/checks/automatic/aria-allowed-role.js +21 -7
  20. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  21. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  22. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  23. package/src/checks/automatic/aria-hidden-body.js +42 -19
  24. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  25. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  26. package/src/checks/automatic/aria-prohibited-children.js +55 -16
  27. package/src/checks/automatic/aria-required-attr.js +23 -8
  28. package/src/checks/automatic/aria-required-children.js +37 -14
  29. package/src/checks/automatic/aria-required-parent.js +48 -14
  30. package/src/checks/automatic/aria-role-name-present.js +47 -21
  31. package/src/checks/automatic/aria-roles-valid.js +22 -12
  32. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  33. package/src/checks/automatic/aria-valid-attr.js +17 -5
  34. package/src/checks/automatic/autocomplete-valid.js +74 -16
  35. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  36. package/src/checks/automatic/binary-control-name-present.js +60 -50
  37. package/src/checks/automatic/button-name-present.js +48 -18
  38. package/src/checks/automatic/bypass-blocks-present.js +42 -25
  39. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  40. package/src/checks/automatic/combobox-name-present.js +38 -45
  41. package/src/checks/automatic/contrast-computable.js +361 -341
  42. package/src/checks/automatic/contrast-enhanced.js +487 -466
  43. package/src/checks/automatic/contrast-minimum.js +486 -465
  44. package/src/checks/automatic/css-orientation-lock.js +30 -8
  45. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  46. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  47. package/src/checks/automatic/dialog-name-present.js +37 -75
  48. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  49. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  50. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  51. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  52. package/src/checks/automatic/form-control-single-label.js +47 -10
  53. package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
  54. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  55. package/src/checks/automatic/iframe-name-present.js +31 -9
  56. package/src/checks/automatic/iframe-title-unique.js +29 -8
  57. package/src/checks/automatic/img-alt-present.js +47 -43
  58. package/src/checks/automatic/input-image-alt-present.js +143 -112
  59. package/src/checks/automatic/label-in-name.js +50 -22
  60. package/src/checks/automatic/language-page-present.js +109 -109
  61. package/src/checks/automatic/link-in-text-block.js +59 -19
  62. package/src/checks/automatic/link-name-present.js +45 -14
  63. package/src/checks/automatic/list-children-valid.js +26 -9
  64. package/src/checks/automatic/listbox-name-present.js +39 -19
  65. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  66. package/src/checks/automatic/menuitem-name-present.js +39 -61
  67. package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
  68. package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
  69. package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
  70. package/src/checks/automatic/meter-name-present.js +36 -33
  71. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  72. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  73. package/src/checks/automatic/option-name-present.js +38 -21
  74. package/src/checks/automatic/page-title-present.js +17 -6
  75. package/src/checks/automatic/progressbar-name-present.js +41 -34
  76. package/src/checks/automatic/role-img-alt-present.js +209 -157
  77. package/src/checks/automatic/searchbox-name-present.js +39 -19
  78. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  79. package/src/checks/automatic/slider-name-present.js +40 -47
  80. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  81. package/src/checks/automatic/summary-name-present.js +37 -17
  82. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  83. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  84. package/src/checks/automatic/tab-name-present.js +37 -60
  85. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  86. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  87. package/src/checks/automatic/target-size-minimum.js +118 -48
  88. package/src/checks/automatic/td-has-header.js +29 -11
  89. package/src/checks/automatic/textbox-name-present.js +39 -19
  90. package/src/checks/automatic/tooltip-name-present.js +37 -18
  91. package/src/checks/automatic/treeitem-name-present.js +38 -21
  92. package/src/checks/automatic/valid-lang.js +20 -6
  93. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  94. package/src/checks/manual/accesskeys-manual.js +14 -5
  95. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  96. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  97. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  98. package/src/checks/manual/aria-text-manual.js +14 -6
  99. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  100. package/src/checks/manual/css-hidden-focus.js +196 -165
  101. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  102. package/src/checks/manual/empty-heading-manual.js +24 -7
  103. package/src/checks/manual/empty-table-header-manual.js +17 -6
  104. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  105. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  106. package/src/checks/manual/heading-order-manual.js +22 -7
  107. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  108. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  109. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  110. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  111. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  112. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  113. package/src/checks/manual/label-title-only-manual.js +14 -5
  114. package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
  115. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
  116. package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
  117. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
  118. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
  119. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  120. package/src/checks/manual/landmark-one-main-manual.js +26 -20
  121. package/src/checks/manual/landmark-unique-manual.js +41 -15
  122. package/src/checks/manual/link-name-quality-manual.js +43 -12
  123. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  124. package/src/checks/manual/meta-viewport-large-manual.js +16 -5
  125. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  126. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  127. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  128. package/src/checks/manual/p-as-heading-manual.js +22 -7
  129. package/src/checks/manual/page-has-heading-one-manual.js +31 -22
  130. package/src/checks/manual/page-title-patterns-manual.js +78 -50
  131. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  132. package/src/checks/manual/region-manual.js +241 -48
  133. package/src/checks/manual/scope-attr-valid-manual.js +10 -3
  134. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  135. package/src/checks/manual/skip-link-manual.js +35 -12
  136. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  137. package/src/checks/manual/tabindex-manual.js +10 -3
  138. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  139. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  140. package/src/checks/manual/video-caption-manual.js +15 -4
  141. package/src/checks/manual-review.js +56 -12
  142. package/src/core/aria-helpers.js +1127 -886
  143. package/src/core/contrast-helpers.js +1217 -1062
  144. package/src/core/dom-helpers.js +4175 -3917
  145. package/src/core/dom-runner.js +720 -604
  146. package/src/core/frame-messaging.js +189 -138
  147. package/src/core/frame-scan.js +94 -82
  148. package/src/core/rollup-composites.js +94 -102
  149. package/src/core/rule-meta.js +58 -41
  150. package/src/core.js +36552 -29111
  151. package/src/i18n/en.js +1194 -889
  152. package/src/i18n/fr.js +1136 -795
  153. package/src/policy/contracts.js +13 -13
  154. package/src/policy/resolvePolicy.js +48 -44
  155. package/src/report.js +63 -43
  156. package/src/sarif.js +175 -0
  157. package/surea11y.browser.js +36042 -0
@@ -78,7 +78,7 @@ function runInPage(ctx) {
78
78
  }
79
79
 
80
80
  function getExplicitRoleToken(el) {
81
- const raw = (el.getAttribute && el.getAttribute('role') || '').trim();
81
+ const raw = ((el.getAttribute && el.getAttribute('role')) || '').trim();
82
82
  if (!raw) return '';
83
83
  return raw.split(/\s+/)[0].toLowerCase();
84
84
  }
@@ -91,7 +91,8 @@ function runInPage(ctx) {
91
91
  return tag === 'main';
92
92
  }
93
93
 
94
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
94
+ const isAccTreeEligible =
95
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
95
96
 
96
97
  function isExposedToAt(el) {
97
98
  if (!isAccTreeEligible) return true;
@@ -108,11 +109,12 @@ function runInPage(ctx) {
108
109
  // landmark-unique-manual.js's header comment for the real page (Airtable, 2026-07-23)
109
110
  // that surfaced this gap: a third-party shadow-DOM-hosted widget's own landmark is
110
111
  // invisible to a light-DOM-only query.
111
- let nodes = [];
112
+ let nodes;
112
113
  try {
113
- nodes = helpers && typeof helpers.queryAllSmart === 'function'
114
- ? helpers.queryAllSmart('main, [role]')
115
- : document.querySelectorAll('main, [role]');
114
+ nodes =
115
+ helpers && typeof helpers.queryAllSmart === 'function'
116
+ ? helpers.queryAllSmart('main, [role]')
117
+ : document.querySelectorAll('main, [role]');
116
118
  } catch {
117
119
  nodes = [];
118
120
  }
@@ -133,26 +135,30 @@ function runInPage(ctx) {
133
135
  }
134
136
 
135
137
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
136
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(body) : (body.outerHTML || '').slice(0, 200);
138
+ const html = helpers.getOuterHtmlSnippet
139
+ ? helpers.getOuterHtmlSnippet(body)
140
+ : (body.outerHTML || '').slice(0, 200);
137
141
 
138
142
  return {
139
143
  ruleId: rule.ruleId,
140
144
  outcome: 'cantTell',
141
145
  severity: rule.defaultSeverity || 'minor',
142
- occurrences: [{
143
- selector: stableSelector,
144
- html,
145
- summary: 'This page has no main landmark.',
146
- hint: 'Add a main landmark (<main> or role="main") around the page\'s primary content.',
147
- i18n: {
148
- summaryKey: 'landmarkOneMain_summary_cantTell_missing',
149
- hintKey: 'landmarkOneMain_hint_cantTell_missing',
150
- params: {}
151
- },
152
- data: {
153
- details: { reasonCode: 'LANDMARK_MAIN_MISSING' }
146
+ occurrences: [
147
+ {
148
+ selector: stableSelector,
149
+ html,
150
+ summary: 'This page has no main landmark.',
151
+ hint: 'Add a main landmark (<main> or role="main") around the page\'s primary content.',
152
+ i18n: {
153
+ summaryKey: 'landmarkOneMain_summary_cantTell_missing',
154
+ hintKey: 'landmarkOneMain_hint_cantTell_missing',
155
+ params: {}
156
+ },
157
+ data: {
158
+ details: { reasonCode: 'LANDMARK_MAIN_MISSING' }
159
+ }
154
160
  }
155
- }]
161
+ ]
156
162
  };
157
163
  }
158
164
 
@@ -31,7 +31,8 @@ const id = 'landmark-unique';
31
31
 
32
32
  const meta = {
33
33
  title: 'Landmarks with the same role must have unique names',
34
- description: 'Checks that when two or more landmarks share the same role, each has a distinct accessible name.',
34
+ description:
35
+ 'Checks that when two or more landmarks share the same role, each has a distinct accessible name.',
35
36
  i18n: {
36
37
  titleKey: 'landmarkUnique_title',
37
38
  descriptionKey: 'landmarkUnique_description'
@@ -51,7 +52,9 @@ function runInPage(ctx) {
51
52
  const { document, helpers, rule } = ctx;
52
53
 
53
54
  function normalizeWs(s) {
54
- return String(s || '').replace(/\s+/g, ' ').trim();
55
+ return String(s || '')
56
+ .replace(/\s+/g, ' ')
57
+ .trim();
55
58
  }
56
59
 
57
60
  // Delegates to the shared helpers.getLandmarkNameInfo (aria-label -> aria-labelledby, via the
@@ -124,7 +127,16 @@ function runInPage(ctx) {
124
127
  return '';
125
128
  }
126
129
 
127
- const LANDMARK_ROLES = new Set(['banner', 'contentinfo', 'main', 'navigation', 'complementary', 'region', 'form', 'search']);
130
+ const LANDMARK_ROLES = new Set([
131
+ 'banner',
132
+ 'contentinfo',
133
+ 'main',
134
+ 'navigation',
135
+ 'complementary',
136
+ 'region',
137
+ 'form',
138
+ 'search'
139
+ ]);
128
140
 
129
141
  function getLandmarkRole(el) {
130
142
  if (!el || !el.getAttribute) return '';
@@ -152,7 +164,8 @@ function runInPage(ctx) {
152
164
  return getImplicitLandmarkRole(el);
153
165
  }
154
166
 
155
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
167
+ const isAccTreeEligible =
168
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
156
169
 
157
170
  function isExposedToAt(el) {
158
171
  if (!isAccTreeEligible) return true;
@@ -172,11 +185,12 @@ function runInPage(ctx) {
172
185
  // own landmark-unique (a real browser DOM, shadow roots included by design) correctly sees as colliding
173
186
  // with the page's own unnamed header <nav>/page <footer> -- a real, confirmed surea11y
174
187
  // false-negative miss, invisible to plain querySelectorAll's light-DOM-only reach.
175
- let nodes = [];
188
+ let nodes;
176
189
  try {
177
- nodes = helpers && typeof helpers.queryAllSmart === 'function'
178
- ? helpers.queryAllSmart('header, footer, main, nav, aside, section, form, [role]')
179
- : document.querySelectorAll('header, footer, main, nav, aside, section, form, [role]');
190
+ nodes =
191
+ helpers && typeof helpers.queryAllSmart === 'function'
192
+ ? helpers.queryAllSmart('header, footer, main, nav, aside, section, form, [role]')
193
+ : document.querySelectorAll('header, footer, main, nav, aside, section, form, [role]');
180
194
  } catch {
181
195
  nodes = [];
182
196
  }
@@ -221,24 +235,31 @@ function runInPage(ctx) {
221
235
 
222
236
  for (const { el } of group) {
223
237
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
224
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
238
+ const html = helpers.getOuterHtmlSnippet
239
+ ? helpers.getOuterHtmlSnippet(el)
240
+ : el.outerHTML || '';
225
241
 
226
242
  occurrences.push({
227
243
  selector: stableSelector,
228
244
  html,
229
245
  summary: normalizedName
230
- ? `This ${role} landmark shares its accessible name with another ${role} landmark.`
231
- : `This ${role} landmark has no accessible name, and more than one unnamed ${role} landmark exists on this page.`,
246
+ ? `This ${role} landmark shares its accessible name with another ${role} landmark.`
247
+ : `This ${role} landmark has no accessible name, and more than one unnamed ${role} landmark exists on this page.`,
232
248
  hint: `Give each ${role} landmark a distinct name via aria-label or aria-labelledby.`,
233
249
  i18n: {
234
250
  summaryKey: normalizedName
235
- ? 'landmarkUnique_summary_cantTell_duplicateName'
236
- : 'landmarkUnique_summary_cantTell_bothUnnamed',
251
+ ? 'landmarkUnique_summary_cantTell_duplicateName'
252
+ : 'landmarkUnique_summary_cantTell_bothUnnamed',
237
253
  hintKey: 'landmarkUnique_hint_cantTell',
238
254
  params: { role }
239
255
  },
240
256
  data: {
241
- details: { reasonCode: 'LANDMARK_NOT_UNIQUE', role, name: normalizedName, groupSize: group.length }
257
+ details: {
258
+ reasonCode: 'LANDMARK_NOT_UNIQUE',
259
+ role,
260
+ name: normalizedName,
261
+ groupSize: group.length
262
+ }
242
263
  }
243
264
  });
244
265
  }
@@ -248,7 +269,12 @@ function runInPage(ctx) {
248
269
  if (!occurrences.length) {
249
270
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
250
271
  }
251
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
272
+ return {
273
+ ruleId: rule.ruleId,
274
+ outcome: 'cantTell',
275
+ severity: rule.defaultSeverity || 'minor',
276
+ occurrences
277
+ };
252
278
  }
253
279
 
254
280
  module.exports = { id, meta, runInPage };
@@ -50,7 +50,13 @@ const meta = {
50
50
  tags: ['wcag2a', 'wcag244', 'navigation', 'quality', 'atomic', 'manual'],
51
51
  wcagSc: ['2.4.4'],
52
52
  normativeMappings: [
53
- { standard: 'WCAG', version: '2.2', requirement: '2.4.4', title: 'Link Purpose (In Context)', conformanceLevel: 'A' }
53
+ {
54
+ standard: 'WCAG',
55
+ version: '2.2',
56
+ requirement: '2.4.4',
57
+ title: 'Link Purpose (In Context)',
58
+ conformanceLevel: 'A'
59
+ }
54
60
  ],
55
61
  defaultSeverity: 'minor',
56
62
  category: 'operable',
@@ -60,13 +66,28 @@ const meta = {
60
66
  };
61
67
 
62
68
  function runInPage(ctx) {
63
- const { document, helpers, rule } = ctx;
69
+ const { helpers, rule } = ctx;
64
70
 
65
71
  const GENERIC_LINK_TEXT = new Set([
66
- 'click here', 'here', 'click', 'more', 'more info', 'more information',
67
- 'read more', 'learn more', 'continue reading', 'continue', 'details',
68
- 'more details', 'link', 'this link', 'go', 'download', 'view more',
69
- 'see more', 'info'
72
+ 'click here',
73
+ 'here',
74
+ 'click',
75
+ 'more',
76
+ 'more info',
77
+ 'more information',
78
+ 'read more',
79
+ 'learn more',
80
+ 'continue reading',
81
+ 'continue',
82
+ 'details',
83
+ 'more details',
84
+ 'link',
85
+ 'this link',
86
+ 'go',
87
+ 'download',
88
+ 'view more',
89
+ 'see more',
90
+ 'info'
70
91
  ]);
71
92
 
72
93
  function normalize(s) {
@@ -79,7 +100,9 @@ function runInPage(ctx) {
79
100
  }
80
101
 
81
102
  const selector = 'a[href]';
82
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
103
+ const nodes = helpers.queryAllSmart
104
+ ? helpers.queryAllSmart(selector)
105
+ : helpers.queryAll(selector);
83
106
 
84
107
  const occurrences = [];
85
108
  let applicableCount = 0;
@@ -88,11 +111,12 @@ function runInPage(ctx) {
88
111
  if (!el || !el.getAttribute) continue;
89
112
 
90
113
  const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
91
- const eligible = typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
114
+ const eligible =
115
+ typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
92
116
  if (!eligible) continue;
93
117
 
94
118
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
95
- const programmaticName = (nameInfo && typeof nameInfo.value === 'string') ? nameInfo.value : '';
119
+ const programmaticName = nameInfo && typeof nameInfo.value === 'string' ? nameInfo.value : '';
96
120
 
97
121
  let rawName = programmaticName;
98
122
  if (!rawName.trim() && helpers.getContentNameInfo) {
@@ -108,8 +132,10 @@ function runInPage(ctx) {
108
132
  if (!GENERIC_LINK_TEXT.has(normalized)) continue;
109
133
 
110
134
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
111
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
112
- const eligInfo = helpers.getEligibilityInfo ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
135
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
136
+ const eligInfo = helpers.getEligibilityInfo
137
+ ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
138
+ : null;
113
139
 
114
140
  occurrences.push({
115
141
  selector: stableSelector,
@@ -133,7 +159,12 @@ function runInPage(ctx) {
133
159
  }
134
160
 
135
161
  if (occurrences.length) {
136
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
162
+ return {
163
+ ruleId: rule.ruleId,
164
+ outcome: 'cantTell',
165
+ severity: rule.defaultSeverity || 'minor',
166
+ occurrences
167
+ };
137
168
  }
138
169
 
139
170
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
@@ -99,20 +99,23 @@ function runInPage(ctx) {
99
99
  const __eligCache = new WeakMap();
100
100
 
101
101
  function getEligibility(node) {
102
- if (!node || node.nodeType !== 1) return { eligible: true, reasons: [], targetSet: 'acc', accEligible: null };
102
+ if (!node || node.nodeType !== 1)
103
+ return { eligible: true, reasons: [], targetSet: 'acc', accEligible: null };
103
104
  const cached = __eligCache.get(node);
104
105
  if (cached) return cached;
105
106
 
106
- let info = null;
107
+ let info;
107
108
  try {
108
- info = (helpers && typeof helpers.getEligibilityInfo === 'function')
109
+ info =
110
+ helpers && typeof helpers.getEligibilityInfo === 'function'
109
111
  ? helpers.getEligibilityInfo(node, ctx, { targetSet: 'acc' })
110
112
  : null;
111
113
  } catch {
112
114
  info = null;
113
115
  }
114
116
 
115
- const norm = (info && typeof info === 'object')
117
+ const norm =
118
+ info && typeof info === 'object'
116
119
  ? info
117
120
  : { eligible: true, reasons: [], targetSet: 'acc', accEligible: null };
118
121
 
@@ -125,10 +128,6 @@ function runInPage(ctx) {
125
128
  return !!(info && info.eligible);
126
129
  }
127
130
 
128
- function safeSelector(node) {
129
- return helpers.buildSelector ? helpers.buildSelector(node) : 'html';
130
- }
131
-
132
131
  // Evidence object includes strength, so the rule can treat "unverified external link" as insufficient proof.
133
132
  function evidenceNone() {
134
133
  return {
@@ -173,7 +172,9 @@ function runInPage(ctx) {
173
172
  if (hasToken || len >= MIN_DESCRIBEDBY_CHARS_WITHOUT_TOKEN) {
174
173
  evidence.strength = 'strong';
175
174
  evidence.method = 'aria-describedby';
176
- evidence.notes.push('media has aria-describedby with explicit or substantial transcript text');
175
+ evidence.notes.push(
176
+ 'media has aria-describedby with explicit or substantial transcript text'
177
+ );
177
178
  return evidence;
178
179
  }
179
180
  }
@@ -197,7 +198,9 @@ function runInPage(ctx) {
197
198
  evidence.strength = 'strong';
198
199
  evidence.method = 'adjacent-heading';
199
200
  evidence.transcriptNodeSelector = nodeRef(h);
200
- evidence.notes.push('found transcript heading with substantial visible adjacent text');
201
+ evidence.notes.push(
202
+ 'found transcript heading with substantial visible adjacent text'
203
+ );
201
204
  return evidence;
202
205
  }
203
206
  }
@@ -215,7 +218,9 @@ function runInPage(ctx) {
215
218
  for (const a of links) {
216
219
  if (!isEligible(a)) continue;
217
220
 
218
- const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(a, ctx) : null;
221
+ const nameInfo = helpers.getAccessibleNameInfo
222
+ ? helpers.getAccessibleNameInfo(a, ctx)
223
+ : null;
219
224
  const linkName = nameInfo && nameInfo.value ? nameInfo.value : getNodeText(a);
220
225
  if (!containsTranscriptToken(linkName)) continue;
221
226
 
@@ -227,7 +232,9 @@ function runInPage(ctx) {
227
232
  if (href.startsWith('#')) {
228
233
  const targetId = href.slice(1);
229
234
  const target = targetId
230
- ? (safeRoot.getElementById ? safeRoot.getElementById(targetId) : document.getElementById(targetId))
235
+ ? safeRoot.getElementById
236
+ ? safeRoot.getElementById(targetId)
237
+ : document.getElementById(targetId)
231
238
  : null;
232
239
 
233
240
  if (isElement(target) && isEligible(target)) {
@@ -246,7 +253,9 @@ function runInPage(ctx) {
246
253
  if (hasTranscriptHeading && textLen(targetText) >= MIN_TRANSCRIPT_CHARS) {
247
254
  evidence.strength = 'strong';
248
255
  evidence.method = 'anchor-target';
249
- evidence.notes.push('resolved transcript link to an on-page section with transcript heading and substantial text');
256
+ evidence.notes.push(
257
+ 'resolved transcript link to an on-page section with transcript heading and substantial text'
258
+ );
250
259
  return evidence;
251
260
  }
252
261
  }
@@ -254,7 +263,9 @@ function runInPage(ctx) {
254
263
  // If anchor cannot be verified, treat as weak (still better than nothing, but not proof).
255
264
  evidence.strength = 'weak';
256
265
  evidence.method = 'anchor-unverified';
257
- evidence.notes.push('transcript link found but anchor target could not be verified as a transcript section');
266
+ evidence.notes.push(
267
+ 'transcript link found but anchor target could not be verified as a transcript section'
268
+ );
258
269
  return evidence;
259
270
  }
260
271
 
@@ -300,9 +311,8 @@ function runInPage(ctx) {
300
311
  if (evidence.strength === 'none') {
301
312
  const baseOccurrence = {
302
313
  summary:
303
- 'A transcript or other text alternative for this time-based media is not strongly evidenced on the page.',
304
- hint:
305
- 'Provide a clearly identified transcript or other text alternative for audio-only/video-only prerecorded media (for example, a “Transcript” section or link).',
314
+ 'A transcript or other text alternative for this time-based media is not strongly evidenced on the page.',
315
+ hint: 'Provide a clearly identified transcript or other text alternative for audio-only/video-only prerecorded media (for example, a “Transcript” section or link).',
306
316
  i18n: {
307
317
  summaryKey: 'mediaTranscriptPresent_summary_cantTell_missing',
308
318
  hintKey: 'mediaTranscriptPresent_hint_cantTell_missing',
@@ -327,12 +337,10 @@ function runInPage(ctx) {
327
337
  }
328
338
 
329
339
  if (evidence.strength === 'weak') {
330
-
331
340
  const baseOccurrence = {
332
341
  summary:
333
- 'A transcript or other text alternative may be available for this time-based media, but it could not be verified from the page content.',
334
- hint:
335
- 'Ensure a clearly identified transcript or other text alternative is available and programmatically or visibly associated with the media on the page.',
342
+ 'A transcript or other text alternative may be available for this time-based media, but it could not be verified from the page content.',
343
+ hint: 'Ensure a clearly identified transcript or other text alternative is available and programmatically or visibly associated with the media on the page.',
336
344
  i18n: {
337
345
  summaryKey: 'mediaTranscriptPresent_summary_cantTell_unverified',
338
346
  hintKey: 'mediaTranscriptPresent_hint_cantTell_unverified',
@@ -361,7 +369,12 @@ function runInPage(ctx) {
361
369
  }
362
370
 
363
371
  if (occurrences.length) {
364
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
372
+ return {
373
+ ruleId: rule.ruleId,
374
+ outcome: 'cantTell',
375
+ severity: rule.defaultSeverity || 'minor',
376
+ occurrences
377
+ };
365
378
  }
366
379
 
367
380
  // Manual rules may only emit cantTell/notApplicable (never pass/fail).
@@ -27,7 +27,8 @@ const id = 'meta-viewport-large';
27
27
 
28
28
  const meta = {
29
29
  title: 'Viewport meta tag should allow zooming up to 500%',
30
- description: 'Checks that <meta name="viewport"> does not set user-scalable=no or maximum-scale below 5 (500%).',
30
+ description:
31
+ 'Checks that <meta name="viewport"> does not set user-scalable=no or maximum-scale below 5 (500%).',
31
32
  i18n: {
32
33
  titleKey: 'metaViewportLarge_title',
33
34
  descriptionKey: 'metaViewportLarge_description'
@@ -60,13 +61,18 @@ function runInPage(ctx) {
60
61
  const eq = pair.indexOf('=');
61
62
  if (eq === -1) continue;
62
63
  const key = pair.slice(0, eq).trim().toLowerCase();
63
- const value = pair.slice(eq + 1).trim().toLowerCase();
64
+ const value = pair
65
+ .slice(eq + 1)
66
+ .trim()
67
+ .toLowerCase();
64
68
  if (key) out[key] = value;
65
69
  }
66
70
  return out;
67
71
  }
68
72
 
69
- const nodes = document.querySelectorAll ? document.querySelectorAll('meta[name="viewport" i]') : [];
73
+ const nodes = document.querySelectorAll
74
+ ? document.querySelectorAll('meta[name="viewport" i]')
75
+ : [];
70
76
 
71
77
  const occurrences = [];
72
78
  let applicableCount = 0;
@@ -97,7 +103,7 @@ function runInPage(ctx) {
97
103
  if (!reasons.length) continue;
98
104
 
99
105
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
100
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
106
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
101
107
 
102
108
  occurrences.push({
103
109
  selector: stableSelector,
@@ -119,7 +125,12 @@ function runInPage(ctx) {
119
125
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
120
126
  }
121
127
  if (occurrences.length) {
122
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
128
+ return {
129
+ ruleId: rule.ruleId,
130
+ outcome: 'cantTell',
131
+ severity: rule.defaultSeverity || 'minor',
132
+ occurrences
133
+ };
123
134
  }
124
135
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
125
136
  }
@@ -57,7 +57,13 @@ const meta = {
57
57
  tags: ['wcag2a', 'wcag211', 'structure', 'atomic', 'manual'],
58
58
  wcagSc: ['2.1.1'],
59
59
  normativeMappings: [
60
- { standard: 'WCAG', version: '2.2', requirement: '2.1.1', title: 'Keyboard', conformanceLevel: 'A' }
60
+ {
61
+ standard: 'WCAG',
62
+ version: '2.2',
63
+ requirement: '2.1.1',
64
+ title: 'Keyboard',
65
+ conformanceLevel: 'A'
66
+ }
61
67
  ],
62
68
  defaultSeverity: 'moderate',
63
69
  category: 'operable',
@@ -67,18 +73,28 @@ const meta = {
67
73
  };
68
74
 
69
75
  function runInPage(ctx) {
70
- const { document, helpers, rule } = ctx;
76
+ const { helpers, rule } = ctx;
71
77
 
72
78
  const MOUSE_ONLY_ATTRS = [
73
- 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup',
74
- 'ondblclick', 'onmousemove', 'onmouseenter', 'onmouseleave'
79
+ 'onmouseover',
80
+ 'onmouseout',
81
+ 'onmousedown',
82
+ 'onmouseup',
83
+ 'ondblclick',
84
+ 'onmousemove',
85
+ 'onmouseenter',
86
+ 'onmouseleave'
75
87
  ];
76
88
  const KEYBOARD_EQUIV_ATTRS = ['onkeydown', 'onkeyup', 'onkeypress', 'onfocus', 'onblur'];
77
89
 
78
- function trim(v) { return (v == null ? '' : String(v)).trim(); }
90
+ function trim(v) {
91
+ return (v == null ? '' : String(v)).trim();
92
+ }
79
93
 
80
94
  const selector = MOUSE_ONLY_ATTRS.map((a) => `[${a}]`).join(', ');
81
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
95
+ const nodes = helpers.queryAllSmart
96
+ ? helpers.queryAllSmart(selector)
97
+ : helpers.queryAll(selector);
82
98
 
83
99
  const occurrences = [];
84
100
  let applicableCount = 0;
@@ -90,7 +106,8 @@ function runInPage(ctx) {
90
106
  if (!presentMouseAttrs.length) continue;
91
107
 
92
108
  const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
93
- const eligible = typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
109
+ const eligible =
110
+ typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
94
111
  if (!eligible) continue;
95
112
 
96
113
  applicableCount += 1;
@@ -99,8 +116,10 @@ function runInPage(ctx) {
99
116
  if (hasKeyboardEquiv) continue;
100
117
 
101
118
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
102
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
103
- const eligInfo = helpers.getEligibilityInfo ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
119
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
120
+ const eligInfo = helpers.getEligibilityInfo
121
+ ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
122
+ : null;
104
123
 
105
124
  occurrences.push({
106
125
  selector: stableSelector,
@@ -113,7 +132,10 @@ function runInPage(ctx) {
113
132
  params: { attrs: presentMouseAttrs.join(', ') }
114
133
  },
115
134
  data: {
116
- details: { reasonCode: 'MOUSE_ONLY_HANDLER_NO_KEYBOARD_EQUIVALENT', mouseAttrs: presentMouseAttrs },
135
+ details: {
136
+ reasonCode: 'MOUSE_ONLY_HANDLER_NO_KEYBOARD_EQUIVALENT',
137
+ mouseAttrs: presentMouseAttrs
138
+ },
117
139
  visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
118
140
  }
119
141
  });
@@ -124,7 +146,12 @@ function runInPage(ctx) {
124
146
  }
125
147
 
126
148
  if (occurrences.length) {
127
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'moderate', occurrences };
149
+ return {
150
+ ruleId: rule.ruleId,
151
+ outcome: 'cantTell',
152
+ severity: rule.defaultSeverity || 'moderate',
153
+ occurrences
154
+ };
128
155
  }
129
156
 
130
157
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
@@ -48,7 +48,13 @@ const meta = {
48
48
  tags: ['wcag2a', 'wcag142', 'media', 'atomic', 'manual'],
49
49
  wcagSc: ['1.4.2'],
50
50
  normativeMappings: [
51
- { standard: 'WCAG', version: '2.2', requirement: '1.4.2', title: 'Audio Control', conformanceLevel: 'A' }
51
+ {
52
+ standard: 'WCAG',
53
+ version: '2.2',
54
+ requirement: '1.4.2',
55
+ title: 'Audio Control',
56
+ conformanceLevel: 'A'
57
+ }
52
58
  ],
53
59
  defaultSeverity: 'moderate',
54
60
  category: 'perceivable',
@@ -58,9 +64,11 @@ const meta = {
58
64
  };
59
65
 
60
66
  function runInPage(ctx) {
61
- const { document, helpers, rule } = ctx;
67
+ const { helpers, rule } = ctx;
62
68
 
63
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('audio[autoplay], video[autoplay]') : helpers.queryAll('audio[autoplay], video[autoplay]');
69
+ const nodes = helpers.queryAllSmart
70
+ ? helpers.queryAllSmart('audio[autoplay], video[autoplay]')
71
+ : helpers.queryAll('audio[autoplay], video[autoplay]');
64
72
 
65
73
  const occurrences = [];
66
74
  let applicableCount = 0;
@@ -75,12 +83,13 @@ function runInPage(ctx) {
75
83
 
76
84
  const mediaTag = (el.tagName || '').toLowerCase();
77
85
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
78
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
86
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
79
87
 
80
88
  const baseOccurrence = {
81
89
  selector: stableSelector,
82
90
  html,
83
- summary: 'This element autoplays audio without a native pause/stop or volume-control mechanism.',
91
+ summary:
92
+ 'This element autoplays audio without a native pause/stop or volume-control mechanism.',
84
93
  hint: 'If this clip plays for more than 3 seconds, add a `controls` attribute (or an equivalent custom mechanism) so users can pause/stop it or control its volume independently of the system volume.',
85
94
  i18n: {
86
95
  summaryKey: 'noAutoplayAudio_summary_cantTell',
@@ -104,7 +113,12 @@ function runInPage(ctx) {
104
113
  }
105
114
 
106
115
  if (occurrences.length) {
107
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'moderate', occurrences };
116
+ return {
117
+ ruleId: rule.ruleId,
118
+ outcome: 'cantTell',
119
+ severity: rule.defaultSeverity || 'moderate',
120
+ occurrences
121
+ };
108
122
  }
109
123
 
110
124
  // Manual rules may only emit cantTell/notApplicable (never pass/fail):