@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,210 +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 && event.source.postMessage(
52
- { __a11ycore: true, channel: channel, requestId: data.requestId, type: 'pong' },
53
- '*'
54
- );
55
- } catch (e) { /* target gone/closed -- nothing to do */ }
56
- return;
57
- }
58
-
59
- if (data.type === 'run') {
60
- const responder = registry.responder;
61
- if (typeof responder !== 'function') return; // no responder enabled here: unreachable, same as a widely-used reference engine's own limitation
62
- Promise.resolve()
63
- .then(function () { return responder(data.payload); })
64
- .then(function (result) {
65
- try {
66
- event.source && event.source.postMessage(
67
- { __a11ycore: true, channel: channel, requestId: data.requestId, type: 'result', payload: result },
68
- '*'
69
- );
70
- } catch (e) { /* target gone/closed */ }
71
- })
72
- .catch(function (err) {
73
- try {
74
- event.source && event.source.postMessage(
75
- { __a11ycore: true, channel: channel, requestId: data.requestId, type: 'error', payload: String(err && err.message ? err.message : err) },
76
- '*'
77
- );
78
- } catch (e) { /* target gone/closed */ }
79
- });
80
- return;
81
- }
82
-
83
- // 'pong' | 'result' | 'error' -- resolve whichever pending request this replies to.
84
- const pending = registry.pending.get(data.requestId);
85
- if (!pending) return;
86
- if (data.type === 'pong') {
87
- pending.onPong();
88
- return;
89
- }
90
- registry.pending.delete(data.requestId);
91
- if (data.type === 'result') pending.resolve(data.payload);
92
- else pending.reject(new Error(typeof data.payload === 'string' ? data.payload : 'surea11y frame RPC error'));
93
- });
94
-
95
- registry.listening = true;
96
- return registry;
97
- }
98
-
99
- function nextFrameRpcRequestId(win) {
100
- const registry = getFrameRpcRegistry(win);
101
- registry.seq += 1;
102
- return 'req_' + Date.now().toString(36) + '_' + registry.seq + '_' + Math.random().toString(36).slice(2, 8);
103
- }
104
-
105
- /**
106
- * Pings a target frame's window; resolves true if a cooperating surea11y
107
- * responder answers within pingWaitTime (default 500ms, matching a widely-used
108
- * reference engine's own default), false otherwise. Never rejects -- "not reachable" is a normal,
109
- * expected outcome (most iframes on the web have no surea11y loaded at
110
- * all), not an error.
111
- */
112
- function pingFrame(win, targetWindow, pingWaitTime) {
113
- installFrameRpcListener(win, FRAME_RPC_CHANNEL);
114
- const registry = getFrameRpcRegistry(win);
115
- const requestId = nextFrameRpcRequestId(win);
116
- const waitMs = typeof pingWaitTime === 'number' ? pingWaitTime : 500;
117
-
118
- return new Promise(function (resolve) {
119
- let settled = false;
120
- const timeout = setTimeout(function () {
121
- if (settled) return;
122
- settled = true;
123
- registry.pending.delete(requestId);
124
- resolve(false);
125
- }, waitMs);
126
-
127
- registry.pending.set(requestId, {
128
- onPong: function () {
129
- if (settled) return;
130
- settled = true;
131
- clearTimeout(timeout);
132
- registry.pending.delete(requestId);
133
- resolve(true);
134
- },
135
- resolve: function () {},
136
- reject: function () {}
137
- });
138
-
139
- try {
140
- targetWindow.postMessage({ __a11ycore: true, channel: FRAME_RPC_CHANNEL, requestId: requestId, type: 'ping' }, '*');
141
- } catch (e) {
142
- if (!settled) {
143
- settled = true;
144
- clearTimeout(timeout);
145
- registry.pending.delete(requestId);
146
- resolve(false);
147
- }
148
- }
149
- });
150
- }
151
-
152
- /**
153
- * Sends a 'run' command to a target frame's window (already confirmed
154
- * reachable via pingFrame) and resolves with its reply payload, or rejects
155
- * on timeout (default 60s, matching a widely-used reference engine's own frameWaitTime default) or an
156
- * explicit error reply.
157
- */
158
- function sendFrameRunCommand(win, targetWindow, payload, frameWaitTime) {
159
- installFrameRpcListener(win, FRAME_RPC_CHANNEL);
160
- const registry = getFrameRpcRegistry(win);
161
- const requestId = nextFrameRpcRequestId(win);
162
- const waitMs = typeof frameWaitTime === 'number' ? frameWaitTime : 60000;
163
-
164
- return new Promise(function (resolve, reject) {
165
- const timeout = setTimeout(function () {
166
- registry.pending.delete(requestId);
167
- reject(new Error('surea11y frame RPC timed out waiting for a run result'));
168
- }, waitMs);
169
-
170
- registry.pending.set(requestId, {
171
- onPong: function () {},
172
- resolve: function (result) { clearTimeout(timeout); resolve(result); },
173
- reject: function (err) { clearTimeout(timeout); reject(err); }
174
- });
175
-
176
- try {
177
- targetWindow.postMessage({ __a11ycore: true, channel: FRAME_RPC_CHANNEL, requestId: requestId, type: 'run', payload: payload }, '*');
178
- } catch (e) {
179
- clearTimeout(timeout);
180
- registry.pending.delete(requestId);
181
- reject(e);
182
- }
183
- });
184
- }
185
-
186
- /**
187
- * Registers this window as reachable by a parent frame's scan: `handler`
188
- * receives the run command's payload and must return a result (or a
189
- * Promise of one). Returns a disable() function that removes the responder
190
- * (the message listener itself stays installed -- harmless/idle -- so a
191
- * later re-enable doesn't need to re-attach it).
192
- */
193
- function enableFrameRpcResponder(win, handler) {
194
- installFrameRpcListener(win, FRAME_RPC_CHANNEL);
195
- const registry = getFrameRpcRegistry(win);
196
- registry.responder = handler;
197
- return function disable() {
198
- if (registry.responder === handler) registry.responder = null;
199
- };
200
- }
201
-
202
- module.exports = {
203
- FRAME_RPC_CHANNEL,
204
- getFrameRpcRegistry,
205
- installFrameRpcListener,
206
- nextFrameRpcRequestId,
207
- pingFrame,
208
- sendFrameRunCommand,
209
- enableFrameRpcResponder
210
- };
@@ -1,178 +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
- function findChildFrameElements(roots) {
35
- const seen = new Set();
36
- const out = [];
37
- for (const root of roots) {
38
- if (!root || typeof root.querySelectorAll !== 'function') continue;
39
- let matches = [];
40
- try {
41
- matches = root.querySelectorAll('iframe, frame');
42
- } catch (e) {
43
- matches = [];
44
- }
45
- for (const el of matches) {
46
- if (el && !seen.has(el)) {
47
- seen.add(el);
48
- out.push(el);
49
- }
50
- }
51
- }
52
- return out;
53
- }
54
-
55
- function getFrameElementUrl(el) {
56
- try {
57
- if (el.contentWindow && el.contentWindow.location && el.contentWindow.location.href) {
58
- return el.contentWindow.location.href;
59
- }
60
- } catch (e) {
61
- // Cross-origin: reading contentWindow.location.href itself throws. Fall
62
- // back to the authored src attribute (always readable, any origin).
63
- }
64
- return el.getAttribute ? (el.getAttribute('src') || null) : null;
65
- }
66
-
67
- /**
68
- * Scans the current frame, then attempts to reach every direct child
69
- * <iframe>/<frame> within the same scan scope via the frame RPC protocol
70
- * (src/core/frame-messaging.js). A child that doesn't respond (no
71
- * cooperating surea11y loaded and enabled there via
72
- * a11yCoreEnableFrameResponder() -- the common case for most third-party
73
- * embeds, and the same real limitation a widely-used reference engine itself has for
74
- * non-cooperating frames) is reported as { url, error } rather than
75
- * aborting the scan, matching the non-fatal-per-frame philosophy already
76
- * established for the Playwright binding's .frames(true). A child that
77
- * does respond replies with its OWN complete { topFrame, frames } result,
78
- * recursively including ITS OWN nested frames -- a tree, not a flat list
79
- * (unlike the Playwright binding: that binding can flatten because
80
- * Playwright's page.frames() already gives a flat list regardless of
81
- * nesting depth; a postMessage relay can't know about a grandchild without
82
- * asking through the child first).
83
- *
84
- * @returns {Promise<{ topFrame: object, frames: Array<{url:string|null, topFrame?:object, frames?:Array, error?:string}> }>}
85
- */
86
- function runa11yCoreAcrossFrames(pageUrl, contextSelector, engineOptions, runOnly) {
87
- const topFrame = runCore(
88
- pageUrl,
89
- contextSelector,
90
- engineOptions,
91
- resolveEffectiveRunOnly(engineOptions, runOnly),
92
- CHECK_DEFS,
93
- RULE_IMPLS,
94
- ENGINE_TAG,
95
- SCHEMA_VERSION,
96
- COMPOSITE_RULES
97
- );
98
-
99
- const eo = (engineOptions && typeof engineOptions === 'object') ? engineOptions : {};
100
- const pingWaitTime = typeof eo.pingWaitTime === 'number' ? eo.pingWaitTime : undefined;
101
- const frameWaitTime = typeof eo.frameWaitTime === 'number' ? eo.frameWaitTime : undefined;
102
-
103
- const { roots } = resolveContextRoots(document, contextSelector);
104
- const frameElements = findChildFrameElements(roots);
105
-
106
- const framePromises = frameElements.map(function (el) {
107
- const url = getFrameElementUrl(el);
108
- let targetWindow = null;
109
- try {
110
- targetWindow = el.contentWindow || null;
111
- } catch (e) {
112
- targetWindow = null;
113
- }
114
- if (!targetWindow) {
115
- return Promise.resolve({ url: url, error: 'frame has no accessible contentWindow' });
116
- }
117
-
118
- return pingFrame(window, targetWindow, pingWaitTime).then(function (reachable) {
119
- if (!reachable) {
120
- return {
121
- url: url,
122
- error: 'no surea11y frame responder detected (that frame never called a11yCoreEnableFrameResponder(), or has not finished loading yet)'
123
- };
124
- }
125
- return sendFrameRunCommand(
126
- window,
127
- targetWindow,
128
- { pageUrl: url, contextSelector: null, engineOptions: eo, runOnly: runOnly },
129
- frameWaitTime
130
- ).then(function (result) {
131
- return { url: url, topFrame: result.topFrame, frames: result.frames };
132
- }).catch(function (err) {
133
- return { url: url, error: String(err && err.message ? err.message : err) };
134
- });
135
- });
136
- });
137
-
138
- return Promise.all(framePromises).then(function (frames) {
139
- return { topFrame: topFrame, frames: frames };
140
- });
141
- }
142
-
143
- /**
144
- * Opt-in: makes the CURRENT window reachable by a parent frame's
145
- * runa11yCoreAcrossFrames() call. A page calls this once (e.g. right after
146
- * loading surea11y) to become scannable from above. Deliberately a
147
- * separate, explicit call rather than an automatic side effect of loading
148
- * surea11y's code -- unlike a widely-used reference engine, whose mere
149
- * presence as a loaded <script> makes it listen automatically. surea11y's distribution model
150
- * (a function you call, not a script tag with load-time side effects)
151
- * doesn't have an equivalent "just including it" moment, and an explicit
152
- * opt-in is a clearer consent point besides -- it means every Node/jsdom
153
- * consumer that merely requires the module never gets a phantom
154
- * `window.addEventListener` they didn't ask for.
155
- *
156
- * The incoming run command's own engineOptions/runOnly are always used
157
- * as-is (matching a widely-used reference engine's own behavior: the parent's
158
- * request carries the options, the child just executes with them, no local
159
- * override) -- there's no origin/identity check on the sender beyond the
160
- * namespaced message envelope itself, matching that reference engine's own permissiveness here
161
- * (running a read-only scan and replying with DOM-derived results isn't a
162
- * privileged operation; the DOM content involved is no more sensitive than
163
- * what's already rendered on the page).
164
- *
165
- * @returns {function(): void} disable() -- stops responding to future scans
166
- */
167
- function a11yCoreEnableFrameResponder() {
168
- return enableFrameRpcResponder(window, function (payload) {
169
- return runa11yCoreAcrossFrames(
170
- payload ? payload.pageUrl : null,
171
- payload ? payload.contextSelector : null,
172
- payload ? payload.engineOptions : {},
173
- payload ? payload.runOnly : null
174
- );
175
- });
176
- }
177
-
178
- module.exports = { findChildFrameElements, getFrameElementUrl, runa11yCoreAcrossFrames, a11yCoreEnableFrameResponder };
@@ -1,135 +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({
55
- atomicResults,
56
- composites,
57
- scanLevel,
58
- runOnly
59
- }) {
60
- try {
61
- const target =
62
- normalizeScanLevel(scanLevel) ||
63
- inferScanLevelFromRunOnly(runOnly) ||
64
- 'AAA'; // safest default: if unspecified, do not hide anything
65
-
66
- const atomicByRuleId = Object.create(null);
67
- if (Array.isArray(atomicResults)) {
68
- for (const r of atomicResults) {
69
- if (!r || typeof r.ruleId !== 'string') continue;
70
- atomicByRuleId[r.ruleId] = r;
71
- }
72
- }
73
-
74
- const out = [];
75
- const list = Array.isArray(composites) ? composites : [];
76
- for (const c of list) {
77
- const level = c && c.meta && c.meta.level;
78
- if (!isWithinTargetLevel(level, target)) continue;
79
-
80
- const checksIds = Array.isArray(c.checksIds) ? c.checksIds : [];
81
- const childOutcomes = checksIds.map((id) => {
82
- const rr = atomicByRuleId[id];
83
- return rr && typeof rr.outcome === 'string' ? rr.outcome : 'notApplicable';
84
- });
85
-
86
- const outcome = aggregateOutcome(childOutcomes);
87
-
88
- // Deterministic i18n keys + structured details (matches your reporting schema expectations)
89
- const i18nKey =
90
- outcome === 'fail'
91
- ? 'composite.outcome.fail'
92
- : outcome === 'cantTell'
93
- ? 'composite.outcome.cantTell'
94
- : outcome === 'pass'
95
- ? 'composite.outcome.pass'
96
- : 'composite.outcome.notApplicable';
97
-
98
- out.push({
99
- ruleId: c.id,
100
- outcome,
101
- confidence: outcome === 'cantTell' ? 'low' : 'high',
102
- summaryKey: i18nKey, // resolved at build-time by your i18n layer
103
- i18nKey,
104
- i18nParams: {
105
- wcagSc: (c.meta && c.meta.wcagSc && c.meta.wcagSc[0]) || null,
106
- level: level || null,
107
- scanLevel: target
108
- },
109
- data: {
110
- details: {
111
- reasonCode:
112
- outcome === 'cantTell'
113
- ? 'oneOrMoreChecksCantTell'
114
- : outcome === 'fail'
115
- ? 'oneOrMoreChecksFailed'
116
- : outcome === 'pass'
117
- ? 'allApplicableChecksPassed'
118
- : 'allChecksNotApplicable',
119
- scanLevel: target,
120
- compositeLevel: level || null,
121
- checksIds,
122
- childOutcomes
123
- }
124
- }
125
- });
126
- }
127
-
128
- return out;
129
- } catch (e) {
130
- // no-throws guarantee
131
- return [];
132
- }
133
- }
134
-
135
- module.exports = { rollupComposites };
@@ -1,159 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Normalizes a rule module's `meta` export into the stable shape CHECK_DEFS
5
- * entries use everywhere else (build-time rules and runtime-registered
6
- * custom rules alike).
7
- *
8
- * Zero free vars (besides its own params) -- this gets inlined into the
9
- * generated core.js runtime via inlineConstFunction, the same mechanism
10
- * dom-helpers.js/dom-runner.js use, so it must stay self-contained/embeddable
11
- * via .toString(). engineTag is a param (not a closed-over module constant)
12
- * for exactly that reason.
13
- */
14
- function normalizeRuleMeta(ruleId, id, meta, engineTag) {
15
- function normalizeStringArray(value) {
16
- if (!Array.isArray(value)) return [];
17
- return value.map(String).map((s) => s.trim()).filter(Boolean);
18
- }
19
-
20
- function normalizeObjectArray(value) {
21
- if (!Array.isArray(value)) return [];
22
- return value
23
- .filter((v) => v && typeof v === 'object' && !Array.isArray(v))
24
- .map((v) => ({ ...v }));
25
- }
26
-
27
- function deriveWcagScFromNormativeMappings(normativeMappings) {
28
- const nm = Array.isArray(normativeMappings) ? normativeMappings : [];
29
- const out = new Set();
30
- for (const m of nm) {
31
- if (!m || typeof m !== 'object') continue;
32
- if (String(m.standard || '').toUpperCase() !== 'WCAG') continue;
33
- const req = String(m.requirement || '').trim();
34
- if (req) out.add(req);
35
- }
36
- return Array.from(out).sort();
37
- }
38
-
39
- const m = (meta && typeof meta === 'object') ? meta : {};
40
-
41
- const title = (typeof m.title === 'string' && m.title.trim()) ? m.title.trim() : id;
42
- const description = (typeof m.description === 'string') ? m.description : '';
43
- const helpUrl = (typeof m.helpUrl === 'string') ? m.helpUrl : '';
44
-
45
- const i18n = (m.i18n && typeof m.i18n === 'object' && !Array.isArray(m.i18n))
46
- ? { ...m.i18n }
47
- : null;
48
-
49
- const tags = normalizeStringArray(m.tags).map((t) => t.toLowerCase());
50
- if (!tags.includes(engineTag)) tags.push(engineTag);
51
-
52
- const normativeMappings = normalizeObjectArray(m.normativeMappings);
53
- const wcagSc = deriveWcagScFromNormativeMappings(normativeMappings);
54
- const informativeReferences = normalizeObjectArray(m.informativeReferences);
55
-
56
- const defaultSeverity = (typeof m.defaultSeverity === 'string' && m.defaultSeverity.trim())
57
- ? m.defaultSeverity.trim()
58
- : 'moderate';
59
-
60
- const defaultConfidence = (typeof m.defaultConfidence === 'string' && m.defaultConfidence.trim())
61
- ? m.defaultConfidence.trim()
62
- : 'medium';
63
-
64
- const type = (m.type === 'manual' || m.type === 'automatic')
65
- ? m.type
66
- : 'automatic';
67
-
68
- const coverage = (m.coverage === null || typeof m.coverage === 'string' || typeof m.coverage === 'object')
69
- ? m.coverage
70
- : null;
71
-
72
- const ruleInterfaceVersion = (typeof m.ruleInterfaceVersion === 'string' && m.ruleInterfaceVersion.trim())
73
- ? m.ruleInterfaceVersion.trim()
74
- : '1.0.0';
75
-
76
- const ruleVersion = (typeof m.ruleVersion === 'string' && m.ruleVersion.trim())
77
- ? m.ruleVersion.trim()
78
- : '0.0.0';
79
-
80
- const normative = (typeof m.normative === 'boolean') ? m.normative : true;
81
- const atomic = (typeof m.atomic === 'boolean') ? m.atomic : true;
82
-
83
- // Deprecation signal for the rule catalog (see docs/API_STABILITY.md).
84
- // Purely informational -- a deprecated rule still runs and produces
85
- // results completely normally; this is a catalog-level migration signal
86
- // for integrators, not an automatic exclusion.
87
- const deprecated = (typeof m.deprecated === 'boolean') ? m.deprecated : false;
88
- const deprecation = (deprecated && m.deprecation && typeof m.deprecation === 'object' && !Array.isArray(m.deprecation))
89
- ? {
90
- replacedBy: (typeof m.deprecation.replacedBy === 'string' && m.deprecation.replacedBy.trim()) ? m.deprecation.replacedBy.trim() : null,
91
- reason: (typeof m.deprecation.reason === 'string') ? m.deprecation.reason.trim() : '',
92
- sinceVersion: (typeof m.deprecation.sinceVersion === 'string') ? m.deprecation.sinceVersion.trim() : ''
93
- }
94
- : null;
95
-
96
- if (deprecated && (!deprecation || !deprecation.reason || !deprecation.sinceVersion)) {
97
- throw new Error(`Rule ${ruleId}: meta.deprecated:true requires meta.deprecation.reason and meta.deprecation.sinceVersion`);
98
- }
99
-
100
- const category = (typeof m.category === 'string' && m.category.trim()) ? m.category.trim() : null;
101
- const standard = (typeof m.standard === 'string' && m.standard.trim()) ? m.standard.trim() : null;
102
-
103
- const applicability = (typeof m.applicability === 'string') ? m.applicability : '';
104
- const expectation = (typeof m.expectation === 'string') ? m.expectation : '';
105
-
106
- const references = Array.isArray(m.references) ? m.references.slice() : [];
107
- const requirements = (m.requirements === null || typeof m.requirements === 'string' || typeof m.requirements === 'object')
108
- ? m.requirements
109
- : null;
110
-
111
- const mappings = (m.mappings === null || typeof m.mappings === 'string' || typeof m.mappings === 'object')
112
- ? m.mappings
113
- : null;
114
-
115
- if (!Array.isArray(tags)) throw new Error(`Rule ${ruleId}: meta.tags must be an array`);
116
- if (!Array.isArray(normativeMappings)) throw new Error(`Rule ${ruleId}: meta.normativeMappings must be an array`);
117
- if (!Array.isArray(informativeReferences)) throw new Error(`Rule ${ruleId}: meta.informativeReferences must be an array`);
118
- if (type !== 'automatic' && type !== 'manual') throw new Error(`Rule ${ruleId}: meta.type must be "automatic" or "manual"`);
119
-
120
- if (i18n) {
121
- if (typeof i18n.titleKey !== 'string' || !i18n.titleKey.trim()) {
122
- throw new Error(`Rule ${ruleId}: meta.i18n.titleKey must be a non-empty string`);
123
- }
124
- if (i18n.descriptionKey != null && (typeof i18n.descriptionKey !== 'string' || !i18n.descriptionKey.trim())) {
125
- throw new Error(`Rule ${ruleId}: meta.i18n.descriptionKey must be a non-empty string when provided`);
126
- }
127
- }
128
-
129
- return {
130
- title,
131
- description,
132
- i18n,
133
- helpUrl,
134
- tags,
135
- wcagSc,
136
- normativeMappings,
137
- informativeReferences,
138
- defaultSeverity,
139
- defaultConfidence,
140
- type,
141
- coverage,
142
-
143
- ruleInterfaceVersion,
144
- ruleVersion,
145
- normative,
146
- atomic,
147
- deprecated,
148
- deprecation,
149
- category,
150
- standard,
151
- applicability,
152
- expectation,
153
- references,
154
- requirements,
155
- mappings
156
- };
157
- }
158
-
159
- module.exports = { normalizeRuleMeta };