@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.
- package/CHANGELOG.md +48 -4
- package/LICENSE +373 -21
- package/README.md +70 -1
- package/bin/core.js +240 -11
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +80 -1
- package/docs/ENGINE_OPTIONS.md +4 -0
- package/docs/INTEGRATION.md +19 -1
- package/docs/OUTPUT_SCHEMA.md +2 -2
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +50 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +125 -29
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +50 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +39 -9
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +117 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
- package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +26 -7
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -12
- package/src/checks/manual/empty-table-header-manual.js +21 -14
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -12
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
- package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +35 -21
- package/src/checks/manual/landmark-unique-manual.js +73 -45
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +25 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +40 -23
- package/src/checks/manual/page-title-patterns-manual.js +87 -51
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +275 -62
- package/src/checks/manual/scope-attr-valid-manual.js +11 -9
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +11 -9
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1128 -823
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4176 -3886
- package/src/core/dom-runner.js +720 -596
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +71 -35
- package/src/core.js +37939 -29121
- package/src/i18n/en.js +1194 -887
- package/src/i18n/fr.js +1136 -793
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +502 -0
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* loaded directly into a page with no automation driver -- see
|
|
12
12
|
* docs/INTEGRATION.md's "Browser extension context" section). A Playwright-
|
|
13
13
|
* driven scan doesn't need any of this: Playwright reaches cross-origin
|
|
14
|
-
* frames unconditionally via CDP (see surea11y
|
|
14
|
+
* frames unconditionally via CDP (see @surea11y/playwright's ROADMAP.md gap
|
|
15
15
|
* #1), which is strictly better than what a cooperative postMessage protocol
|
|
16
16
|
* can achieve. This exists for when there IS no automation driver.
|
|
17
17
|
*
|
|
@@ -27,79 +27,112 @@
|
|
|
27
27
|
const FRAME_RPC_CHANNEL = '__frame_rpc_v1__';
|
|
28
28
|
|
|
29
29
|
function getFrameRpcRegistry(win) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
39
|
}
|
|
40
40
|
|
|
41
41
|
function installFrameRpcListener(win, channel) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
});
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
registry.listening = true;
|
|
122
|
+
return registry;
|
|
97
123
|
}
|
|
98
124
|
|
|
99
125
|
function nextFrameRpcRequestId(win) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
);
|
|
103
136
|
}
|
|
104
137
|
|
|
105
138
|
/**
|
|
@@ -110,43 +143,46 @@ function nextFrameRpcRequestId(win) {
|
|
|
110
143
|
* all), not an error.
|
|
111
144
|
*/
|
|
112
145
|
function pingFrame(win, targetWindow, pingWaitTime) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
});
|
|
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);
|
|
138
159
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 () {}
|
|
149
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
|
+
});
|
|
150
186
|
}
|
|
151
187
|
|
|
152
188
|
/**
|
|
@@ -156,31 +192,46 @@ function pingFrame(win, targetWindow, pingWaitTime) {
|
|
|
156
192
|
* explicit error reply.
|
|
157
193
|
*/
|
|
158
194
|
function sendFrameRunCommand(win, targetWindow, payload, frameWaitTime) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
});
|
|
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);
|
|
175
205
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
}
|
|
183
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
|
+
});
|
|
184
235
|
}
|
|
185
236
|
|
|
186
237
|
/**
|
|
@@ -191,20 +242,20 @@ function sendFrameRunCommand(win, targetWindow, payload, frameWaitTime) {
|
|
|
191
242
|
* later re-enable doesn't need to re-attach it).
|
|
192
243
|
*/
|
|
193
244
|
function enableFrameRpcResponder(win, handler) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
};
|
|
200
251
|
}
|
|
201
252
|
|
|
202
253
|
module.exports = {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
254
|
+
FRAME_RPC_CHANNEL,
|
|
255
|
+
getFrameRpcRegistry,
|
|
256
|
+
installFrameRpcListener,
|
|
257
|
+
nextFrameRpcRequestId,
|
|
258
|
+
pingFrame,
|
|
259
|
+
sendFrameRunCommand,
|
|
260
|
+
enableFrameRpcResponder
|
|
210
261
|
};
|
package/src/core/frame-scan.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* mode (surea11y loaded directly into a page with no automation driver --
|
|
6
6
|
* see docs/INTEGRATION.md's "Browser extension context" section). A
|
|
7
7
|
* Playwright-driven scan doesn't need any of this (see
|
|
8
|
-
* surea11y
|
|
8
|
+
* @surea11y/playwright's ROADMAP.md gap #1 -- CDP-level frame access is
|
|
9
9
|
* unconditional, strictly better than what a cooperative protocol like this
|
|
10
10
|
* one can achieve). This exists for when there is no automation driver, the
|
|
11
11
|
* same situation a widely-used reference engine itself is built around.
|
|
@@ -31,37 +31,41 @@
|
|
|
31
31
|
* bundle instead.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
/* global runCore, resolveContextRoots, resolveEffectiveRunOnly, pingFrame,
|
|
35
|
+
sendFrameRunCommand, enableFrameRpcResponder, CHECK_DEFS, RULE_IMPLS,
|
|
36
|
+
ENGINE_TAG, SCHEMA_VERSION, COMPOSITE_RULES */
|
|
37
|
+
|
|
34
38
|
function findChildFrameElements(roots) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
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
|
+
}
|
|
51
54
|
}
|
|
52
|
-
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
function getFrameElementUrl(el) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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).
|
|
60
|
+
try {
|
|
61
|
+
if (el.contentWindow && el.contentWindow.location && el.contentWindow.location.href) {
|
|
62
|
+
return el.contentWindow.location.href;
|
|
63
63
|
}
|
|
64
|
-
|
|
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;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
/**
|
|
@@ -84,60 +88,63 @@ function getFrameElementUrl(el) {
|
|
|
84
88
|
* @returns {Promise<{ topFrame: object, frames: Array<{url:string|null, topFrame?:object, frames?:Array, error?:string}> }>}
|
|
85
89
|
*/
|
|
86
90
|
function runa11yCoreAcrossFrames(pageUrl, contextSelector, engineOptions, runOnly) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
);
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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;
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
const { roots } = resolveContextRoots(document, contextSelector);
|
|
108
|
+
const frameElements = findChildFrameElements(roots);
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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) };
|
|
135
141
|
});
|
|
136
142
|
});
|
|
143
|
+
});
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
return Promise.all(framePromises).then(function (frames) {
|
|
146
|
+
return { topFrame: topFrame, frames: frames };
|
|
147
|
+
});
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
/**
|
|
@@ -165,14 +172,19 @@ function runa11yCoreAcrossFrames(pageUrl, contextSelector, engineOptions, runOnl
|
|
|
165
172
|
* @returns {function(): void} disable() -- stops responding to future scans
|
|
166
173
|
*/
|
|
167
174
|
function a11yCoreEnableFrameResponder() {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
});
|
|
176
183
|
}
|
|
177
184
|
|
|
178
|
-
module.exports = {
|
|
185
|
+
module.exports = {
|
|
186
|
+
findChildFrameElements,
|
|
187
|
+
getFrameElementUrl,
|
|
188
|
+
runa11yCoreAcrossFrames,
|
|
189
|
+
a11yCoreEnableFrameResponder
|
|
190
|
+
};
|