blob: 1bd850b63b06a10e9a75ce5e6d0df5d09a4d9e7f [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview This script runs within the guest page (a <webview>,
* <controlledframe>, or <iframe>) and initiates several types of network
* requests (Fetch, WebSocket, WebTransport) from various contexts (main page,
* Dedicated Worker, Shared Worker, Service Worker). It verifies if these
* requests were intercepted by the embedder by communicating with it via
* postMessage.
*/
// The timeout for tests that are expected to succeed.
const kLongObservationTimeoutMs = 2000;
// The timeout for tests that are expected to fail. This reduces wait time for
// known failures.
const kShortObservationTimeoutMs = 500;
const kParams = new URLSearchParams(window.location.search);
// The port for the WebSocket test server.
const kWebSocketPort = kParams.get('ws_port');
// The port for the WebTransport test server.
const kWebTransportPort = kParams.get('wt_port');
// A list of test titles that are expected to fail in the current environment.
// For these tests, we use a shorter timeout (kShortObservationTimeoutMs)
// instead of the default long timeout to avoid unnecessary delays and
// improve test efficiency and stability.
const kExpectedFailures = kParams.get('expected_failures').split(',');
const kWorkerScript = './request_interception_coverage_guest_worker.js';
const kWebSocketBaseUrl =
`ws://localhost:${kWebSocketPort}/echo-with-no-extension`;
const kWebTransportBaseUrl = `https://localhost:${kWebTransportPort}/echo`;
const kResultMessageType = 'TEST_RESULT';
const kObservedRequestMessageType = 'OBSERVED_REQUEST';
const kStartTestsMessageType = 'START_TESTS';
/**
* Loads a URL into a new <iframe>.
* @param {string} url
*/
async function loadIframe(url) {
const iframe = document.createElement('iframe');
iframe.src = url;
const loadPromise = new Promise((resolve) => {
iframe.addEventListener('load', resolve);
});
document.body.appendChild(iframe);
await loadPromise;
}
/**
* Runs a test within a Dedicated Worker.
* @param {Object} message Data to send to the worker.
* @param {string} workerScript
*/
async function runTestInDedicatedWorker(message, workerScript = kWorkerScript) {
const worker = new Worker(workerScript);
const result = await new Promise((resolve) => {
worker.addEventListener('message', (e) => {
resolve(e.data);
worker.terminate();
});
worker.postMessage(message);
});
if (result !== 'OK') {
throw new Error(`Dedicated Worker test failed: ${result}`);
}
}
/**
* Runs a test within a Shared Worker.
* @param {Object} message Data to send to the worker.
* @param {string} workerScript
*/
async function runTestInSharedWorker(message, workerScript = kWorkerScript) {
const worker =
new SharedWorker(workerScript, {name: Math.random().toString()});
worker.port.start();
const result = await new Promise((resolve) => {
worker.port.addEventListener('message', (e) => {
resolve(e.data);
worker.port.close();
});
worker.port.postMessage(message);
});
if (result !== 'OK') {
throw new Error(`Shared Worker test failed: ${result}`);
}
}
/**
* Runs a test within a Service Worker.
* @param {Object} message Data to send to the worker.
* @param {string} workerScript
*/
async function runTestInServiceWorker(message, workerScript = kWorkerScript) {
const scope = `./sw-scope-${Math.random()}/`;
const registration =
await navigator.serviceWorker.register(workerScript, {scope});
// Wait for the worker to be ready.
const worker =
registration.active || registration.waiting || registration.installing;
const channel = new MessageChannel();
const resultPromise = new Promise((resolve) => {
channel.port1.onmessage = (e) => resolve(e.data);
});
worker.postMessage(message, [channel.port2]);
const result = await resultPromise;
await registration.unregister();
if (result !== 'OK') {
throw new Error(`Service Worker test failed: ${result}`);
}
}
const kTestFunctions = [
{
title: 'subresource',
baseUrl: '/echo',
run: async (url) => {
await fetch(url);
}
},
{
title: 'main resource',
baseUrl: '/empty.html',
run: async (url) => {
await loadIframe(url);
}
},
{
title: 'Dedicated Worker script',
baseUrl: kWorkerScript,
run: async (url) => {
await runTestInDedicatedWorker({test: 'ping'}, url);
}
},
{
title: 'Shared Worker script',
baseUrl: kWorkerScript,
run: async (url) => {
await runTestInSharedWorker({test: 'ping'}, url);
}
},
{
title: 'Service Worker script',
baseUrl: kWorkerScript,
run: async (url) => {
await runTestInServiceWorker({test: 'ping'}, url);
}
},
{
title: 'Fetch from Dedicated Worker',
baseUrl: './echo',
run: async (url) => {
await runTestInDedicatedWorker({test: 'fetch', url});
}
},
{
title: 'Fetch from Shared Worker',
baseUrl: './echo',
run: async (url) => {
await runTestInSharedWorker({test: 'fetch', url});
}
},
{
title: 'Fetch from Service Worker',
baseUrl: './echo',
run: async (url) => {
await runTestInServiceWorker({test: 'fetch', url});
}
},
{
title: 'WebSocket',
baseUrl: kWebSocketBaseUrl,
run: async (url) => {
const ws = new WebSocket(url);
await new Promise((resolve) => ws.onopen = resolve);
ws.close();
}
},
{
title: 'WebSocket in Dedicated Worker',
baseUrl: kWebSocketBaseUrl,
run: async (url) => {
await runTestInDedicatedWorker({test: 'WebSocket', url});
}
},
{
title: 'WebSocket in Shared Worker',
baseUrl: kWebSocketBaseUrl,
run: async (url) => {
await runTestInSharedWorker({test: 'WebSocket', url});
}
},
{
title: 'WebSocket in Service Worker',
baseUrl: kWebSocketBaseUrl,
run: async (url) => {
await runTestInServiceWorker({test: 'WebSocket', url});
}
},
{
title: 'WebTransport',
baseUrl: kWebTransportBaseUrl,
run: async (url) => {
const transport = new WebTransport(url);
await transport.ready;
transport.close();
}
},
{
title: 'WebTransport in Dedicated Worker',
baseUrl: kWebTransportBaseUrl,
run: async (url) => {
await runTestInDedicatedWorker({test: 'WebTransport', url});
}
},
{
title: 'WebTransport in Shared Worker',
baseUrl: kWebTransportBaseUrl,
run: async (url) => {
await runTestInSharedWorker({test: 'WebTransport', url});
}
},
{
title: 'WebTransport in Service Worker',
baseUrl: kWebTransportBaseUrl,
run: async (url) => {
await runTestInServiceWorker({test: 'WebTransport', url});
}
},
{
title: 'Auth request',
baseUrl: '/auth-basic?password=pass',
event: 'onAuthRequired',
run: async (url) => {
await loadIframe(url);
}
},
];
const observedRequests = new Set();
const requestObservers = new Map();
/**
* Generates a unique key for a given URL and event.
* @param {string} url
* @param {string} event
* @return {string}
*/
function getObservationKey(url, event) {
return `${event}:${url}`;
}
/**
* Called when an observation signal is received from the embedder.
* @param {Object} data The observation data containing url and event.
*/
function onObservationReceived(data) {
const observedKey = getObservationKey(data.url, data.event);
observedRequests.add(observedKey);
const keyObservers = requestObservers.get(observedKey);
if (keyObservers) {
keyObservers.forEach(resolve => resolve());
requestObservers.delete(observedKey);
}
}
/**
* Waits until a request to the given URL is observed by the embedder.
* Interception is signaled by the embedder via postMessage.
* @param {string} url The URL to watch for.
* @param {string} event The expected event that should observe the request.
* @param {number} timeout The timeout in milliseconds.
*/
async function waitUntilRequestObserved(url, event, timeout) {
const key = getObservationKey(url, event);
if (observedRequests.has(key)) {
return;
}
const observers = requestObservers.get(key) || [];
requestObservers.set(key, observers);
await new Promise((resolve, reject) => {
observers.push(resolve);
setTimeout(() => reject(new Error(`not observed by ${event}`)), timeout);
});
}
/**
* Runs the request interception coverage tests.
* @param {MessagePort} resultPort The port to send the result to.
*/
async function runCoverageTests(resultPort) {
const runTest = async (test) => {
const url = new URL(test.baseUrl, location.href);
url.searchParams.append('rand', Math.random().toString());
await test.run(url.href);
const expectedEvent = test.event || 'onBeforeRequest';
const timeout = kExpectedFailures.includes(test.title) ?
kShortObservationTimeoutMs :
kLongObservationTimeoutMs;
await waitUntilRequestObserved(url.href, expectedEvent, timeout);
};
const results = await Promise.allSettled(kTestFunctions.map(runTest));
const failures =
results
.map(
(res, i) => res.status === 'rejected' ?
`${kTestFunctions[i].title}: ${res.reason.message}` :
null)
.filter(f => f !== null);
resultPort.postMessage({
type: kResultMessageType,
result: failures.length === 0 ? 'Done' : failures.join('\n')
});
}
window.addEventListener('message', async (e) => {
if (e.data.type === kStartTestsMessageType) {
await runCoverageTests(e.ports[0]);
} else if (e.data.type === kObservedRequestMessageType) {
onObservationReceived(e.data.data);
}
});