@surea11y/core 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +81 -7
  2. package/LICENSE +373 -21
  3. package/README.md +175 -35
  4. package/bin/surea11y-core.js +20 -0
  5. package/docs/API_STABILITY.md +27 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/ENGINE_OPTIONS.md +2 -0
  9. package/docs/I18N.md +12 -9
  10. package/docs/INTEGRATION.md +19 -1
  11. package/docs/LIMITATIONS.md +1 -1
  12. package/docs/OUTPUT_SCHEMA.md +1 -1
  13. package/docs/REPORT.md +1 -1
  14. package/docs/RULE_CATALOG.md +1 -1
  15. package/docs/SARIF.md +59 -0
  16. package/package.json +63 -18
  17. package/src/baseline.js +0 -0
  18. package/src/checks/automatic/area-alt-present.js +63 -31
  19. package/src/checks/automatic/aria-allowed-attr.js +204 -80
  20. package/src/checks/automatic/aria-allowed-role.js +23 -7
  21. package/src/checks/automatic/aria-braille-equivalent.js +34 -10
  22. package/src/checks/automatic/aria-conditional-attr.js +32 -14
  23. package/src/checks/automatic/aria-deprecated-role.js +26 -11
  24. package/src/checks/automatic/aria-hidden-body.js +48 -23
  25. package/src/checks/automatic/aria-hidden-focus.js +420 -66
  26. package/src/checks/automatic/aria-prohibited-attr.js +327 -60
  27. package/src/checks/automatic/aria-prohibited-children.js +111 -103
  28. package/src/checks/automatic/aria-required-attr.js +29 -15
  29. package/src/checks/automatic/aria-required-children.js +44 -24
  30. package/src/checks/automatic/aria-required-parent.js +64 -35
  31. package/src/checks/automatic/aria-role-name-present.js +49 -21
  32. package/src/checks/automatic/aria-roles-valid.js +24 -12
  33. package/src/checks/automatic/aria-valid-attr-value.js +46 -22
  34. package/src/checks/automatic/aria-valid-attr.js +19 -5
  35. package/src/checks/automatic/autocomplete-valid.js +76 -16
  36. package/src/checks/automatic/avoid-inline-spacing.js +23 -8
  37. package/src/checks/automatic/binary-control-name-present.js +62 -50
  38. package/src/checks/automatic/button-name-present.js +54 -24
  39. package/src/checks/automatic/bypass-blocks-present.js +51 -32
  40. package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
  41. package/src/checks/automatic/combobox-name-present.js +40 -45
  42. package/src/checks/automatic/contrast-computable.js +363 -341
  43. package/src/checks/automatic/contrast-enhanced.js +489 -466
  44. package/src/checks/automatic/contrast-minimum.js +488 -465
  45. package/src/checks/automatic/css-orientation-lock.js +51 -35
  46. package/src/checks/automatic/definition-list-children-valid.js +46 -25
  47. package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
  48. package/src/checks/automatic/dialog-name-present.js +47 -85
  49. package/src/checks/automatic/dlitem-parent-valid.js +25 -8
  50. package/src/checks/automatic/duplicate-id-aria.js +28 -9
  51. package/src/checks/automatic/embed-text-alternative-present.js +88 -35
  52. package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
  53. package/src/checks/automatic/form-control-single-label.js +50 -14
  54. package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
  55. package/src/checks/automatic/iframe-focusable-content.js +265 -22
  56. package/src/checks/automatic/iframe-name-present.js +33 -9
  57. package/src/checks/automatic/iframe-title-unique.js +32 -9
  58. package/src/checks/automatic/img-alt-present.js +54 -52
  59. package/src/checks/automatic/input-image-alt-present.js +141 -112
  60. package/src/checks/automatic/label-in-name.js +65 -41
  61. package/src/checks/automatic/language-page-present.js +111 -109
  62. package/src/checks/automatic/link-in-text-block.js +61 -19
  63. package/src/checks/automatic/link-name-present.js +47 -14
  64. package/src/checks/automatic/list-children-valid.js +40 -33
  65. package/src/checks/automatic/listbox-name-present.js +41 -19
  66. package/src/checks/automatic/listitem-parent-valid.js +48 -13
  67. package/src/checks/automatic/menuitem-name-present.js +41 -61
  68. package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
  69. package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
  70. package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
  71. package/src/checks/automatic/meter-name-present.js +40 -36
  72. package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
  73. package/src/checks/automatic/object-text-alternative-present.js +93 -39
  74. package/src/checks/automatic/option-name-present.js +40 -21
  75. package/src/checks/automatic/page-title-present.js +19 -6
  76. package/src/checks/automatic/progressbar-name-present.js +49 -44
  77. package/src/checks/automatic/role-img-alt-present.js +211 -159
  78. package/src/checks/automatic/searchbox-name-present.js +41 -19
  79. package/src/checks/automatic/server-side-image-map-absent.js +27 -11
  80. package/src/checks/automatic/slider-name-present.js +42 -47
  81. package/src/checks/automatic/spinbutton-name-present.js +41 -19
  82. package/src/checks/automatic/summary-name-present.js +39 -17
  83. package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
  84. package/src/checks/automatic/svg-text-alternative-present.js +262 -230
  85. package/src/checks/automatic/tab-name-present.js +39 -60
  86. package/src/checks/automatic/table-headers-attr-valid.js +27 -10
  87. package/src/checks/automatic/table-th-has-data-cells.js +24 -8
  88. package/src/checks/automatic/target-size-minimum.js +123 -48
  89. package/src/checks/automatic/td-has-header.js +53 -12
  90. package/src/checks/automatic/textbox-name-present.js +41 -19
  91. package/src/checks/automatic/tooltip-name-present.js +39 -18
  92. package/src/checks/automatic/treeitem-name-present.js +40 -21
  93. package/src/checks/automatic/valid-lang.js +22 -6
  94. package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
  95. package/src/checks/manual/accesskeys-manual.js +17 -6
  96. package/src/checks/manual/area-alt-decorative-manual.js +194 -193
  97. package/src/checks/manual/area-alt-quality-manual.js +184 -141
  98. package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
  99. package/src/checks/manual/aria-text-manual.js +20 -11
  100. package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
  101. package/src/checks/manual/css-hidden-focus.js +375 -169
  102. package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
  103. package/src/checks/manual/empty-heading-manual.js +41 -24
  104. package/src/checks/manual/empty-table-header-manual.js +69 -31
  105. package/src/checks/manual/focus-order-semantics-manual.js +60 -13
  106. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
  107. package/src/checks/manual/heading-order-manual.js +50 -8
  108. package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
  109. package/src/checks/manual/image-redundant-alt-manual.js +38 -8
  110. package/src/checks/manual/img-alt-decorative-manual.js +133 -96
  111. package/src/checks/manual/img-alt-quality-manual.js +178 -127
  112. package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
  113. package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
  114. package/src/checks/manual/label-title-only-manual.js +44 -28
  115. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
  116. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
  117. package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
  118. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
  119. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
  120. package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
  121. package/src/checks/manual/landmark-one-main-manual.js +38 -43
  122. package/src/checks/manual/landmark-unique-manual.js +78 -67
  123. package/src/checks/manual/link-name-quality-manual.js +45 -12
  124. package/src/checks/manual/media-transcript-present-manual.js +37 -22
  125. package/src/checks/manual/meta-viewport-large-manual.js +19 -6
  126. package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
  127. package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
  128. package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
  129. package/src/checks/manual/p-as-heading-manual.js +24 -7
  130. package/src/checks/manual/page-has-heading-one-manual.js +42 -32
  131. package/src/checks/manual/page-title-patterns-manual.js +80 -50
  132. package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
  133. package/src/checks/manual/region-manual.js +244 -60
  134. package/src/checks/manual/scope-attr-valid-manual.js +13 -4
  135. package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
  136. package/src/checks/manual/skip-link-manual.js +42 -18
  137. package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
  138. package/src/checks/manual/tabindex-manual.js +13 -4
  139. package/src/checks/manual/table-duplicate-name-manual.js +22 -11
  140. package/src/checks/manual/table-fake-caption-manual.js +48 -10
  141. package/src/checks/manual/video-caption-manual.js +17 -4
  142. package/src/checks/manual-review.js +58 -12
  143. package/src/core.js +41705 -29650
  144. package/src/index.js +2 -0
  145. package/src/report.js +109 -47
  146. package/src/sarif.js +190 -0
  147. package/surea11y.browser.js +37774 -0
  148. package/bin/core.js +0 -348
  149. package/docs/CLI.md +0 -75
  150. package/src/catalogs/composites.wcag.js +0 -490
  151. package/src/checks/rules-and-tags.full.csv +0 -19
  152. package/src/checks/rules-and-tags.full.json +0 -259
  153. package/src/core/aria-helpers.js +0 -970
  154. package/src/core/contrast-helpers.js +0 -1147
  155. package/src/core/dom-helpers.js +0 -4235
  156. package/src/core/dom-runner.js +0 -671
  157. package/src/core/frame-messaging.js +0 -210
  158. package/src/core/frame-scan.js +0 -178
  159. package/src/core/rollup-composites.js +0 -135
  160. package/src/core/rule-meta.js +0 -159
  161. package/src/coverage/wcag-facets.js +0 -1079
  162. package/src/coverage/wcag-version-map.js +0 -84
  163. package/src/i18n/en.js +0 -923
  164. package/src/i18n/fr.js +0 -844
  165. package/src/policy/contracts.js +0 -18
  166. package/src/policy/resolvePolicy.js +0 -55
  167. package/src/policy/schemas/engine-options.schema.json +0 -103
  168. package/src/policy/schemas/policy-contract.schema.json +0 -40
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -37,7 +39,13 @@ const meta = {
37
39
  tags: ['wcag2a', 'wcag111', 'media', 'video', 'atomic', 'automatic'],
38
40
  wcagSc: ['1.1.1'],
39
41
  normativeMappings: [
40
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
42
+ {
43
+ standard: 'WCAG',
44
+ version: '2.2',
45
+ requirement: '1.1.1',
46
+ title: 'Non-text Content',
47
+ conformanceLevel: 'A'
48
+ }
41
49
  ],
42
50
  defaultSeverity: 'serious',
43
51
  category: 'perceivable',
@@ -54,32 +62,28 @@ function runInPage(ctx) {
54
62
  const { document, root, helpers, rule } = ctx;
55
63
  const safeRoot = root || document;
56
64
 
57
- const reportOccurrence = helpers && typeof helpers.reportOccurrence === 'function'
58
- ? helpers.reportOccurrence
59
- : null;
65
+ const reportOccurrence =
66
+ helpers && typeof helpers.reportOccurrence === 'function' ? helpers.reportOccurrence : null;
60
67
 
61
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
62
- const queryAll = helpers && typeof helpers.queryAll === 'function'
63
- ? helpers.queryAll
64
- : null;
68
+ const queryAllSmart =
69
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
70
+ const queryAll = helpers && typeof helpers.queryAll === 'function' ? helpers.queryAll : null;
65
71
 
66
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
67
- ? helpers.getEligibilityInfo
68
- : null;
72
+ const getEligibilityInfo =
73
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
69
74
 
70
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
71
- ? helpers.isAccTreeEligible
72
- : null;
75
+ const isAccTreeEligible =
76
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
73
77
 
74
- const isFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
75
- ? helpers.getFocusableInfo
76
- : null;
78
+ const isFocusableInfo =
79
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
77
80
 
78
- const getAriaNameInfo = helpers && typeof helpers.getAriaNameInfo === 'function'
79
- ? helpers.getAriaNameInfo
80
- : null;
81
+ const getAriaNameInfo =
82
+ helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
81
83
 
82
- function trim(v) { return (v == null ? '' : String(v)).trim(); }
84
+ function trim(v) {
85
+ return (v == null ? '' : String(v)).trim();
86
+ }
83
87
 
84
88
  function computeNameInfo(el) {
85
89
  // <video> is not a labelable element (no browser computes an accessible
@@ -89,11 +93,22 @@ function runInPage(ctx) {
89
93
  let aria = null;
90
94
 
91
95
  if (getAriaNameInfo) {
92
- aria = (() => { try { return getAriaNameInfo(el, ctx); } catch { return null; } })();
96
+ aria = (() => {
97
+ try {
98
+ return getAriaNameInfo(el, ctx);
99
+ } catch {
100
+ return null;
101
+ }
102
+ })();
93
103
  }
94
104
 
95
105
  if (aria && aria.present && trim(aria.value)) {
96
- return { present: true, value: trim(aria.value), mechanism: aria.mechanism || 'aria', flags: (aria.flags || []).slice(0) };
106
+ return {
107
+ present: true,
108
+ value: trim(aria.value),
109
+ mechanism: aria.mechanism || 'aria',
110
+ flags: (aria.flags || []).slice(0)
111
+ };
97
112
  }
98
113
 
99
114
  const title = trim(el.getAttribute && el.getAttribute('title'));
@@ -113,14 +128,22 @@ function runInPage(ctx) {
113
128
  try {
114
129
  if (queryAllSmart) return Array.from(queryAllSmart('video') || []);
115
130
  if (queryAll) return Array.from(queryAll('video') || []);
116
- return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll('video')) : [];
131
+ return safeRoot && safeRoot.querySelectorAll
132
+ ? Array.from(safeRoot.querySelectorAll('video'))
133
+ : [];
117
134
  } catch {
118
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll('video')) : []; }
119
- catch { return []; }
135
+ try {
136
+ return safeRoot && safeRoot.querySelectorAll
137
+ ? Array.from(safeRoot.querySelectorAll('video'))
138
+ : [];
139
+ } catch {
140
+ return [];
141
+ }
120
142
  }
121
143
  })();
122
144
 
123
- if (!videos.length) return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
145
+ if (!videos.length)
146
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
124
147
 
125
148
  const occurrences = [];
126
149
  let applicableCount = 0;
@@ -134,7 +157,11 @@ function runInPage(ctx) {
134
157
  // Eligibility: only elements exposed to AT (with helper exceptions)
135
158
  if (isAccTreeEligible) {
136
159
  const elig = (() => {
137
- try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
160
+ try {
161
+ return isAccTreeEligible(el, ctx);
162
+ } catch {
163
+ return { eligible: true, reasons: [] };
164
+ }
138
165
  })();
139
166
  if (elig && elig.eligible === false) continue;
140
167
  }
@@ -142,13 +169,20 @@ function runInPage(ctx) {
142
169
  // Role presentation/none excluded ONLY if not focusable (mirrors img behavior)
143
170
  const role = trim(el.getAttribute('role')).toLowerCase();
144
171
  if (role === 'presentation' || role === 'none') {
145
- let focusable = false;
172
+ let focusable;
146
173
  if (isFocusableInfo) {
147
- const fi = (() => { try { return isFocusableInfo(el, ctx); } catch { return null; } })();
174
+ const fi = (() => {
175
+ try {
176
+ return isFocusableInfo(el, ctx);
177
+ } catch {
178
+ return null;
179
+ }
180
+ })();
148
181
  focusable = !!(fi && fi.focusable);
149
182
  } else {
150
183
  const tabindex = el.getAttribute('tabindex');
151
- focusable = tabindex != null && trim(tabindex) !== '' && !Number.isNaN(Number(trim(tabindex)));
184
+ focusable =
185
+ tabindex != null && trim(tabindex) !== '' && !Number.isNaN(Number(trim(tabindex)));
152
186
  }
153
187
  if (!focusable) continue;
154
188
  }
@@ -160,12 +194,16 @@ function runInPage(ctx) {
160
194
 
161
195
  let eligInfo = null;
162
196
  if (getEligibilityInfo) {
163
- try { eligInfo = getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { eligInfo = null; }
197
+ try {
198
+ eligInfo = getEligibilityInfo(el, ctx, { targetSet: 'acc' });
199
+ } catch {
200
+ eligInfo = null;
201
+ }
164
202
  }
165
203
 
166
204
  const baseOccurrence = {
167
205
  summary: 'Missing text alternative for <video> poster.',
168
- hint: 'Provide an accessible name (e.g., aria-label/aria-labelledby) for the poster image.',
206
+ hint: 'Provide an accessible name for the poster image (aria-label/aria-labelledby preferred, or a title attribute as a fallback).',
169
207
  i18n: {
170
208
  summaryKey: 'videoPoster_textAltPresent_summary_fail',
171
209
  hintKey: 'videoPoster_textAltPresent_hint_fail',
@@ -185,9 +223,16 @@ function runInPage(ctx) {
185
223
  }
186
224
  }
187
225
 
188
- if (applicableCount === 0) return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
189
- if (!occurrences.length) return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
190
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
226
+ if (applicableCount === 0)
227
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
228
+ if (!occurrences.length)
229
+ return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
230
+ return {
231
+ ruleId: rule.ruleId,
232
+ outcome: 'fail',
233
+ severity: rule.defaultSeverity || 'minor',
234
+ occurrences
235
+ };
191
236
  }
192
237
 
193
238
  module.exports = { id, meta, runInPage };
@@ -1,10 +1,12 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
4
6
  * @check accesskeys
5
7
  * @atomic true
6
8
  * @summary accesskey values must be unique on the page
7
- * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
10
  * @applicability
9
11
  * Applies whenever two or more elements share the same non-empty
10
12
  * accesskey attribute value (case-insensitive).
@@ -40,8 +42,10 @@ const meta = {
40
42
  };
41
43
 
42
44
  function runInPage(ctx) {
43
- const { document, helpers, rule } = ctx;
44
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[accesskey]') : helpers.queryAll('[accesskey]');
45
+ const { helpers, rule } = ctx;
46
+ const nodes = helpers.queryAllSmart
47
+ ? helpers.queryAllSmart('[accesskey]')
48
+ : helpers.queryAll('[accesskey]');
45
49
 
46
50
  const groups = new Map(); // normalized key -> elements[]
47
51
  for (const el of nodes) {
@@ -59,12 +63,14 @@ function runInPage(ctx) {
59
63
  if (els.length <= 1) continue;
60
64
  for (const el of els) {
61
65
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
62
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
66
+ const html = helpers.getOuterHtmlSnippet
67
+ ? helpers.getOuterHtmlSnippet(el)
68
+ : el.outerHTML || '';
63
69
 
64
70
  occurrences.push({
65
71
  selector: stableSelector,
66
72
  html,
67
- summary: 'This element\'s accesskey is shared with another element on the page.',
73
+ summary: "This element's accesskey is shared with another element on the page.",
68
74
  hint: 'Make each accesskey value unique across the page.',
69
75
  i18n: {
70
76
  summaryKey: 'accesskeys_summary_cantTell',
@@ -81,7 +87,12 @@ function runInPage(ctx) {
81
87
  if (!occurrences.length) {
82
88
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
83
89
  }
84
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
90
+ return {
91
+ ruleId: rule.ruleId,
92
+ outcome: 'cantTell',
93
+ severity: rule.defaultSeverity || 'minor',
94
+ occurrences
95
+ };
85
96
  }
86
97
 
87
98
  module.exports = { id, meta, runInPage };