@surea11y/core 1.3.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 (163) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/README.md +109 -35
  3. package/bin/surea11y-core.js +20 -0
  4. package/docs/API_STABILITY.md +26 -0
  5. package/docs/CI_INTEGRATIONS.md +7 -7
  6. package/docs/ENGINE_OPTIONS.md +1 -1
  7. package/docs/I18N.md +12 -9
  8. package/docs/INTEGRATION.md +1 -1
  9. package/docs/LIMITATIONS.md +1 -1
  10. package/docs/REPORT.md +1 -1
  11. package/package.json +50 -16
  12. package/src/baseline.js +0 -0
  13. package/src/checks/automatic/area-alt-present.js +4 -6
  14. package/src/checks/automatic/aria-allowed-attr.js +15 -51
  15. package/src/checks/automatic/aria-allowed-role.js +2 -0
  16. package/src/checks/automatic/aria-braille-equivalent.js +2 -0
  17. package/src/checks/automatic/aria-conditional-attr.js +8 -7
  18. package/src/checks/automatic/aria-deprecated-role.js +4 -3
  19. package/src/checks/automatic/aria-hidden-body.js +6 -4
  20. package/src/checks/automatic/aria-hidden-focus.js +12 -13
  21. package/src/checks/automatic/aria-prohibited-attr.js +98 -105
  22. package/src/checks/automatic/aria-prohibited-children.js +56 -87
  23. package/src/checks/automatic/aria-required-attr.js +6 -7
  24. package/src/checks/automatic/aria-required-children.js +7 -10
  25. package/src/checks/automatic/aria-required-parent.js +20 -25
  26. package/src/checks/automatic/aria-role-name-present.js +2 -0
  27. package/src/checks/automatic/aria-roles-valid.js +2 -0
  28. package/src/checks/automatic/aria-valid-attr-value.js +18 -15
  29. package/src/checks/automatic/aria-valid-attr.js +2 -0
  30. package/src/checks/automatic/autocomplete-valid.js +2 -0
  31. package/src/checks/automatic/avoid-inline-spacing.js +3 -2
  32. package/src/checks/automatic/binary-control-name-present.js +2 -0
  33. package/src/checks/automatic/button-name-present.js +7 -7
  34. package/src/checks/automatic/bypass-blocks-present.js +9 -7
  35. package/src/checks/automatic/canvas-text-alternative-present.js +2 -0
  36. package/src/checks/automatic/combobox-name-present.js +2 -0
  37. package/src/checks/automatic/contrast-computable.js +2 -0
  38. package/src/checks/automatic/contrast-enhanced.js +2 -0
  39. package/src/checks/automatic/contrast-minimum.js +2 -0
  40. package/src/checks/automatic/css-orientation-lock.js +21 -27
  41. package/src/checks/automatic/definition-list-children-valid.js +6 -6
  42. package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
  43. package/src/checks/automatic/dialog-name-present.js +10 -10
  44. package/src/checks/automatic/dlitem-parent-valid.js +2 -0
  45. package/src/checks/automatic/duplicate-id-aria.js +4 -3
  46. package/src/checks/automatic/embed-text-alternative-present.js +2 -0
  47. package/src/checks/automatic/form-control-programmatic-label-present.js +2 -0
  48. package/src/checks/automatic/form-control-single-label.js +6 -7
  49. package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
  50. package/src/checks/automatic/iframe-focusable-content.js +246 -18
  51. package/src/checks/automatic/iframe-name-present.js +2 -0
  52. package/src/checks/automatic/iframe-title-unique.js +3 -1
  53. package/src/checks/automatic/img-alt-present.js +7 -9
  54. package/src/checks/automatic/input-image-alt-present.js +4 -6
  55. package/src/checks/automatic/label-in-name.js +15 -19
  56. package/src/checks/automatic/language-page-present.js +2 -0
  57. package/src/checks/automatic/link-in-text-block.js +2 -0
  58. package/src/checks/automatic/link-name-present.js +2 -0
  59. package/src/checks/automatic/list-children-valid.js +14 -24
  60. package/src/checks/automatic/listbox-name-present.js +2 -0
  61. package/src/checks/automatic/listitem-parent-valid.js +30 -7
  62. package/src/checks/automatic/menuitem-name-present.js +2 -0
  63. package/src/checks/automatic/meta-refresh-no-exceptions.js +4 -4
  64. package/src/checks/automatic/meta-refresh-timing-absent.js +2 -0
  65. package/src/checks/automatic/meta-viewport-zoom-enabled.js +2 -0
  66. package/src/checks/automatic/meter-name-present.js +4 -3
  67. package/src/checks/automatic/nested-interactive-controls-absent.js +27 -5
  68. package/src/checks/automatic/object-text-alternative-present.js +2 -0
  69. package/src/checks/automatic/option-name-present.js +2 -0
  70. package/src/checks/automatic/page-title-present.js +2 -0
  71. package/src/checks/automatic/progressbar-name-present.js +8 -10
  72. package/src/checks/automatic/role-img-alt-present.js +4 -4
  73. package/src/checks/automatic/searchbox-name-present.js +2 -0
  74. package/src/checks/automatic/server-side-image-map-absent.js +4 -3
  75. package/src/checks/automatic/slider-name-present.js +2 -0
  76. package/src/checks/automatic/spinbutton-name-present.js +2 -0
  77. package/src/checks/automatic/summary-name-present.js +2 -0
  78. package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
  79. package/src/checks/automatic/svg-text-alternative-present.js +17 -5
  80. package/src/checks/automatic/tab-name-present.js +2 -0
  81. package/src/checks/automatic/table-headers-attr-valid.js +3 -2
  82. package/src/checks/automatic/table-th-has-data-cells.js +2 -0
  83. package/src/checks/automatic/target-size-minimum.js +5 -0
  84. package/src/checks/automatic/td-has-header.js +24 -1
  85. package/src/checks/automatic/textbox-name-present.js +2 -0
  86. package/src/checks/automatic/tooltip-name-present.js +2 -0
  87. package/src/checks/automatic/treeitem-name-present.js +2 -0
  88. package/src/checks/automatic/valid-lang.js +2 -0
  89. package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
  90. package/src/checks/manual/accesskeys-manual.js +3 -1
  91. package/src/checks/manual/area-alt-decorative-manual.js +2 -0
  92. package/src/checks/manual/area-alt-quality-manual.js +2 -0
  93. package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
  94. package/src/checks/manual/aria-text-manual.js +6 -5
  95. package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
  96. package/src/checks/manual/css-hidden-focus.js +184 -9
  97. package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
  98. package/src/checks/manual/empty-heading-manual.js +17 -17
  99. package/src/checks/manual/empty-table-header-manual.js +52 -25
  100. package/src/checks/manual/focus-order-semantics-manual.js +16 -4
  101. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
  102. package/src/checks/manual/heading-order-manual.js +28 -1
  103. package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
  104. package/src/checks/manual/image-redundant-alt-manual.js +21 -1
  105. package/src/checks/manual/img-alt-decorative-manual.js +2 -0
  106. package/src/checks/manual/img-alt-quality-manual.js +2 -0
  107. package/src/checks/manual/input-image-alt-decorative-manual.js +2 -0
  108. package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
  109. package/src/checks/manual/label-title-only-manual.js +29 -22
  110. package/src/checks/manual/landmark-banner-is-top-level-manual.js +54 -47
  111. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +42 -26
  112. package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
  113. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
  114. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
  115. package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
  116. package/src/checks/manual/landmark-one-main-manual.js +12 -23
  117. package/src/checks/manual/landmark-unique-manual.js +37 -52
  118. package/src/checks/manual/link-name-quality-manual.js +2 -0
  119. package/src/checks/manual/media-transcript-present-manual.js +2 -0
  120. package/src/checks/manual/meta-viewport-large-manual.js +3 -1
  121. package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
  122. package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
  123. package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
  124. package/src/checks/manual/p-as-heading-manual.js +2 -0
  125. package/src/checks/manual/page-has-heading-one-manual.js +12 -11
  126. package/src/checks/manual/page-title-patterns-manual.js +2 -0
  127. package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
  128. package/src/checks/manual/region-manual.js +27 -36
  129. package/src/checks/manual/scope-attr-valid-manual.js +3 -1
  130. package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
  131. package/src/checks/manual/skip-link-manual.js +7 -6
  132. package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
  133. package/src/checks/manual/tabindex-manual.js +3 -1
  134. package/src/checks/manual/table-duplicate-name-manual.js +5 -4
  135. package/src/checks/manual/table-fake-caption-manual.js +24 -3
  136. package/src/checks/manual/video-caption-manual.js +2 -0
  137. package/src/checks/manual-review.js +2 -0
  138. package/src/core.js +6772 -2158
  139. package/src/index.js +2 -0
  140. package/src/report.js +51 -9
  141. package/src/sarif.js +18 -3
  142. package/surea11y.browser.js +2731 -999
  143. package/bin/core.js +0 -473
  144. package/docs/CLI.md +0 -128
  145. package/src/catalogs/composites.wcag.js +0 -454
  146. package/src/checks/rules-and-tags.full.csv +0 -19
  147. package/src/checks/rules-and-tags.full.json +0 -259
  148. package/src/core/aria-helpers.js +0 -1211
  149. package/src/core/contrast-helpers.js +0 -1302
  150. package/src/core/dom-helpers.js +0 -4493
  151. package/src/core/dom-runner.js +0 -787
  152. package/src/core/frame-messaging.js +0 -261
  153. package/src/core/frame-scan.js +0 -190
  154. package/src/core/rollup-composites.js +0 -127
  155. package/src/core/rule-meta.js +0 -176
  156. package/src/coverage/wcag-facets.js +0 -1079
  157. package/src/coverage/wcag-version-map.js +0 -84
  158. package/src/i18n/en.js +0 -1228
  159. package/src/i18n/fr.js +0 -1185
  160. package/src/policy/contracts.js +0 -18
  161. package/src/policy/resolvePolicy.js +0 -59
  162. package/src/policy/schemas/engine-options.schema.json +0 -103
  163. package/src/policy/schemas/policy-contract.schema.json +0 -40
@@ -1,261 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * postMessage-based RPC used to reach into cooperating child frames (same-
5
- * or cross-origin -- treated identically, exactly like a widely-used
6
- * reference engine's own runPartial/finishRun/frameMessenger protocol,
7
- * confirmed by reading that engine's _sendCommandToFrame/_collectResultsFromFrames
8
- * source directly rather than assuming). Used by frame-scan.js.
9
- *
10
- * Only matters for the "plain script injection" consumption mode (surea11y
11
- * loaded directly into a page with no automation driver -- see
12
- * docs/INTEGRATION.md's "Browser extension context" section). A Playwright-
13
- * driven scan doesn't need any of this: Playwright reaches cross-origin
14
- * frames unconditionally via CDP (see @surea11y/playwright's ROADMAP.md gap
15
- * #1), which is strictly better than what a cooperative postMessage protocol
16
- * can achieve. This exists for when there IS no automation driver.
17
- *
18
- * Zero free vars in each exported piece -- inlined into generated core.js
19
- * via inlineConstFunction (scripts/build-core.js), same as dom-helpers.js/
20
- * dom-runner.js/rule-meta.js. All functions here become sibling `const`
21
- * declarations in the same generated scope, so they may freely reference
22
- * each other (matching how resolveEffectiveRunOnly/ruleMatchesRunOnly/etc.
23
- * already do) -- but each is independently a zero-free-var function with
24
- * respect to anything OUTSIDE that shared generated scope.
25
- */
26
-
27
- const FRAME_RPC_CHANNEL = '__frame_rpc_v1__';
28
-
29
- function getFrameRpcRegistry(win) {
30
- if (!win.__a11yCoreFrameRpc__) {
31
- win.__a11yCoreFrameRpc__ = {
32
- pending: new Map(),
33
- seq: 0,
34
- responder: null,
35
- listening: false
36
- };
37
- }
38
- return win.__a11yCoreFrameRpc__;
39
- }
40
-
41
- function installFrameRpcListener(win, channel) {
42
- const registry = getFrameRpcRegistry(win);
43
- if (registry.listening) return registry;
44
-
45
- win.addEventListener('message', function a11yCoreFrameRpcListener(event) {
46
- const data = event && event.data;
47
- if (!data || data.__a11ycore !== true || data.channel !== channel) return;
48
-
49
- if (data.type === 'ping') {
50
- try {
51
- event.source &&
52
- event.source.postMessage(
53
- { __a11ycore: true, channel: channel, requestId: data.requestId, type: 'pong' },
54
- '*'
55
- );
56
- } catch (e) {
57
- /* target gone/closed -- nothing to do */
58
- }
59
- return;
60
- }
61
-
62
- if (data.type === 'run') {
63
- const responder = registry.responder;
64
- if (typeof responder !== 'function') return; // no responder enabled here: unreachable, same as a widely-used reference engine's own limitation
65
- Promise.resolve()
66
- .then(function () {
67
- return responder(data.payload);
68
- })
69
- .then(function (result) {
70
- try {
71
- event.source &&
72
- event.source.postMessage(
73
- {
74
- __a11ycore: true,
75
- channel: channel,
76
- requestId: data.requestId,
77
- type: 'result',
78
- payload: result
79
- },
80
- '*'
81
- );
82
- } catch (e) {
83
- /* target gone/closed */
84
- }
85
- })
86
- .catch(function (err) {
87
- try {
88
- event.source &&
89
- event.source.postMessage(
90
- {
91
- __a11ycore: true,
92
- channel: channel,
93
- requestId: data.requestId,
94
- type: 'error',
95
- payload: String(err && err.message ? err.message : err)
96
- },
97
- '*'
98
- );
99
- } catch (e) {
100
- /* target gone/closed */
101
- }
102
- });
103
- return;
104
- }
105
-
106
- // 'pong' | 'result' | 'error' -- resolve whichever pending request this replies to.
107
- const pending = registry.pending.get(data.requestId);
108
- if (!pending) return;
109
- if (data.type === 'pong') {
110
- pending.onPong();
111
- return;
112
- }
113
- registry.pending.delete(data.requestId);
114
- if (data.type === 'result') pending.resolve(data.payload);
115
- else
116
- pending.reject(
117
- new Error(typeof data.payload === 'string' ? data.payload : 'surea11y frame RPC error')
118
- );
119
- });
120
-
121
- registry.listening = true;
122
- return registry;
123
- }
124
-
125
- function nextFrameRpcRequestId(win) {
126
- const registry = getFrameRpcRegistry(win);
127
- registry.seq += 1;
128
- return (
129
- 'req_' +
130
- Date.now().toString(36) +
131
- '_' +
132
- registry.seq +
133
- '_' +
134
- Math.random().toString(36).slice(2, 8)
135
- );
136
- }
137
-
138
- /**
139
- * Pings a target frame's window; resolves true if a cooperating surea11y
140
- * responder answers within pingWaitTime (default 500ms, matching a widely-used
141
- * reference engine's own default), false otherwise. Never rejects -- "not reachable" is a normal,
142
- * expected outcome (most iframes on the web have no surea11y loaded at
143
- * all), not an error.
144
- */
145
- function pingFrame(win, targetWindow, pingWaitTime) {
146
- installFrameRpcListener(win, FRAME_RPC_CHANNEL);
147
- const registry = getFrameRpcRegistry(win);
148
- const requestId = nextFrameRpcRequestId(win);
149
- const waitMs = typeof pingWaitTime === 'number' ? pingWaitTime : 500;
150
-
151
- return new Promise(function (resolve) {
152
- let settled = false;
153
- const timeout = setTimeout(function () {
154
- if (settled) return;
155
- settled = true;
156
- registry.pending.delete(requestId);
157
- resolve(false);
158
- }, waitMs);
159
-
160
- registry.pending.set(requestId, {
161
- onPong: function () {
162
- if (settled) return;
163
- settled = true;
164
- clearTimeout(timeout);
165
- registry.pending.delete(requestId);
166
- resolve(true);
167
- },
168
- resolve: function () {},
169
- reject: function () {}
170
- });
171
-
172
- try {
173
- targetWindow.postMessage(
174
- { __a11ycore: true, channel: FRAME_RPC_CHANNEL, requestId: requestId, type: 'ping' },
175
- '*'
176
- );
177
- } catch (e) {
178
- if (!settled) {
179
- settled = true;
180
- clearTimeout(timeout);
181
- registry.pending.delete(requestId);
182
- resolve(false);
183
- }
184
- }
185
- });
186
- }
187
-
188
- /**
189
- * Sends a 'run' command to a target frame's window (already confirmed
190
- * reachable via pingFrame) and resolves with its reply payload, or rejects
191
- * on timeout (default 60s, matching a widely-used reference engine's own frameWaitTime default) or an
192
- * explicit error reply.
193
- */
194
- function sendFrameRunCommand(win, targetWindow, payload, frameWaitTime) {
195
- installFrameRpcListener(win, FRAME_RPC_CHANNEL);
196
- const registry = getFrameRpcRegistry(win);
197
- const requestId = nextFrameRpcRequestId(win);
198
- const waitMs = typeof frameWaitTime === 'number' ? frameWaitTime : 60000;
199
-
200
- return new Promise(function (resolve, reject) {
201
- const timeout = setTimeout(function () {
202
- registry.pending.delete(requestId);
203
- reject(new Error('surea11y frame RPC timed out waiting for a run result'));
204
- }, waitMs);
205
-
206
- registry.pending.set(requestId, {
207
- onPong: function () {},
208
- resolve: function (result) {
209
- clearTimeout(timeout);
210
- resolve(result);
211
- },
212
- reject: function (err) {
213
- clearTimeout(timeout);
214
- reject(err);
215
- }
216
- });
217
-
218
- try {
219
- targetWindow.postMessage(
220
- {
221
- __a11ycore: true,
222
- channel: FRAME_RPC_CHANNEL,
223
- requestId: requestId,
224
- type: 'run',
225
- payload: payload
226
- },
227
- '*'
228
- );
229
- } catch (e) {
230
- clearTimeout(timeout);
231
- registry.pending.delete(requestId);
232
- reject(e);
233
- }
234
- });
235
- }
236
-
237
- /**
238
- * Registers this window as reachable by a parent frame's scan: `handler`
239
- * receives the run command's payload and must return a result (or a
240
- * Promise of one). Returns a disable() function that removes the responder
241
- * (the message listener itself stays installed -- harmless/idle -- so a
242
- * later re-enable doesn't need to re-attach it).
243
- */
244
- function enableFrameRpcResponder(win, handler) {
245
- installFrameRpcListener(win, FRAME_RPC_CHANNEL);
246
- const registry = getFrameRpcRegistry(win);
247
- registry.responder = handler;
248
- return function disable() {
249
- if (registry.responder === handler) registry.responder = null;
250
- };
251
- }
252
-
253
- module.exports = {
254
- FRAME_RPC_CHANNEL,
255
- getFrameRpcRegistry,
256
- installFrameRpcListener,
257
- nextFrameRpcRequestId,
258
- pingFrame,
259
- sendFrameRunCommand,
260
- enableFrameRpcResponder
261
- };
@@ -1,190 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Cross-frame orchestration for the "plain script injection" consumption
5
- * mode (surea11y loaded directly into a page with no automation driver --
6
- * see docs/INTEGRATION.md's "Browser extension context" section). A
7
- * Playwright-driven scan doesn't need any of this (see
8
- * @surea11y/playwright's ROADMAP.md gap #1 -- CDP-level frame access is
9
- * unconditional, strictly better than what a cooperative protocol like this
10
- * one can achieve). This exists for when there is no automation driver, the
11
- * same situation a widely-used reference engine itself is built around.
12
- *
13
- * Inlined into generated core.js (via scripts/build-core.js), wrapped in its
14
- * OWN private IIFE together with its own local copies of CHECK_DEFS/
15
- * RULE_IMPLS/ENGINE_TAG/SCHEMA_VERSION/COMPOSITE_RULES and the shared
16
- * runnersSharedSource block (runCore, resolveEffectiveRunOnly,
17
- * resolveContextRoots, pingFrame, sendFrameRunCommand,
18
- * enableFrameRpcResponder, etc.) -- mirroring exactly how runa11yCoreInPage
19
- * itself achieves self-containment, and deliberately calling runCore(...)
20
- * directly here (not the sibling runa11yCoreInPage) so this stays
21
- * independent of the outer, Node-require-based RULE_IMPLS section. That
22
- * independence matters concretely: it's what lets these two functions be
23
- * used the exact same bundler-free way runa11yCoreInPage already is (raw
24
- * source injected into a page, e.g. a bookmarklet or a content script with
25
- * no build step) rather than requiring a real bundler to resolve `require()`
26
- * calls first. References runCore/resolveContextRoots/resolveEffectiveRunOnly/
27
- * pingFrame/sendFrameRunCommand/enableFrameRpcResponder/CHECK_DEFS/
28
- * RULE_IMPLS/ENGINE_TAG/SCHEMA_VERSION/COMPOSITE_RULES as free vars,
29
- * satisfied by that wrapping. Not requireable/testable in isolation for
30
- * that reason (same as dom-runner.js) -- test via the generated core.js
31
- * bundle instead.
32
- */
33
-
34
- /* global runCore, resolveContextRoots, resolveEffectiveRunOnly, pingFrame,
35
- sendFrameRunCommand, enableFrameRpcResponder, CHECK_DEFS, RULE_IMPLS,
36
- ENGINE_TAG, SCHEMA_VERSION, COMPOSITE_RULES */
37
-
38
- function findChildFrameElements(roots) {
39
- const seen = new Set();
40
- const out = [];
41
- for (const root of roots) {
42
- if (!root || typeof root.querySelectorAll !== 'function') continue;
43
- let matches;
44
- try {
45
- matches = root.querySelectorAll('iframe, frame');
46
- } catch (e) {
47
- matches = [];
48
- }
49
- for (const el of matches) {
50
- if (el && !seen.has(el)) {
51
- seen.add(el);
52
- out.push(el);
53
- }
54
- }
55
- }
56
- return out;
57
- }
58
-
59
- function getFrameElementUrl(el) {
60
- try {
61
- if (el.contentWindow && el.contentWindow.location && el.contentWindow.location.href) {
62
- return el.contentWindow.location.href;
63
- }
64
- } catch (e) {
65
- // Cross-origin: reading contentWindow.location.href itself throws. Fall
66
- // back to the authored src attribute (always readable, any origin).
67
- }
68
- return el.getAttribute ? el.getAttribute('src') || null : null;
69
- }
70
-
71
- /**
72
- * Scans the current frame, then attempts to reach every direct child
73
- * <iframe>/<frame> within the same scan scope via the frame RPC protocol
74
- * (src/core/frame-messaging.js). A child that doesn't respond (no
75
- * cooperating surea11y loaded and enabled there via
76
- * a11yCoreEnableFrameResponder() -- the common case for most third-party
77
- * embeds, and the same real limitation a widely-used reference engine itself has for
78
- * non-cooperating frames) is reported as { url, error } rather than
79
- * aborting the scan, matching the non-fatal-per-frame philosophy already
80
- * established for the Playwright binding's .frames(true). A child that
81
- * does respond replies with its OWN complete { topFrame, frames } result,
82
- * recursively including ITS OWN nested frames -- a tree, not a flat list
83
- * (unlike the Playwright binding: that binding can flatten because
84
- * Playwright's page.frames() already gives a flat list regardless of
85
- * nesting depth; a postMessage relay can't know about a grandchild without
86
- * asking through the child first).
87
- *
88
- * @returns {Promise<{ topFrame: object, frames: Array<{url:string|null, topFrame?:object, frames?:Array, error?:string}> }>}
89
- */
90
- function runa11yCoreAcrossFrames(pageUrl, contextSelector, engineOptions, runOnly) {
91
- const topFrame = runCore(
92
- pageUrl,
93
- contextSelector,
94
- engineOptions,
95
- resolveEffectiveRunOnly(engineOptions, runOnly),
96
- CHECK_DEFS,
97
- RULE_IMPLS,
98
- ENGINE_TAG,
99
- SCHEMA_VERSION,
100
- COMPOSITE_RULES
101
- );
102
-
103
- const eo = engineOptions && typeof engineOptions === 'object' ? engineOptions : {};
104
- const pingWaitTime = typeof eo.pingWaitTime === 'number' ? eo.pingWaitTime : undefined;
105
- const frameWaitTime = typeof eo.frameWaitTime === 'number' ? eo.frameWaitTime : undefined;
106
-
107
- const { roots } = resolveContextRoots(document, contextSelector);
108
- const frameElements = findChildFrameElements(roots);
109
-
110
- const framePromises = frameElements.map(function (el) {
111
- const url = getFrameElementUrl(el);
112
- let targetWindow = null;
113
- try {
114
- targetWindow = el.contentWindow || null;
115
- } catch (e) {
116
- targetWindow = null;
117
- }
118
- if (!targetWindow) {
119
- return Promise.resolve({ url: url, error: 'frame has no accessible contentWindow' });
120
- }
121
-
122
- return pingFrame(window, targetWindow, pingWaitTime).then(function (reachable) {
123
- if (!reachable) {
124
- return {
125
- url: url,
126
- error:
127
- 'no surea11y frame responder detected (that frame never called a11yCoreEnableFrameResponder(), or has not finished loading yet)'
128
- };
129
- }
130
- return sendFrameRunCommand(
131
- window,
132
- targetWindow,
133
- { pageUrl: url, contextSelector: null, engineOptions: eo, runOnly: runOnly },
134
- frameWaitTime
135
- )
136
- .then(function (result) {
137
- return { url: url, topFrame: result.topFrame, frames: result.frames };
138
- })
139
- .catch(function (err) {
140
- return { url: url, error: String(err && err.message ? err.message : err) };
141
- });
142
- });
143
- });
144
-
145
- return Promise.all(framePromises).then(function (frames) {
146
- return { topFrame: topFrame, frames: frames };
147
- });
148
- }
149
-
150
- /**
151
- * Opt-in: makes the CURRENT window reachable by a parent frame's
152
- * runa11yCoreAcrossFrames() call. A page calls this once (e.g. right after
153
- * loading surea11y) to become scannable from above. Deliberately a
154
- * separate, explicit call rather than an automatic side effect of loading
155
- * surea11y's code -- unlike a widely-used reference engine, whose mere
156
- * presence as a loaded <script> makes it listen automatically. surea11y's distribution model
157
- * (a function you call, not a script tag with load-time side effects)
158
- * doesn't have an equivalent "just including it" moment, and an explicit
159
- * opt-in is a clearer consent point besides -- it means every Node/jsdom
160
- * consumer that merely requires the module never gets a phantom
161
- * `window.addEventListener` they didn't ask for.
162
- *
163
- * The incoming run command's own engineOptions/runOnly are always used
164
- * as-is (matching a widely-used reference engine's own behavior: the parent's
165
- * request carries the options, the child just executes with them, no local
166
- * override) -- there's no origin/identity check on the sender beyond the
167
- * namespaced message envelope itself, matching that reference engine's own permissiveness here
168
- * (running a read-only scan and replying with DOM-derived results isn't a
169
- * privileged operation; the DOM content involved is no more sensitive than
170
- * what's already rendered on the page).
171
- *
172
- * @returns {function(): void} disable() -- stops responding to future scans
173
- */
174
- function a11yCoreEnableFrameResponder() {
175
- return enableFrameRpcResponder(window, function (payload) {
176
- return runa11yCoreAcrossFrames(
177
- payload ? payload.pageUrl : null,
178
- payload ? payload.contextSelector : null,
179
- payload ? payload.engineOptions : {},
180
- payload ? payload.runOnly : null
181
- );
182
- });
183
- }
184
-
185
- module.exports = {
186
- findChildFrameElements,
187
- getFrameElementUrl,
188
- runa11yCoreAcrossFrames,
189
- a11yCoreEnableFrameResponder
190
- };
@@ -1,127 +0,0 @@
1
- 'use strict';
2
-
3
- const LEVEL_RANK = Object.freeze({ A: 1, AA: 2, AAA: 3 });
4
-
5
- function normalizeScanLevel(scanLevel) {
6
- if (scanLevel === 'A' || scanLevel === 'AA' || scanLevel === 'AAA') return scanLevel;
7
- return null;
8
- }
9
-
10
- /**
11
- * Try to infer scan level deterministically from runOnly.tags if provided by the caller.
12
- * (If you already pass scanLevel explicitly, you can ignore this.)
13
- */
14
- function inferScanLevelFromRunOnly(runOnly) {
15
- const tags = runOnly && Array.isArray(runOnly.tags) ? runOnly.tags : null;
16
- if (!tags || tags.length === 0) return null;
17
-
18
- // Common reference-engine-style tags; adjust if your app uses different ones.
19
- if (tags.includes('wcag2aaa') || tags.includes('wcag22aaa')) return 'AAA';
20
- if (tags.includes('wcag2aa') || tags.includes('wcag22aa')) return 'AA';
21
- if (tags.includes('wcag2a') || tags.includes('wcag22a')) return 'A';
22
-
23
- return null;
24
- }
25
-
26
- function isWithinTargetLevel(compositeLevel, scanLevel) {
27
- const c = LEVEL_RANK[compositeLevel];
28
- const s = LEVEL_RANK[scanLevel];
29
- if (!c || !s) return false;
30
- return c <= s;
31
- }
32
-
33
- function aggregateOutcome(childOutcomes) {
34
- // Deterministic priority: fail > cantTell > pass > notApplicable
35
- let hasPass = false;
36
- let hasCantTell = false;
37
-
38
- for (const o of childOutcomes) {
39
- if (o === 'fail') return 'fail';
40
- if (o === 'cantTell') hasCantTell = true;
41
- else if (o === 'pass') hasPass = true;
42
- }
43
-
44
- if (hasCantTell) return 'cantTell';
45
- if (hasPass) return 'pass';
46
- return 'notApplicable';
47
- }
48
-
49
- /**
50
- * Roll up atomic rule results into composite (SC) results, filtered by target level.
51
- *
52
- * Inputs are plain JS objects/arrays; function never throws and is deterministic.
53
- */
54
- function rollupComposites({ atomicResults, composites, scanLevel, runOnly }) {
55
- try {
56
- const target = normalizeScanLevel(scanLevel) || inferScanLevelFromRunOnly(runOnly) || 'AAA'; // safest default: if unspecified, do not hide anything
57
-
58
- const atomicByRuleId = Object.create(null);
59
- if (Array.isArray(atomicResults)) {
60
- for (const r of atomicResults) {
61
- if (!r || typeof r.ruleId !== 'string') continue;
62
- atomicByRuleId[r.ruleId] = r;
63
- }
64
- }
65
-
66
- const out = [];
67
- const list = Array.isArray(composites) ? composites : [];
68
- for (const c of list) {
69
- const level = c && c.meta && c.meta.level;
70
- if (!isWithinTargetLevel(level, target)) continue;
71
-
72
- const checksIds = Array.isArray(c.checksIds) ? c.checksIds : [];
73
- const childOutcomes = checksIds.map((id) => {
74
- const rr = atomicByRuleId[id];
75
- return rr && typeof rr.outcome === 'string' ? rr.outcome : 'notApplicable';
76
- });
77
-
78
- const outcome = aggregateOutcome(childOutcomes);
79
-
80
- // Deterministic i18n keys + structured details (matches your reporting schema expectations)
81
- const i18nKey =
82
- outcome === 'fail'
83
- ? 'composite.outcome.fail'
84
- : outcome === 'cantTell'
85
- ? 'composite.outcome.cantTell'
86
- : outcome === 'pass'
87
- ? 'composite.outcome.pass'
88
- : 'composite.outcome.notApplicable';
89
-
90
- out.push({
91
- ruleId: c.id,
92
- outcome,
93
- confidence: outcome === 'cantTell' ? 'low' : 'high',
94
- summaryKey: i18nKey, // resolved at build-time by your i18n layer
95
- i18nKey,
96
- i18nParams: {
97
- wcagSc: (c.meta && c.meta.wcagSc && c.meta.wcagSc[0]) || null,
98
- level: level || null,
99
- scanLevel: target
100
- },
101
- data: {
102
- details: {
103
- reasonCode:
104
- outcome === 'cantTell'
105
- ? 'oneOrMoreChecksCantTell'
106
- : outcome === 'fail'
107
- ? 'oneOrMoreChecksFailed'
108
- : outcome === 'pass'
109
- ? 'allApplicableChecksPassed'
110
- : 'allChecksNotApplicable',
111
- scanLevel: target,
112
- compositeLevel: level || null,
113
- checksIds,
114
- childOutcomes
115
- }
116
- }
117
- });
118
- }
119
-
120
- return out;
121
- } catch (e) {
122
- // no-throws guarantee
123
- return [];
124
- }
125
- }
126
-
127
- module.exports = { rollupComposites };