Skip to content

Commit 602fdcd

Browse files
ptomatomoz-wptsync-bot
authored andcommitted
Bug 1930534 [wpt PR 49108] - Run ShadowRealm tests in multiple scopes, a=testonly
Automatic update from web-platform-tests Change 'shadowrealm' global into a shorthand for all possible ShadowRealm scopes In order to automatically run tests not only in a ShadowRealm created in a window scope, but also in ShadowRealms created in other realms, change the 'shadowrealm' global type to a collection, and rename the existing ShadowRealm handler to 'shadowrealm-in-window'. -- Remove monkeypatch of globalThis.self in ShadowRealm As per whatwg/html#9893, ShadowRealmGlobalScope should have a `self` attribute already. There is no need to monkeypatch it for the test harness. -- Factor out JS code that will be common to multiple ShadowRealm handlers We will add multiple ShadowRealm handlers, and they will all need to set up certain global properties. Avoid repeating this code in each handler as well as in idlharness-shadowrealm.js. This should also increase readability, which is good since the ShadowRealm setup code can be confusing. -- Add 'shadowrealm-in-shadowrealm' global This will add to any test with global=shadowrealm in its metadata, an .any.shadowrealm-in-shadowrealm.html variant. The test wrapper creates an outer ShadowRealm, which creates an inner ShadowRealm and runs the tests inside that, relaying the results through the outer ShadowRealm. -- Add 'shadowrealm-in-dedicatedworker' global This will add to any test with global=shadowrealm in its metadata, an .any.shadowrealm-in-dedicatedworker.html variant. The test loads an intermediate .any.worker-shadowrealm.js wrapper into a Worker, and forwards the message port to the Worker's message port so that fetch_tests_from_worker can receive the results. -- Add 'shadowrealm-in-sharedworker' global This will add to any test with global=shadowrealm in its metadata, an .any.shadowrealm-in-sharedworker.html variant. The test loads the same intermediate .any.worker-shadowrealm.js wrapper as .any.shadowrealm-in-dedicatedworker.html, but populates a 'port' variable with the port received from the connect event, instead of calling the global postMessage since that won't work in a SharedWorker. -- Add 'shadowrealm-in-serviceworker' global This will add to any test with global=shadowrealm in its metadata, an .any.shadowrealm-in-serviceworker.html variant. We have to use a slightly different .any.serviceworker-shadowrealm.js wrapper from the wrapper used for the other types of workers, because dynamic import() is forbidden in ServiceWorker scopes. Instead, add a utility function to set up a fakeDynamicImport() function inside the ShadowRealm which uses the fetch adaptor to get the module's source text and evaluate it in the shadowRealm. Also add a case for ServiceWorkers to getPostMessageFunc(), which returns a postMessage() drop-in replacement that broadcasts the message to all clients, since test result messages from the ShadowRealm are not in response to any particular message received by the ServiceWorker. Note '.https.' needs to be added to the test path. -- Add 'shadowrealm-in-audioworklet' global This will add to any test with global=shadowrealm in its metadata, an .any.shadowrealm-in-audioworklet.html variant. The wrapper here is similar to the one for ServiceWorkers, since dynamic import() is also forbidden in worklet scopes. But additionally fetch() is not exposed, so we add a utility function to set up the ability to call the window realm's fetch() through the AudioWorklet's message port. We also add /resources/testharness-shadowrealm-audioworkletprocessor.js to contain most of the AudioWorklet setup boilerplate, so that it isn't written inline in serve.py. Note '.https.' needs to be added to the test path. -- wpt-commits: 9c8db8af89efbe0f67b215af2a6b49e9564e2971, 65a205aea5d02ff5bea7b1a0579287035d02d6c4, eb9c8e7259ef8bd5cca5019c1ca15ccd430e81dc, 3a20c56893472783b5e20c0d61cbb7b7b278cc6d, 7d8458ed291b139307430a102180c9a617d7876e, 42160ae827c863ac6787c8451fe377901c8f0652, 59367bb21d053abb9ed6de3cca5409486816acc9, 60d6c48e5fa76876bc3924b9d6185dfb56c9ab1c wpt-pr: 49108
1 parent 4f8b7d5 commit 602fdcd

19 files changed

+491
-61
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
// META: script=/resources/testharness-shadowrealm-outer.js
12
// META: script=/resources/idlharness-shadowrealm.js
23
idl_test_shadowrealm(["compression"], ["streams"]);

testing/web-platform/tests/console/idlharness-shadowrealm.window.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// META: script=/resources/testharness-shadowrealm-outer.js
12
// META: script=/resources/idlharness-shadowrealm.js
23

34
// https://console.spec.whatwg.org/

testing/web-platform/tests/docs/writing-tests/testharness.md

+18-2
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,25 @@ are:
167167
* `jsshell`: to be run in a JavaScript shell, without access to the DOM
168168
(currently only supported in SpiderMonkey, and skipped in wptrunner)
169169
* `worker`: shorthand for the dedicated, shared, and service worker scopes
170-
* `shadowrealm`: runs the test code in a
170+
* `shadowrealm-in-window`: runs the test code in a
171171
[ShadowRealm](https://github.com/tc39/proposal-shadowrealm) context hosted in
172-
an ordinary Window context; to be run at <code><var>x</var>.any.shadowrealm.html</code>
172+
an ordinary Window context; to be run at <code><var>x</var>.any.shadowrealm-in-window.html</code>
173+
* `shadowrealm-in-shadowrealm`: runs the test code in a ShadowRealm context
174+
hosted in another ShadowRealm context; to be run at
175+
<code><var>x</var>.any.shadowrealm-in-shadowrealm.html</code>
176+
* `shadowrealm-in-dedicatedworker`: runs the test code in a ShadowRealm context
177+
hosted in a dedicated worker; to be run at
178+
<code><var>x</var>.any.shadowrealm-in-dedicatedworker.html</code>
179+
* `shadowrealm-in-sharedworker`: runs the test code in a ShadowRealm context
180+
hosted in a shared worker; to be run at
181+
<code><var>x</var>.any.shadowrealm-in-sharedworker.html</code>
182+
* `shadowrealm-in-serviceworker`: runs the test code in a ShadowRealm context
183+
hosted in a service worker; to be run at
184+
<code><var>x</var>.https.any.shadowrealm-in-serviceworker.html</code>
185+
* `shadowrealm-in-audioworklet`: runs the test code in a ShadowRealm context
186+
hosted in an AudioWorklet processor; to be run at
187+
<code><var>x</var>.https.any.shadowrealm-in-audioworklet.html</code>
188+
* `shadowrealm`: shorthand for all of the ShadowRealm scopes
173189

174190
To check what scope your test is run from, you can use the following methods that will
175191
be made available by the framework:
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
// META: script=/resources/testharness-shadowrealm-outer.js
12
// META: script=/resources/idlharness-shadowrealm.js
23
idl_test_shadowrealm(["dom"], ["html"]);
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
// META: script=/resources/testharness-shadowrealm-outer.js
12
// META: script=/resources/idlharness-shadowrealm.js
23
idl_test_shadowrealm(["encoding"], ["streams"]);
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
// META: script=/resources/testharness-shadowrealm-outer.js
12
// META: script=/resources/idlharness-shadowrealm.js
23
idl_test_shadowrealm(["hr-time"], ["html", "dom"]);
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
// META: script=/resources/testharness-shadowrealm-outer.js
12
// META: script=/resources/idlharness-shadowrealm.js
23
idl_test_shadowrealm(["html"], ["wai-aria", "SVG", "cssom", "touch-events", "uievents", "dom", "xhr", "FileAPI", "mediacapture-streams", "performance-timeline"]);
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
// META: script=/resources/testharness-shadowrealm-outer.js
12
// META: script=/resources/idlharness-shadowrealm.js
23
idl_test_shadowrealm(["performance-timeline"], ["hr-time", "dom"]);

testing/web-platform/tests/resources/idlharness-shadowrealm.js

+19-28
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/* global shadowRealmEvalAsync */
2+
3+
// requires /resources/idlharness-shadowrealm-outer.js
4+
15
// TODO: it would be nice to support `idl_array.add_objects`
26
function fetch_text(url) {
37
return fetch(url).then(function (r) {
@@ -23,38 +27,25 @@ function fetch_text(url) {
2327
function idl_test_shadowrealm(srcs, deps) {
2428
promise_setup(async t => {
2529
const realm = new ShadowRealm();
26-
// https://github.com/web-platform-tests/wpt/issues/31996
27-
realm.evaluate("globalThis.self = globalThis; undefined;");
28-
29-
realm.evaluate(`
30-
globalThis.self.GLOBAL = {
31-
isWindow: function() { return false; },
32-
isWorker: function() { return false; },
33-
isShadowRealm: function() { return true; },
34-
}; undefined;
35-
`);
3630
const specs = await Promise.all(srcs.concat(deps).map(spec => {
3731
return fetch_text("/interfaces/" + spec + ".idl");
3832
}));
3933
const idls = JSON.stringify(specs);
40-
await new Promise(
41-
realm.evaluate(`(resolve,reject) => {
42-
(async () => {
43-
await import("/resources/testharness.js");
44-
await import("/resources/WebIDLParser.js");
45-
await import("/resources/idlharness.js");
46-
const idls = ${idls};
47-
const idl_array = new IdlArray();
48-
for (let i = 0; i < ${srcs.length}; i++) {
49-
idl_array.add_idls(idls[i]);
50-
}
51-
for (let i = ${srcs.length}; i < ${srcs.length + deps.length}; i++) {
52-
idl_array.add_dependency_idls(idls[i]);
53-
}
54-
idl_array.test();
55-
})().then(resolve, (e) => reject(e.toString()));
56-
}`)
57-
);
34+
await shadowRealmEvalAsync(realm, `
35+
await import("/resources/testharness-shadowrealm-inner.js");
36+
await import("/resources/testharness.js");
37+
await import("/resources/WebIDLParser.js");
38+
await import("/resources/idlharness.js");
39+
const idls = ${idls};
40+
const idl_array = new IdlArray();
41+
for (let i = 0; i < ${srcs.length}; i++) {
42+
idl_array.add_idls(idls[i]);
43+
}
44+
for (let i = ${srcs.length}; i < ${srcs.length + deps.length}; i++) {
45+
idl_array.add_dependency_idls(idls[i]);
46+
}
47+
idl_array.test();
48+
`);
5849
await fetch_tests_from_shadow_realm(realm);
5950
});
6051
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* AudioWorkletProcessor intended for hosting a ShadowRealm and running a test
3+
* inside of that ShadowRealm.
4+
*/
5+
globalThis.TestRunner = class TestRunner extends AudioWorkletProcessor {
6+
constructor() {
7+
super();
8+
this.createShadowRealmAndStartTests();
9+
}
10+
11+
/**
12+
* Fetch adaptor function intended as a drop-in replacement for fetchAdaptor()
13+
* (see testharness-shadowrealm-outer.js), but it does not assume fetch() is
14+
* present in the realm. Instead, it relies on setupFakeFetchOverMessagePort()
15+
* having been called on the port on the other side of this.port's channel.
16+
*/
17+
fetchOverPortExecutor(resource) {
18+
return (resolve, reject) => {
19+
const listener = (event) => {
20+
if (typeof event.data !== "string" || !event.data.startsWith("fetchResult::")) {
21+
return;
22+
}
23+
24+
const result = event.data.slice("fetchResult::".length);
25+
if (result.startsWith("success::")) {
26+
resolve(result.slice("success::".length));
27+
} else {
28+
reject(result.slice("fail::".length));
29+
}
30+
31+
this.port.removeEventListener("message", listener);
32+
}
33+
this.port.addEventListener("message", listener);
34+
this.port.start();
35+
this.port.postMessage(`fetchRequest::${resource}`);
36+
}
37+
}
38+
39+
/**
40+
* Async method, which is patched over in
41+
* (test).any.audioworklet-shadowrealm.js; see serve.py
42+
*/
43+
async createShadowRealmAndStartTests() {
44+
throw new Error("Forgot to overwrite this method!");
45+
}
46+
47+
/** Overrides AudioWorkletProcessor.prototype.process() */
48+
process() {
49+
return false;
50+
}
51+
};
52+
registerProcessor("test-runner", TestRunner);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// testharness file with ShadowRealm utilities to be imported inside ShadowRealm
2+
3+
/**
4+
* Set up all properties on the ShadowRealm's global object that tests will
5+
* expect to be present.
6+
*
7+
* @param {string} queryString - string to use as value for location.search,
8+
* used for subsetting some tests
9+
* @param {function} fetchAdaptor - a function that takes a resource URI and
10+
* returns a function which itself takes a (resolve, reject) pair from the
11+
* hosting realm, and calls resolve with the text result of fetching the
12+
* resource, or reject with a string indicating the error that occurred
13+
*/
14+
globalThis.setShadowRealmGlobalProperties = function (queryString, fetchAdaptor) {
15+
globalThis.fetch_json = (resource) => {
16+
const executor = fetchAdaptor(resource);
17+
return new Promise(executor).then((s) => JSON.parse(s));
18+
};
19+
20+
globalThis.location = { search: queryString };
21+
};
22+
23+
globalThis.GLOBAL = {
24+
isWindow: function() { return false; },
25+
isWorker: function() { return false; },
26+
isShadowRealm: function() { return true; },
27+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// testharness file with ShadowRealm utilities to be imported in the realm
2+
// hosting the ShadowRealm
3+
4+
/**
5+
* Convenience function for evaluating some async code in the ShadowRealm and
6+
* waiting for the result.
7+
*
8+
* @param {ShadowRealm} realm - the ShadowRealm to evaluate the code in
9+
* @param {string} asyncBody - the code to evaluate; will be put in the body of
10+
* an async function, and must return a value explicitly if a value is to be
11+
* returned to the hosting realm.
12+
*/
13+
globalThis.shadowRealmEvalAsync = function (realm, asyncBody) {
14+
return new Promise(realm.evaluate(`
15+
(resolve, reject) => {
16+
(async () => {
17+
${asyncBody}
18+
})().then(resolve, (e) => reject(e.toString()));
19+
}
20+
`));
21+
};
22+
23+
/**
24+
* Convenience adaptor function for fetch() that can be passed to
25+
* setShadowRealmGlobalProperties() (see testharness-shadowrealm-inner.js).
26+
* Used to adapt the hosting realm's fetch(), if present, to fetch a resource
27+
* and pass its text through the callable boundary to the ShadowRealm.
28+
*/
29+
globalThis.fetchAdaptor = (resource) => (resolve, reject) => {
30+
fetch(resource)
31+
.then(res => res.text())
32+
.then(resolve, (e) => reject(e.toString()));
33+
};
34+
35+
let sharedWorkerMessagePortPromise;
36+
/**
37+
* Used when the hosting realm is a worker. This value is a Promise that
38+
* resolves to a function that posts a message to the worker's message port,
39+
* just like postMessage(). The message port is only available asynchronously in
40+
* SharedWorkers and ServiceWorkers.
41+
*/
42+
globalThis.getPostMessageFunc = async function () {
43+
if (typeof postMessage === "function") {
44+
return postMessage; // postMessage available directly in dedicated worker
45+
}
46+
47+
if (typeof clients === "object") {
48+
// Messages from the ShadowRealm are not in response to any message received
49+
// from the ServiceWorker's client, so broadcast them to all clients
50+
const allClients = await clients.matchAll({ includeUncontrolled: true });
51+
return function broadcast(msg) {
52+
allClients.map(client => client.postMessage(msg));
53+
}
54+
}
55+
56+
if (sharedWorkerMessagePortPromise) {
57+
return await sharedWorkerMessagePortPromise;
58+
}
59+
60+
throw new Error("getPostMessageFunc is intended for Worker scopes");
61+
}
62+
63+
// Port available asynchronously in shared worker, but not via an async func
64+
let savedResolver;
65+
if (globalThis.constructor.name === "SharedWorkerGlobalScope") {
66+
sharedWorkerMessagePortPromise = new Promise((resolve) => {
67+
savedResolver = resolve;
68+
});
69+
addEventListener("connect", function (event) {
70+
const port = event.ports[0];
71+
savedResolver(port.postMessage.bind(port));
72+
});
73+
}
74+
75+
/**
76+
* Used when the hosting realm does not permit dynamic import, e.g. in
77+
* ServiceWorkers or AudioWorklets. Requires an adaptor function such as
78+
* fetchAdaptor() above, or an equivalent if fetch() is not present in the
79+
* hosting realm.
80+
*
81+
* @param {ShadowRealm} realm - the ShadowRealm in which to setup a
82+
* fakeDynamicImport() global function.
83+
* @param {function} adaptor - an adaptor function that does what fetchAdaptor()
84+
* does.
85+
*/
86+
globalThis.setupFakeDynamicImportInShadowRealm = function(realm, adaptor) {
87+
function fetchModuleTextExecutor(url) {
88+
return (resolve, reject) => {
89+
new Promise(adaptor(url))
90+
.then(text => realm.evaluate(text + ";\nundefined"))
91+
.then(resolve, (e) => reject(e.toString()));
92+
}
93+
}
94+
95+
realm.evaluate(`
96+
(fetchModuleTextExecutor) => {
97+
globalThis.fakeDynamicImport = function (url) {
98+
return new Promise(fetchModuleTextExecutor(url));
99+
}
100+
}
101+
`)(fetchModuleTextExecutor);
102+
};
103+
104+
/**
105+
* Used when the hosting realm does not expose fetch(), i.e. in worklets. The
106+
* port on the other side of the channel needs to send messages starting with
107+
* 'fetchRequest::' and listen for messages starting with 'fetchResult::'. See
108+
* testharness-shadowrealm-audioworkletprocessor.js.
109+
*
110+
* @param {port} MessagePort - the message port on which to listen for fetch
111+
* requests
112+
*/
113+
globalThis.setupFakeFetchOverMessagePort = function (port) {
114+
port.addEventListener("message", (event) => {
115+
if (typeof event.data !== "string" || !event.data.startsWith("fetchRequest::")) {
116+
return;
117+
}
118+
119+
fetch(event.data.slice("fetchRequest::".length))
120+
.then(res => res.text())
121+
.then(
122+
text => port.postMessage(`fetchResult::success::${text}`),
123+
error => port.postMessage(`fetchResult::fail::${error}`),
124+
);
125+
});
126+
port.start();
127+
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
// META: script=/resources/testharness-shadowrealm-outer.js
12
// META: script=/resources/idlharness-shadowrealm.js
23
idl_test_shadowrealm(["streams"], ["dom"]);

testing/web-platform/tests/tools/manifest/sourcefile.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,26 @@ class VariantData(TypedDict, total=False):
8585
"dedicatedworker-module": {"suffix": ".any.worker-module.html"},
8686
"worker": {"longhand": {"dedicatedworker", "sharedworker", "serviceworker"}},
8787
"worker-module": {},
88-
"shadowrealm": {},
88+
"shadowrealm-in-window": {},
89+
"shadowrealm-in-shadowrealm": {},
90+
"shadowrealm-in-dedicatedworker": {},
91+
"shadowrealm-in-sharedworker": {},
92+
"shadowrealm-in-serviceworker": {
93+
"force_https": True,
94+
"suffix": ".https.any.shadowrealm-in-serviceworker.html",
95+
},
96+
"shadowrealm-in-audioworklet": {
97+
"force_https": True,
98+
"suffix": ".https.any.shadowrealm-in-audioworklet.html",
99+
},
100+
"shadowrealm": {"longhand": {
101+
"shadowrealm-in-window",
102+
"shadowrealm-in-shadowrealm",
103+
"shadowrealm-in-dedicatedworker",
104+
"shadowrealm-in-sharedworker",
105+
"shadowrealm-in-serviceworker",
106+
"shadowrealm-in-audioworklet",
107+
}},
89108
"jsshell": {"suffix": ".any.js"},
90109
}
91110

0 commit comments

Comments
 (0)