@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
@@ -11,20 +11,27 @@
11
11
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
12
  */
13
13
 
14
- const id = "canvas-text-alternative-quality";
14
+ const id = 'canvas-text-alternative-quality';
15
15
 
16
16
  const meta = {
17
- title: "<canvas> text alternative must be appropriate (manual review)",
18
- description: "Flags <canvas> elements with a detected text alternative for human review of equivalence and appropriateness.",
17
+ title: '<canvas> text alternative must be appropriate (manual review)',
18
+ description:
19
+ 'Flags <canvas> elements with a detected text alternative for human review of equivalence and appropriateness.',
19
20
  i18n: {
20
- titleKey: "canvas_textAltQuality_title",
21
- descriptionKey: "canvas_textAltQuality_description"
21
+ titleKey: 'canvas_textAltQuality_title',
22
+ descriptionKey: 'canvas_textAltQuality_description'
22
23
  },
23
24
  helpUrl: null,
24
- tags: ["wcag2a", "wcag111", "nontext", "canvas", "manual", "atomic"],
25
+ tags: ['wcag2a', 'wcag111', 'nontext', 'canvas', 'manual', 'atomic'],
25
26
  wcagSc: ['1.1.1'],
26
27
  normativeMappings: [
27
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
28
+ {
29
+ standard: 'WCAG',
30
+ version: '2.2',
31
+ requirement: '1.1.1',
32
+ title: 'Non-text Content',
33
+ conformanceLevel: 'A'
34
+ }
28
35
  ],
29
36
  defaultSeverity: 'minor',
30
37
  category: 'perceivable',
@@ -38,133 +45,161 @@ const meta = {
38
45
  };
39
46
 
40
47
  function runInPage(ctx) {
41
-
42
- const { document, root, helpers, rule } = ctx;
43
- const safeRoot = root || document;
44
-
45
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
46
- const queryAll = helpers && typeof helpers.queryAll === 'function'
47
- ? helpers.queryAll
48
- : (sel) => {
49
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
50
- catch { return []; }
48
+ const { document, root, helpers, rule } = ctx;
49
+ const safeRoot = root || document;
50
+
51
+ const queryAllSmart =
52
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
53
+ const queryAll =
54
+ helpers && typeof helpers.queryAll === 'function'
55
+ ? helpers.queryAll
56
+ : (sel) => {
57
+ try {
58
+ return safeRoot && safeRoot.querySelectorAll
59
+ ? Array.from(safeRoot.querySelectorAll(sel))
60
+ : [];
61
+ } catch {
62
+ return [];
63
+ }
51
64
  };
52
65
 
53
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
54
- ? helpers.getEligibilityInfo
55
- : null;
56
-
57
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
58
- ? helpers.isAccTreeEligible
59
- : null;
60
-
61
- const __accEligCache = new WeakMap();
62
- function accEligibleCached(node) {
63
- if (!isAccTreeEligible) return { eligible: true, reasons: [] };
64
- if (!node || typeof node !== 'object') return { eligible: true, reasons: [] };
65
- const c = __accEligCache.get(node);
66
- if (c) return c;
67
- let r;
68
- try { r = isAccTreeEligible(node, ctx); } catch { r = { eligible: true, reasons: [] }; }
69
- r = r && typeof r === 'object' ? r : { eligible: !!r, reasons: [] };
70
- __accEligCache.set(node, r);
71
- return r;
66
+ const getEligibilityInfo =
67
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
68
+
69
+ const isAccTreeEligible =
70
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
71
+
72
+ const __accEligCache = new WeakMap();
73
+ function accEligibleCached(node) {
74
+ if (!isAccTreeEligible) return { eligible: true, reasons: [] };
75
+ if (!node || typeof node !== 'object') return { eligible: true, reasons: [] };
76
+ const c = __accEligCache.get(node);
77
+ if (c) return c;
78
+ let r;
79
+ try {
80
+ r = isAccTreeEligible(node, ctx);
81
+ } catch {
82
+ r = { eligible: true, reasons: [] };
72
83
  }
84
+ r = r && typeof r === 'object' ? r : { eligible: !!r, reasons: [] };
85
+ __accEligCache.set(node, r);
86
+ return r;
87
+ }
73
88
 
74
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
75
- ? helpers.getFocusableInfo
76
- : null;
77
-
78
- function isRolePresentationExcluded(el) {
79
- const role = (() => {
80
- try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
81
- catch { return ''; }
82
- })();
83
- if (role !== 'presentation' && role !== 'none') return false;
84
-
85
- // Exclude only when NOT focusable (mirrors img-alt-present policy)
86
- let focusable = false;
87
- if (getFocusableInfo) {
88
- const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
89
- focusable = !!(fi && fi.focusable);
90
- } else {
91
- let tabindex = null;
92
- try { tabindex = el.getAttribute('tabindex'); } catch { tabindex = null; }
93
- const t = tabindex == null ? '' : String(tabindex).trim();
94
- focusable = t !== '' && !Number.isNaN(Number(t));
89
+ const getFocusableInfo =
90
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
91
+
92
+ function isRolePresentationExcluded(el) {
93
+ const role = (() => {
94
+ try {
95
+ return String(el.getAttribute('role') || '')
96
+ .trim()
97
+ .toLowerCase();
98
+ } catch {
99
+ return '';
100
+ }
101
+ })();
102
+ if (role !== 'presentation' && role !== 'none') return false;
103
+
104
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
105
+ let focusable;
106
+ if (getFocusableInfo) {
107
+ const fi = (() => {
108
+ try {
109
+ return getFocusableInfo(el, ctx);
110
+ } catch {
111
+ return null;
95
112
  }
96
- return !focusable;
113
+ })();
114
+ focusable = !!(fi && fi.focusable);
115
+ } else {
116
+ let tabindex;
117
+ try {
118
+ tabindex = el.getAttribute('tabindex');
119
+ } catch {
120
+ tabindex = null;
121
+ }
122
+ const t = tabindex == null ? '' : String(tabindex).trim();
123
+ focusable = t !== '' && !Number.isNaN(Number(t));
97
124
  }
125
+ return !focusable;
126
+ }
98
127
 
99
-
100
- const els = (() => {
101
- try { return Array.from((queryAllSmart ? queryAllSmart('canvas') : queryAll('canvas')) || []); }
102
- catch { return queryAll('canvas'); }
103
- })();
104
-
105
- if (!els.length) {
106
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
128
+ const els = (() => {
129
+ try {
130
+ return Array.from((queryAllSmart ? queryAllSmart('canvas') : queryAll('canvas')) || []);
131
+ } catch {
132
+ return queryAll('canvas');
107
133
  }
134
+ })();
108
135
 
109
- const occurrences = [];
110
- let applicableCount = 0;
136
+ if (!els.length) {
137
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
138
+ }
111
139
 
112
- for (const el of els) {
113
- if (!el || !el.getAttribute) continue;
140
+ const occurrences = [];
141
+ let applicableCount = 0;
114
142
 
115
- if (isAccTreeEligible) {
116
- const elig = accEligibleCached(el);
117
- if (elig && elig.eligible === false) continue;
118
- }
143
+ for (const el of els) {
144
+ if (!el || !el.getAttribute) continue;
119
145
 
120
- if (isRolePresentationExcluded(el)) continue;
121
-
122
- const getTextAlternativeInfo =
123
- helpers && typeof helpers.getTextAlternativeInfo === 'function'
124
- ? helpers.getTextAlternativeInfo
125
- : null;
146
+ if (isAccTreeEligible) {
147
+ const elig = accEligibleCached(el);
148
+ if (elig && elig.eligible === false) continue;
149
+ }
126
150
 
127
- const trim = (v) => (v == null ? '' : String(v)).trim();
151
+ if (isRolePresentationExcluded(el)) continue;
128
152
 
129
- // Rule-specific applicability (only elements that already have a text alternative mechanism)
130
- let textAltInfo = null;
131
- if (getTextAlternativeInfo) {
132
- try { textAltInfo = getTextAlternativeInfo(el, ctx); } catch { textAltInfo = null; }
133
- }
153
+ const getTextAlternativeInfo =
154
+ helpers && typeof helpers.getTextAlternativeInfo === 'function'
155
+ ? helpers.getTextAlternativeInfo
156
+ : null;
134
157
 
135
- const hasTextAlt = !!(textAltInfo && textAltInfo.present);
136
- if (!hasTextAlt) continue;
137
-
138
- applicableCount += 1;
139
-
140
- const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
141
-
142
- const baseOccurrence = {
143
- summary: 'Review text alternative for <canvas> for equivalence and appropriateness.',
144
- hint: 'Confirm the fallback text or accessible name conveys the same information/function as the canvas content.',
145
- i18n: {
146
- summaryKey: 'canvas_textAltQuality_summary_cantTell',
147
- hintKey: 'canvas_textAltQuality_hint_cantTell',
148
- params: { element: (el.tagName || '').toLowerCase() }
149
- },
150
- data: {
151
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
152
- details: textAltInfo || null
153
- }
154
- };
158
+ const trim = (v) => (v == null ? '' : String(v)).trim();
155
159
 
156
- if (helpers && typeof helpers.reportOccurrence === 'function') {
157
- occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
158
- } else {
159
- occurrences.push({ selector: '', html: '', ...baseOccurrence });
160
- }
160
+ // Rule-specific applicability (only elements that already have a text alternative mechanism)
161
+ let textAltInfo = null;
162
+ if (getTextAlternativeInfo) {
163
+ try {
164
+ textAltInfo = getTextAlternativeInfo(el, ctx);
165
+ } catch {
166
+ textAltInfo = null;
167
+ }
161
168
  }
162
169
 
163
- if (applicableCount === 0) {
164
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
170
+ const hasTextAlt = !!(textAltInfo && textAltInfo.present);
171
+ if (!hasTextAlt) continue;
172
+
173
+ applicableCount += 1;
174
+
175
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
176
+
177
+ const baseOccurrence = {
178
+ summary: 'Review text alternative for <canvas> for equivalence and appropriateness.',
179
+ hint: 'Confirm the fallback text or accessible name conveys the same information/function as the canvas content.',
180
+ i18n: {
181
+ summaryKey: 'canvas_textAltQuality_summary_cantTell',
182
+ hintKey: 'canvas_textAltQuality_hint_cantTell',
183
+ params: { element: (el.tagName || '').toLowerCase() }
184
+ },
185
+ data: {
186
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
187
+ details: textAltInfo || null
188
+ }
189
+ };
190
+
191
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
192
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
193
+ } else {
194
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
165
195
  }
196
+ }
197
+
198
+ if (applicableCount === 0) {
199
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
200
+ }
166
201
 
167
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
202
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
168
203
  }
169
204
 
170
205
  module.exports = { id, meta, runInPage };