Source code
Revision control
Copy as Markdown
Other Tools
// testharness file with ShadowRealm utilities to be imported in the realm
// hosting the ShadowRealm
/**
* Convenience function for evaluating some async code in the ShadowRealm and
* waiting for the result.
*
* In case of error, this function intentionally exposes the stack trace (if it
* is available) to the hosting realm, for debugging purposes.
*
* @param {ShadowRealm} realm - the ShadowRealm to evaluate the code in
* @param {string} asyncBody - the code to evaluate; will be put in the body of
* an async function, and must return a value explicitly if a value is to be
* returned to the hosting realm.
*/
globalThis.shadowRealmEvalAsync = function (realm, asyncBody) {
return new Promise(realm.evaluate(`
(resolve, reject) => {
(async () => {
${asyncBody}
})().then(resolve, (e) => reject(e.toString() + "\\n" + (e.stack || "")));
}
`));
};
/**
* Convenience adaptor function for fetch() that can be passed to
* setShadowRealmGlobalProperties() (see testharness-shadowrealm-inner.js).
* Used to adapt the hosting realm's fetch(), if present, to fetch a resource
* and pass its text through the callable boundary to the ShadowRealm.
*/
globalThis.fetchAdaptor = (resource) => (resolve, reject) => {
fetch(resource)
.then(res => res.text())
.then(resolve, (e) => reject(e.toString()));
};
let workerMessagePortPromise;
/**
* Used when the hosting realm is a worker. This value is a Promise that
* resolves to a function that posts a message to the worker's message port,
* just like postMessage(). The message port is only available asynchronously in
* SharedWorkers and ServiceWorkers.
*/
globalThis.getPostMessageFunc = async function () {
if (typeof postMessage === "function") {
return postMessage; // postMessage available directly in dedicated worker
}
if (workerMessagePortPromise) {
return await workerMessagePortPromise;
}
throw new Error("getPostMessageFunc is intended for Worker scopes");
}
// Port available asynchronously in shared worker, but not via an async func
let savedResolver;
if (globalThis.constructor.name === "SharedWorkerGlobalScope") {
workerMessagePortPromise = new Promise((resolve) => {
savedResolver = resolve;
});
addEventListener("connect", function (event) {
const port = event.ports[0];
savedResolver(port.postMessage.bind(port));
});
} else if (globalThis.constructor.name === "ServiceWorkerGlobalScope") {
workerMessagePortPromise = new Promise((resolve) => {
savedResolver = resolve;
});
addEventListener("message", (e) => {
if (typeof e.data === "object" && e.data !== null && e.data.type === "connect") {
const client = e.source;
savedResolver(client.postMessage.bind(client));
}
});
}
/**
* Used when the hosting realm does not permit dynamic import, e.g. in
* ServiceWorkers or AudioWorklets. Requires an adaptor function such as
* fetchAdaptor() above, or an equivalent if fetch() is not present in the
* hosting realm.
*
* @param {ShadowRealm} realm - the ShadowRealm in which to setup a
* fakeDynamicImport() global function.
* @param {function} adaptor - an adaptor function that does what fetchAdaptor()
* does.
*/
globalThis.setupFakeDynamicImportInShadowRealm = function(realm, adaptor) {
function fetchModuleTextExecutor(url) {
return (resolve, reject) => {
new Promise(adaptor(url))
.then(text => realm.evaluate(text + ";\nundefined"))
.then(resolve, (e) => reject(e.toString()));
}
}
realm.evaluate(`
(fetchModuleTextExecutor) => {
globalThis.fakeDynamicImport = function (url) {
return new Promise(fetchModuleTextExecutor(url));
}
}
`)(fetchModuleTextExecutor);
};
/**
* Used when the hosting realm does not expose fetch(), i.e. in worklets. The
* port on the other side of the channel needs to send messages starting with
* 'fetchRequest::' and listen for messages starting with 'fetchResult::'. See
* testharness-shadowrealm-audioworkletprocessor.js.
*
* @param {port} MessagePort - the message port on which to listen for fetch
* requests
*/
globalThis.setupFakeFetchOverMessagePort = function (port) {
port.addEventListener("message", (event) => {
if (typeof event.data !== "string" || !event.data.startsWith("fetchRequest::")) {
return;
}
fetch(event.data.slice("fetchRequest::".length))
.then(res => res.text())
.then(
text => port.postMessage(`fetchResult::success::${text}`),
error => port.postMessage(`fetchResult::fail::${error}`),
);
});
port.start();
}
/**
* Returns a message suitable for posting with postMessage() that will signal to
* the test harness that the tests are finished and there was an error in the
* setup code.
*
* @param {message} any - error
*/
globalThis.createSetupErrorResult = function (message) {
return {
type: "complete",
tests: [],
asserts: [],
status: {
status: 1, // TestsStatus.ERROR,
message: String(message),
stack: typeof message === "object" && message !== null && "stack" in message ? message.stack : undefined,
},
};
};