testharness-shadowrealm-outer.js (5176B)
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 * In case of error, this function intentionally exposes the stack trace (if it 9 * is available) to the hosting realm, for debugging purposes. 10 * 11 * @param {ShadowRealm} realm - the ShadowRealm to evaluate the code in 12 * @param {string} asyncBody - the code to evaluate; will be put in the body of 13 * an async function, and must return a value explicitly if a value is to be 14 * returned to the hosting realm. 15 */ 16 globalThis.shadowRealmEvalAsync = function (realm, asyncBody) { 17 return new Promise(realm.evaluate(` 18 (resolve, reject) => { 19 (async () => { 20 ${asyncBody} 21 })().then(resolve, (e) => reject(e.toString() + "\\n" + (e.stack || ""))); 22 } 23 `)); 24 }; 25 26 /** 27 * Convenience adaptor function for fetch() that can be passed to 28 * setShadowRealmGlobalProperties() (see testharness-shadowrealm-inner.js). 29 * Used to adapt the hosting realm's fetch(), if present, to fetch a resource 30 * and pass its text through the callable boundary to the ShadowRealm. 31 */ 32 globalThis.fetchAdaptor = (resource) => (resolve, reject) => { 33 fetch(resource) 34 .then(res => res.text()) 35 .then(resolve, (e) => reject(e.toString())); 36 }; 37 38 let workerMessagePortPromise; 39 /** 40 * Used when the hosting realm is a worker. This value is a Promise that 41 * resolves to a function that posts a message to the worker's message port, 42 * just like postMessage(). The message port is only available asynchronously in 43 * SharedWorkers and ServiceWorkers. 44 */ 45 globalThis.getPostMessageFunc = async function () { 46 if (typeof postMessage === "function") { 47 return postMessage; // postMessage available directly in dedicated worker 48 } 49 50 if (workerMessagePortPromise) { 51 return await workerMessagePortPromise; 52 } 53 54 throw new Error("getPostMessageFunc is intended for Worker scopes"); 55 } 56 57 // Port available asynchronously in shared worker, but not via an async func 58 let savedResolver; 59 if (globalThis.constructor.name === "SharedWorkerGlobalScope") { 60 workerMessagePortPromise = new Promise((resolve) => { 61 savedResolver = resolve; 62 }); 63 addEventListener("connect", function (event) { 64 const port = event.ports[0]; 65 savedResolver(port.postMessage.bind(port)); 66 }); 67 } else if (globalThis.constructor.name === "ServiceWorkerGlobalScope") { 68 workerMessagePortPromise = new Promise((resolve) => { 69 savedResolver = resolve; 70 }); 71 addEventListener("message", (e) => { 72 if (typeof e.data === "object" && e.data !== null && e.data.type === "connect") { 73 const client = e.source; 74 savedResolver(client.postMessage.bind(client)); 75 } 76 }); 77 } 78 79 /** 80 * Used when the hosting realm does not permit dynamic import, e.g. in 81 * ServiceWorkers or AudioWorklets. Requires an adaptor function such as 82 * fetchAdaptor() above, or an equivalent if fetch() is not present in the 83 * hosting realm. 84 * 85 * @param {ShadowRealm} realm - the ShadowRealm in which to setup a 86 * fakeDynamicImport() global function. 87 * @param {function} adaptor - an adaptor function that does what fetchAdaptor() 88 * does. 89 */ 90 globalThis.setupFakeDynamicImportInShadowRealm = function(realm, adaptor) { 91 function fetchModuleTextExecutor(url) { 92 return (resolve, reject) => { 93 new Promise(adaptor(url)) 94 .then(text => realm.evaluate(text + ";\nundefined")) 95 .then(resolve, (e) => reject(e.toString())); 96 } 97 } 98 99 realm.evaluate(` 100 (fetchModuleTextExecutor) => { 101 globalThis.fakeDynamicImport = function (url) { 102 return new Promise(fetchModuleTextExecutor(url)); 103 } 104 } 105 `)(fetchModuleTextExecutor); 106 }; 107 108 /** 109 * Used when the hosting realm does not expose fetch(), i.e. in worklets. The 110 * port on the other side of the channel needs to send messages starting with 111 * 'fetchRequest::' and listen for messages starting with 'fetchResult::'. See 112 * testharness-shadowrealm-audioworkletprocessor.js. 113 * 114 * @param {port} MessagePort - the message port on which to listen for fetch 115 * requests 116 */ 117 globalThis.setupFakeFetchOverMessagePort = function (port) { 118 port.addEventListener("message", (event) => { 119 if (typeof event.data !== "string" || !event.data.startsWith("fetchRequest::")) { 120 return; 121 } 122 123 fetch(event.data.slice("fetchRequest::".length)) 124 .then(res => res.text()) 125 .then( 126 text => port.postMessage(`fetchResult::success::${text}`), 127 error => port.postMessage(`fetchResult::fail::${error}`), 128 ); 129 }); 130 port.start(); 131 } 132 133 /** 134 * Returns a message suitable for posting with postMessage() that will signal to 135 * the test harness that the tests are finished and there was an error in the 136 * setup code. 137 * 138 * @param {message} any - error 139 */ 140 globalThis.createSetupErrorResult = function (message) { 141 return { 142 type: "complete", 143 tests: [], 144 asserts: [], 145 status: { 146 status: 1, // TestsStatus.ERROR, 147 message: String(message), 148 stack: typeof message === "object" && message !== null && "stack" in message ? message.stack : undefined, 149 }, 150 }; 151 };