| // Creates a new iframe in `doc`, calls `func` on it and appends it as a child |
| // of `doc`. |
| // Returns a promise that resolves to the iframe once loaded (successfully or |
| // not). |
| // The iframe is removed from `doc` once test `t` is done running. |
| // |
| // NOTE: There exists no interoperable way to check whether an iframe failed to |
| // load, so this should only be used when the iframe is expected to load. It |
| // also means we cannot wire the iframe's `error` event to a promise |
| // rejection. See: https://github.com/whatwg/html/issues/125 |
| function appendIframeWith(t, doc, func) { |
| return new Promise(resolve => { |
| const child = doc.createElement("iframe"); |
| t.add_cleanup(() => child.remove()); |
| |
| child.addEventListener("load", () => resolve(child), { once: true }); |
| func(child); |
| doc.body.appendChild(child); |
| }); |
| } |
| |
| // Appends a child iframe to `doc` sourced from `src`. |
| // |
| // See `appendIframeWith()` for more details. |
| function appendIframe(t, doc, src) { |
| return appendIframeWith(t, doc, child => { child.src = src; }); |
| } |
| |
| // Registers an event listener that will resolve this promise when this |
| // window receives a message posted to it. |
| // |
| // `options` has the following shape: |
| // |
| // { |
| // source: If specified, this function waits for the first message from the |
| // given source only, ignoring other messages. |
| // |
| // filter: If specified, this function calls `filter` on each incoming |
| // message, and resolves iff it returns true. |
| // } |
| // |
| function futureMessage(options) { |
| return new Promise(resolve => { |
| window.addEventListener("message", (e) => { |
| if (options?.source && options.source !== e.source) { |
| return; |
| } |
| |
| if (options?.filter && !options.filter(e.data)) { |
| return; |
| } |
| |
| resolve(e.data); |
| }); |
| }); |
| }; |
| |
| // Like `promise_test()`, but executes tests in parallel like `async_test()`. |
| // |
| // Cribbed from COEP tests. |
| function promise_test_parallel(promise, description) { |
| async_test(test => { |
| promise(test) |
| .then(() => test.done()) |
| .catch(test.step_func(error => { throw error; })); |
| }, description); |
| }; |
| |
| async function postMessageAndAwaitReply(target, message) { |
| const reply = futureMessage({ source: target }); |
| target.postMessage(message, "*"); |
| return await reply; |
| } |
| |
| // Maps protocol (without the trailing colon) and address space to port. |
| const SERVER_PORTS = { |
| "http": { |
| "local": {{ports[http][0]}}, |
| "private": {{ports[http-private][0]}}, |
| "public": {{ports[http-public][0]}}, |
| }, |
| "https": { |
| "local": {{ports[https][0]}}, |
| "other-local": {{ports[https][1]}}, |
| "private": {{ports[https-private][0]}}, |
| "public": {{ports[https-public][0]}}, |
| }, |
| "ws": { |
| "local": {{ports[ws][0]}}, |
| }, |
| "wss": { |
| "local": {{ports[wss][0]}}, |
| }, |
| }; |
| |
| // A `Server` is a web server accessible by tests. It has the following shape: |
| // |
| // { |
| // addressSpace: the IP address space of the server ("local", "private" or |
| // "public"), |
| // name: a human-readable name for the server, |
| // port: the port on which the server listens for connections, |
| // protocol: the protocol (including trailing colon) spoken by the server, |
| // } |
| // |
| // Constants below define the available servers, which can also be accessed |
| // programmatically with `get()`. |
| class Server { |
| // Maps the given `protocol` (without a trailing colon) and `addressSpace` to |
| // a server. Returns null if no such server exists. |
| static get(protocol, addressSpace) { |
| const ports = SERVER_PORTS[protocol]; |
| if (ports === undefined) { |
| return null; |
| } |
| |
| const port = ports[addressSpace]; |
| if (port === undefined) { |
| return null; |
| } |
| |
| return { |
| addressSpace, |
| name: `${protocol}-${addressSpace}`, |
| port, |
| protocol: protocol + ':', |
| }; |
| } |
| |
| static HTTP_LOCAL = Server.get("http", "local"); |
| static HTTP_PRIVATE = Server.get("http", "private"); |
| static HTTP_PUBLIC = Server.get("http", "public"); |
| static HTTPS_LOCAL = Server.get("https", "local"); |
| static OTHER_HTTPS_LOCAL = Server.get("https", "other-local"); |
| static HTTPS_PRIVATE = Server.get("https", "private"); |
| static HTTPS_PUBLIC = Server.get("https", "public"); |
| static WS_LOCAL = Server.get("ws", "local"); |
| static WSS_LOCAL = Server.get("wss", "local"); |
| }; |
| |
| // Resolves a URL relative to the current location, returning an absolute URL. |
| // |
| // `url` specifies the relative URL, e.g. "foo.html" or "http://foo.example". |
| // `options`, if defined, should have the following shape: |
| // |
| // { |
| // // Optional. Overrides the protocol of the returned URL. |
| // protocol, |
| // |
| // // Optional. Overrides the port of the returned URL. |
| // port, |
| // |
| // // Extra headers. |
| // headers, |
| // |
| // // Extra search params. |
| // searchParams, |
| // } |
| // |
| function resolveUrl(url, options) { |
| const result = new URL(url, window.location); |
| if (options === undefined) { |
| return result; |
| } |
| |
| const { port, protocol, headers, searchParams } = options; |
| if (port !== undefined) { |
| result.port = port; |
| } |
| if (protocol !== undefined) { |
| result.protocol = protocol; |
| } |
| if (headers !== undefined) { |
| const pipes = []; |
| for (key in headers) { |
| pipes.push(`header(${key},${headers[key]})`); |
| } |
| result.searchParams.append("pipe", pipes.join("|")); |
| } |
| if (searchParams !== undefined) { |
| for (key in searchParams) { |
| result.searchParams.append(key, searchParams[key]); |
| } |
| } |
| |
| return result; |
| } |
| |
| // Computes options to pass to `resolveUrl()` for a source document's URL. |
| // |
| // `server` identifies the server from which to load the document. |
| // `treatAsPublic`, if set to true, specifies that the source document should |
| // be artificially placed in the `public` address space using CSP. |
| function sourceResolveOptions({ server, treatAsPublic }) { |
| const options = {...server}; |
| if (treatAsPublic) { |
| options.headers = { "Content-Security-Policy": "treat-as-public-address" }; |
| } |
| return options; |
| } |
| |
| // Computes the URL of a preflight handler configured with the given options. |
| // |
| // `server` identifies the server from which to load the resource. |
| // `behavior` specifies the behavior of the target server. It may contain: |
| // - `preflight`: The result of calling one of `PreflightBehavior`'s methods. |
| // - `response`: The result of calling one of `ResponseBehavior`'s methods. |
| // - `redirect`: A URL to which the target should redirect GET requests. |
| function preflightUrl({ server, behavior }) { |
| assert_not_equals(server, undefined, 'server'); |
| const options = {...server}; |
| if (behavior) { |
| const { preflight, response, redirect } = behavior; |
| options.searchParams = { |
| ...preflight, |
| ...response, |
| }; |
| if (redirect !== undefined) { |
| options.searchParams.redirect = redirect; |
| } |
| } |
| |
| return resolveUrl("resources/preflight.py", options); |
| } |
| |
| // Methods generate behavior specifications for how `resources/preflight.py` |
| // should behave upon receiving a preflight request. |
| const PreflightBehavior = { |
| // The preflight response should fail with a non-2xx code. |
| failure: () => ({}), |
| |
| // The preflight response should be missing CORS headers. |
| // `uuid` should be a UUID that uniquely identifies the preflight request. |
| noCorsHeader: (uuid) => ({ |
| "preflight-uuid": uuid, |
| }), |
| |
| // The preflight response should be missing PNA headers. |
| // `uuid` should be a UUID that uniquely identifies the preflight request. |
| noPnaHeader: (uuid) => ({ |
| "preflight-uuid": uuid, |
| "preflight-headers": "cors", |
| }), |
| |
| // The preflight response should succeed. |
| // `uuid` should be a UUID that uniquely identifies the preflight request. |
| success: (uuid) => ({ |
| "preflight-uuid": uuid, |
| "preflight-headers": "cors+pna", |
| }), |
| |
| optionalSuccess: (uuid) => ({ |
| "preflight-uuid": uuid, |
| "preflight-headers": "cors+pna", |
| "is-preflight-optional": true, |
| }), |
| |
| // The preflight response should succeed and allow service-worker header. |
| // `uuid` should be a UUID that uniquely identifies the preflight request. |
| serviceWorkerSuccess: (uuid) => ({ |
| "preflight-uuid": uuid, |
| "preflight-headers": "cors+pna+sw", |
| }), |
| |
| // The preflight response should succeed only if it is the first preflight. |
| // `uuid` should be a UUID that uniquely identifies the preflight request. |
| singlePreflight: (uuid) => ({ |
| "preflight-uuid": uuid, |
| "preflight-headers": "cors+pna", |
| "expect-single-preflight": true, |
| }), |
| |
| // The preflight response should succeed and allow origins and headers for |
| // navigations. |
| navigation: (uuid) => ({ |
| "preflight-uuid": uuid, |
| "preflight-headers": "navigation", |
| }), |
| }; |
| |
| // Methods generate behavior specifications for how `resources/preflight.py` |
| // should behave upon receiving a regular (non-preflight) request. |
| const ResponseBehavior = { |
| // The response should succeed without CORS headers. |
| default: () => ({}), |
| |
| // The response should succeed with CORS headers. |
| allowCrossOrigin: () => ({ "final-headers": "cors" }), |
| }; |
| |
| const FetchTestResult = { |
| SUCCESS: { |
| ok: true, |
| body: "success", |
| }, |
| OPAQUE: { |
| ok: false, |
| type: "opaque", |
| body: "", |
| }, |
| FAILURE: { |
| error: "TypeError: Failed to fetch", |
| }, |
| }; |
| |
| // Runs a fetch test. Tries to fetch a given subresource from a given document. |
| // |
| // Main argument shape: |
| // |
| // { |
| // // Optional. Passed to `sourceResolveOptions()`. |
| // source, |
| // |
| // // Optional. Passed to `preflightUrl()`. |
| // target, |
| // |
| // // Optional. Passed to `fetch()`. |
| // fetchOptions, |
| // |
| // // Required. One of the values in `FetchTestResult`. |
| // expected, |
| // } |
| // |
| async function fetchTest(t, { source, target, fetchOptions, expected }) { |
| const sourceUrl = |
| resolveUrl("resources/fetcher.html", sourceResolveOptions(source)); |
| |
| const targetUrl = preflightUrl(target); |
| |
| const iframe = await appendIframe(t, document, sourceUrl); |
| const reply = futureMessage({ source: iframe.contentWindow }); |
| |
| const message = { |
| url: targetUrl.href, |
| options: fetchOptions, |
| }; |
| iframe.contentWindow.postMessage(message, "*"); |
| |
| const { error, ok, type, body } = await reply; |
| |
| assert_equals(error, expected.error, "error"); |
| |
| assert_equals(ok, expected.ok, "response ok"); |
| assert_equals(body, expected.body, "response body"); |
| |
| if (expected.type !== undefined) { |
| assert_equals(type, expected.type, "response type"); |
| } |
| } |
| |
| // Similar to `fetchTest`, but replaced iframes with fenced frames. |
| async function fencedFrameFetchTest(t, { source, target, fetchOptions, expected }) { |
| const fetcher_url = |
| resolveUrl("resources/fenced-frame-fetcher.https.html", sourceResolveOptions(source)); |
| |
| const target_url = preflightUrl(target); |
| target_url.searchParams.set("is-loaded-in-fenced-frame", true); |
| |
| fetcher_url.searchParams.set("mode", fetchOptions.mode); |
| fetcher_url.searchParams.set("method", fetchOptions.method); |
| fetcher_url.searchParams.set("url", target_url); |
| |
| const error_token = token(); |
| const ok_token = token(); |
| const body_token = token(); |
| const type_token = token(); |
| const source_url = generateURL(fetcher_url, [error_token, ok_token, body_token, type_token]); |
| |
| const urn = await generateURNFromFledge(source_url, []); |
| attachFencedFrame(urn); |
| |
| const error = await nextValueFromServer(error_token); |
| const ok = await nextValueFromServer(ok_token); |
| const body = await nextValueFromServer(body_token); |
| const type = await nextValueFromServer(type_token); |
| |
| assert_equals(error, expected.error || "" , "error"); |
| assert_equals(body, expected.body || "", "response body"); |
| assert_equals(ok, expected.ok !== undefined ? expected.ok.toString() : "", "response ok"); |
| if (expected.type !== undefined) { |
| assert_equals(type, expected.type, "response type"); |
| } |
| } |
| |
| const XhrTestResult = { |
| SUCCESS: { |
| loaded: true, |
| status: 200, |
| body: "success", |
| }, |
| FAILURE: { |
| loaded: false, |
| status: 0, |
| }, |
| }; |
| |
| // Runs an XHR test. Tries to fetch a given subresource from a given document. |
| // |
| // Main argument shape: |
| // |
| // { |
| // // Optional. Passed to `sourceResolveOptions()`. |
| // source, |
| // |
| // // Optional. Passed to `preflightUrl()`. |
| // target, |
| // |
| // // Optional. Method to use when sending the request. Defaults to "GET". |
| // method, |
| // |
| // // Required. One of the values in `XhrTestResult`. |
| // expected, |
| // } |
| // |
| async function xhrTest(t, { source, target, method, expected }) { |
| const sourceUrl = |
| resolveUrl("resources/xhr-sender.html", sourceResolveOptions(source)); |
| |
| const targetUrl = preflightUrl(target); |
| |
| const iframe = await appendIframe(t, document, sourceUrl); |
| const reply = futureMessage(); |
| |
| const message = { |
| url: targetUrl.href, |
| method: method, |
| }; |
| iframe.contentWindow.postMessage(message, "*"); |
| |
| const { loaded, status, body } = await reply; |
| |
| assert_equals(loaded, expected.loaded, "response loaded"); |
| assert_equals(status, expected.status, "response status"); |
| assert_equals(body, expected.body, "response body"); |
| } |
| |
| const FrameTestResult = { |
| SUCCESS: "loaded", |
| FAILURE: "timeout", |
| }; |
| |
| async function iframeTest(t, { source, target, expected }) { |
| // Allows running tests in parallel. |
| const uuid = token(); |
| |
| const targetUrl = preflightUrl(target); |
| targetUrl.searchParams.set("file", "iframed.html"); |
| targetUrl.searchParams.set("iframe-uuid", uuid); |
| targetUrl.searchParams.set( |
| "file-if-no-preflight-received", |
| "iframed-no-preflight-received.html", |
| ); |
| |
| const sourceUrl = |
| resolveUrl("resources/iframer.html", sourceResolveOptions(source)); |
| sourceUrl.searchParams.set("url", targetUrl); |
| |
| const messagePromise = futureMessage({ |
| filter: (data) => data.uuid === uuid, |
| }); |
| const iframe = await appendIframe(t, document, sourceUrl); |
| |
| // The grandchild frame posts a message iff it loads successfully. |
| // There exists no interoperable way to check whether an iframe failed to |
| // load, so we use a timeout. |
| // See: https://github.com/whatwg/html/issues/125 |
| const result = await Promise.race([ |
| messagePromise.then((data) => data.message), |
| new Promise((resolve) => { |
| t.step_timeout(() => resolve("timeout"), 2000 /* ms */); |
| }), |
| ]); |
| |
| assert_equals(result, expected); |
| } |
| |
| const NavigationTestResult = { |
| SUCCESS: "success", |
| FAILURE: "timeout", |
| }; |
| |
| async function windowOpenTest(t, { source, target, expected }) { |
| const targetUrl = preflightUrl(target); |
| targetUrl.searchParams.set("file", "openee.html"); |
| targetUrl.searchParams.set( |
| "file-if-no-preflight-received", |
| "no-preflight-received.html", |
| ); |
| |
| const sourceUrl = |
| resolveUrl("resources/opener.html", sourceResolveOptions(source)); |
| sourceUrl.searchParams.set("url", targetUrl); |
| |
| const iframe = await appendIframe(t, document, sourceUrl); |
| const reply = futureMessage({ source: iframe.contentWindow }); |
| |
| iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); |
| |
| const result = await Promise.race([ |
| reply, |
| new Promise((resolve) => { |
| t.step_timeout(() => resolve("timeout"), 10000 /* ms */); |
| }), |
| ]); |
| |
| assert_equals(result, expected); |
| } |
| |
| async function windowOpenExistingTest(t, { source, target, expected }) { |
| const targetUrl = preflightUrl(target); |
| targetUrl.searchParams.set("file", "openee.html"); |
| targetUrl.searchParams.set( |
| "file-if-no-preflight-received", |
| "no-preflight-received.html", |
| ); |
| |
| const sourceUrl = resolveUrl( |
| 'resources/open-to-existing-window.html', sourceResolveOptions(source)); |
| sourceUrl.searchParams.set("url", targetUrl); |
| sourceUrl.searchParams.set("token", token()); |
| |
| const iframe = await appendIframe(t, document, sourceUrl); |
| const reply = futureMessage({ source: iframe.contentWindow }); |
| |
| iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); |
| |
| const result = await Promise.race([ |
| reply, |
| new Promise((resolve) => { |
| t.step_timeout(() => resolve("timeout"), 10000 /* ms */); |
| }), |
| ]); |
| |
| assert_equals(result, expected); |
| } |
| |
| async function anchorTest(t, { source, target, expected }) { |
| const targetUrl = preflightUrl(target); |
| targetUrl.searchParams.set("file", "openee.html"); |
| targetUrl.searchParams.set( |
| "file-if-no-preflight-received", |
| "no-preflight-received.html", |
| ); |
| |
| const sourceUrl = |
| resolveUrl("resources/anchor.html", sourceResolveOptions(source)); |
| sourceUrl.searchParams.set("url", targetUrl); |
| |
| const iframe = await appendIframe(t, document, sourceUrl); |
| const reply = futureMessage({ source: iframe.contentWindow }); |
| |
| iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); |
| |
| const result = await Promise.race([ |
| reply, |
| new Promise((resolve) => { |
| t.step_timeout(() => resolve("timeout"), 10000 /* ms */); |
| }), |
| ]); |
| |
| assert_equals(result, expected); |
| } |
| |
| // Similar to `iframeTest`, but replaced iframes with fenced frames. |
| async function fencedFrameTest(t, { source, target, expected }) { |
| // Allows running tests in parallel. |
| const target_url = preflightUrl(target); |
| target_url.searchParams.set("file", "fenced-frame-private-network-access-target.https.html"); |
| target_url.searchParams.set("is-loaded-in-fenced-frame", true); |
| |
| const frame_loaded_key = token(); |
| const child_frame_target = generateURL(target_url, [frame_loaded_key]); |
| |
| const source_url = |
| resolveUrl("resources/fenced-frame-private-network-access.https.html", sourceResolveOptions(source)); |
| source_url.searchParams.set("fenced_frame_url", child_frame_target); |
| |
| const urn = await generateURNFromFledge(source_url, []); |
| attachFencedFrame(urn); |
| |
| // The grandchild fenced frame writes a value to the server iff it loads |
| // successfully. |
| const result = (expected == FrameTestResult.SUCCESS) ? |
| await nextValueFromServer(frame_loaded_key) : |
| await Promise.race([ |
| nextValueFromServer(frame_loaded_key), |
| new Promise((resolve) => { |
| t.step_timeout(() => resolve("timeout"), 10000 /* ms */); |
| }), |
| ]); |
| |
| assert_equals(result, expected); |
| } |
| |
| const iframeGrandparentTest = ({ |
| name, |
| grandparentServer, |
| child, |
| grandchild, |
| expected, |
| }) => promise_test_parallel(async (t) => { |
| // Allows running tests in parallel. |
| const grandparentUuid = token(); |
| const childUuid = token(); |
| const grandchildUuid = token(); |
| |
| const grandparentUrl = |
| resolveUrl("resources/executor.html", grandparentServer); |
| grandparentUrl.searchParams.set("executor-uuid", grandparentUuid); |
| |
| const childUrl = preflightUrl(child); |
| childUrl.searchParams.set("file", "executor.html"); |
| childUrl.searchParams.set("executor-uuid", childUuid); |
| |
| const grandchildUrl = preflightUrl(grandchild); |
| grandchildUrl.searchParams.set("file", "iframed.html"); |
| grandchildUrl.searchParams.set("iframe-uuid", grandchildUuid); |
| |
| const iframe = await appendIframe(t, document, grandparentUrl); |
| |
| const addChild = (url) => new Promise((resolve) => { |
| const child = document.createElement("iframe"); |
| child.src = url; |
| child.addEventListener("load", () => resolve(), { once: true }); |
| document.body.appendChild(child); |
| }); |
| |
| const grandparentCtx = new RemoteContext(grandparentUuid); |
| await grandparentCtx.execute_script(addChild, [childUrl]); |
| |
| // Add a blank grandchild frame inside the child. |
| // Apply a timeout to this step so that failures at this step do not block the |
| // execution of other tests. |
| const childCtx = new RemoteContext(childUuid); |
| await Promise.race([ |
| childCtx.execute_script(addChild, ["about:blank"]), |
| new Promise((resolve, reject) => t.step_timeout( |
| () => reject("timeout adding grandchild"), |
| 2000 /* ms */ |
| )), |
| ]); |
| |
| const messagePromise = futureMessage({ |
| filter: (data) => data.uuid === grandchildUuid, |
| }); |
| await grandparentCtx.execute_script((url) => { |
| const child = window.frames[0]; |
| const grandchild = child.frames[0]; |
| grandchild.location = url; |
| }, [grandchildUrl]); |
| |
| // The great-grandchild frame posts a message iff it loads successfully. |
| // There exists no interoperable way to check whether an iframe failed to |
| // load, so we use a timeout. |
| // See: https://github.com/whatwg/html/issues/125 |
| const result = await Promise.race([ |
| messagePromise.then((data) => data.message), |
| new Promise((resolve) => { |
| t.step_timeout(() => resolve("timeout"), 2000 /* ms */); |
| }), |
| ]); |
| |
| assert_equals(result, expected); |
| }, name); |
| |
| const WebsocketTestResult = { |
| SUCCESS: "open", |
| |
| // The code is a best guess. It is not yet entirely specified, so it may need |
| // to be changed in the future based on implementation experience. |
| FAILURE: "close: code 1006", |
| }; |
| |
| // Runs a websocket test. Attempts to open a websocket from `source` (in an |
| // iframe) to `target`, then checks that the result is as `expected`. |
| // |
| // Argument shape: |
| // |
| // { |
| // // Required. Passed to `sourceResolveOptions()`. |
| // source, |
| // |
| // // Required. |
| // target: { |
| // // Required. Target server. |
| // server, |
| // } |
| // |
| // // Required. Should be one of the values in `WebsocketTestResult`. |
| // expected, |
| // } |
| // |
| async function websocketTest(t, { source, target, expected }) { |
| const sourceUrl = |
| resolveUrl("resources/socket-opener.html", sourceResolveOptions(source)); |
| |
| const targetUrl = resolveUrl("/echo", target.server); |
| |
| const iframe = await appendIframe(t, document, sourceUrl); |
| |
| const reply = futureMessage(); |
| iframe.contentWindow.postMessage(targetUrl.href, "*"); |
| |
| assert_equals(await reply, expected); |
| } |
| |
| const WorkerScriptTestResult = { |
| SUCCESS: { loaded: true }, |
| FAILURE: { error: "unknown error" }, |
| }; |
| |
| function workerScriptUrl(target) { |
| const url = preflightUrl(target); |
| |
| url.searchParams.append("body", "postMessage({ loaded: true })") |
| url.searchParams.append("mime-type", "application/javascript") |
| |
| return url; |
| } |
| |
| async function workerScriptTest(t, { source, target, expected }) { |
| const sourceUrl = |
| resolveUrl("resources/worker-fetcher.html", sourceResolveOptions(source)); |
| |
| const targetUrl = workerScriptUrl(target); |
| |
| const iframe = await appendIframe(t, document, sourceUrl); |
| const reply = futureMessage(); |
| |
| iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); |
| |
| const { error, loaded } = await reply; |
| |
| assert_equals(error, expected.error, "worker error"); |
| assert_equals(loaded, expected.loaded, "response loaded"); |
| } |
| |
| async function nestedWorkerScriptTest(t, { source, target, expected }) { |
| const targetUrl = workerScriptUrl(target); |
| |
| const sourceUrl = resolveUrl( |
| "resources/worker-fetcher.js", sourceResolveOptions(source)); |
| sourceUrl.searchParams.append("url", targetUrl); |
| |
| // Iframe must be same-origin with the parent worker. |
| const iframeUrl = new URL("worker-fetcher.html", sourceUrl); |
| |
| const iframe = await appendIframe(t, document, iframeUrl); |
| const reply = futureMessage(); |
| |
| iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*"); |
| |
| const { error, loaded } = await reply; |
| |
| assert_equals(error, expected.error, "worker error"); |
| assert_equals(loaded, expected.loaded, "response loaded"); |
| } |
| |
| async function sharedWorkerScriptTest(t, { source, target, expected }) { |
| const sourceUrl = resolveUrl("resources/shared-worker-fetcher.html", |
| sourceResolveOptions(source)); |
| const targetUrl = preflightUrl(target); |
| targetUrl.searchParams.append( |
| "body", "onconnect = (e) => e.ports[0].postMessage({ loaded: true })") |
| targetUrl.searchParams.append("mime-type", "application/javascript") |
| |
| const iframe = await appendIframe(t, document, sourceUrl); |
| const reply = futureMessage(); |
| |
| iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); |
| |
| const { error, loaded } = await reply; |
| |
| assert_equals(error, expected.error, "worker error"); |
| assert_equals(loaded, expected.loaded, "response loaded"); |
| } |
| |
| // Results that may be expected in tests. |
| const WorkerFetchTestResult = { |
| SUCCESS: { status: 200, body: "success" }, |
| FAILURE: { error: "TypeError" }, |
| }; |
| |
| async function workerFetchTest(t, { source, target, expected }) { |
| const targetUrl = preflightUrl(target); |
| |
| const sourceUrl = |
| resolveUrl("resources/fetcher.js", sourceResolveOptions(source)); |
| sourceUrl.searchParams.append("url", targetUrl.href); |
| |
| const fetcherUrl = new URL("worker-fetcher.html", sourceUrl); |
| |
| const reply = futureMessage(); |
| const iframe = await appendIframe(t, document, fetcherUrl); |
| |
| iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*"); |
| |
| const { error, status, body } = await reply; |
| assert_equals(error, expected.error, "fetch error"); |
| assert_equals(status, expected.status, "response status"); |
| assert_equals(body, expected.body, "response body"); |
| } |
| |
| async function workerBlobFetchTest(t, { source, target, expected }) { |
| const targetUrl = preflightUrl(target); |
| |
| const fetcherUrl = resolveUrl( |
| 'resources/worker-blob-fetcher.html', sourceResolveOptions(source)); |
| |
| const reply = futureMessage(); |
| const iframe = await appendIframe(t, document, fetcherUrl); |
| |
| iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); |
| |
| const { error, status, body } = await reply; |
| assert_equals(error, expected.error, "fetch error"); |
| assert_equals(status, expected.status, "response status"); |
| assert_equals(body, expected.body, "response body"); |
| } |
| |
| async function sharedWorkerFetchTest(t, { source, target, expected }) { |
| const targetUrl = preflightUrl(target); |
| |
| const sourceUrl = |
| resolveUrl("resources/shared-fetcher.js", sourceResolveOptions(source)); |
| sourceUrl.searchParams.append("url", targetUrl.href); |
| |
| const fetcherUrl = new URL("shared-worker-fetcher.html", sourceUrl); |
| |
| const reply = futureMessage(); |
| const iframe = await appendIframe(t, document, fetcherUrl); |
| |
| iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*"); |
| |
| const { error, status, body } = await reply; |
| assert_equals(error, expected.error, "fetch error"); |
| assert_equals(status, expected.status, "response status"); |
| assert_equals(body, expected.body, "response body"); |
| } |
| |
| async function sharedWorkerBlobFetchTest(t, { source, target, expected }) { |
| const targetUrl = preflightUrl(target); |
| |
| const fetcherUrl = resolveUrl( |
| 'resources/shared-worker-blob-fetcher.html', |
| sourceResolveOptions(source)); |
| |
| const reply = futureMessage(); |
| const iframe = await appendIframe(t, document, fetcherUrl); |
| |
| iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); |
| |
| const { error, status, body } = await reply; |
| assert_equals(error, expected.error, "fetch error"); |
| assert_equals(status, expected.status, "response status"); |
| assert_equals(body, expected.body, "response body"); |
| } |
| |
| async function makeServiceWorkerTest(t, { source, target, expected, fetch_document=false }) { |
| const bridgeUrl = resolveUrl( |
| "resources/service-worker-bridge.html", |
| sourceResolveOptions({ server: source.server })); |
| |
| const scriptUrl = fetch_document? |
| resolveUrl("resources/service-worker-fetch-all.js", sourceResolveOptions(source)): |
| resolveUrl("resources/service-worker.js", sourceResolveOptions(source)); |
| |
| const realTargetUrl = preflightUrl(target); |
| |
| // Fetch a URL within the service worker's scope, but tell it which URL to |
| // really fetch. |
| const targetUrl = new URL("service-worker-proxy", scriptUrl); |
| targetUrl.searchParams.append("proxied-url", realTargetUrl.href); |
| |
| const iframe = await appendIframe(t, document, bridgeUrl); |
| |
| const request = (message) => { |
| const reply = futureMessage(); |
| iframe.contentWindow.postMessage(message, "*"); |
| return reply; |
| }; |
| |
| { |
| const { error, loaded } = await request({ |
| action: "register", |
| url: scriptUrl.href, |
| }); |
| |
| assert_equals(error, undefined, "register error"); |
| assert_true(loaded, "response loaded"); |
| } |
| |
| try { |
| const { controlled, numControllerChanges } = await request({ |
| action: "wait", |
| numControllerChanges: 1, |
| }); |
| |
| assert_equals(numControllerChanges, 1, "controller change"); |
| assert_true(controlled, "bridge script is controlled"); |
| |
| const { error, ok, body } = await request({ |
| action: "fetch", |
| url: targetUrl.href, |
| }); |
| |
| assert_equals(error, expected.error, "fetch error"); |
| assert_equals(ok, expected.ok, "response ok"); |
| assert_equals(body, expected.body, "response body"); |
| } finally { |
| // Always unregister the service worker. |
| const { error, unregistered } = await request({ |
| action: "unregister", |
| scope: new URL("./", scriptUrl).href, |
| }); |
| |
| assert_equals(error, undefined, "unregister error"); |
| assert_true(unregistered, "unregistered"); |
| } |
| } |