<!DOCTYPE html>
<title>Service Worker: controlling Worker/SharedWorker</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/test-helpers.sub.js"></script>
<body>
<script>
// This tests service worker interception for worker clients, when the request
// for the worker script goes through redirects. For example, a request can go
// through a chain of URLs like A -> B -> C -> D and each URL might fall in the
// scope of a different service worker, if any.
// The two key questions are:
// 1. Upon a redirect from A -> B, should a service worker for scope B
// intercept the request?
// 2. After the final response, which service worker controls the resulting
// client?
//
// The standard prescribes the following:
// 1. The service worker for scope B intercepts the redirect. *However*, once a
// request falls back to network (i.e., a service worker did not call
// respondWith()) and a redirect is then received from network, no service
// worker should intercept that redirect or any subsequent redirects.
// 2. The final service worker that got a fetch event (or would have, in the
// case of a non-fetch-event worker) becomes the controller of the client.
//
// The standard may change later, see:
// https://github.com/w3c/ServiceWorker/issues/1289
//
// The basic test setup is:
// 1. Page registers service workers for scope1 and scope2.
// 2. Page requests a worker from scope1.
// 3. The request is redirected to scope2 or out-of-scope.
// 4. The worker posts message to the page describing where the final response
// was served from (service worker or network).
// 5. The worker does an importScripts() and fetch(), and posts back the
// responses, which describe where the responses where served from.
// Globals for easier cleanup.
const scope1 = 'resources/scope1';
const scope2 = 'resources/scope2';
let frame;
function get_message_from_worker(port) {
return new Promise(resolve => {
port.onmessage = evt => {
resolve(evt.data);
}
});
}
async function cleanup() {
if (frame)
frame.remove();
const reg1 = await navigator.serviceWorker.getRegistration(scope1);
if (reg1)
await reg1.unregister();
const reg2 = await navigator.serviceWorker.getRegistration(scope2);
if (reg2)
await reg2.unregister();
}
// Builds the worker script URL, which encodes information about where
// to redirect to. The URL falls in sw1's scope.
//
// - |redirector| is "network" or "serviceworker". If "serviceworker", sw1 will
// respondWith() a redirect. Otherwise, it falls back to network and the server
// responds with a redirect.
// - |redirect_location| is "scope2" or "out-of-scope". If "scope2", the
// redirect ends up in sw2's scope2. Otherwise it's out of scope.
function build_worker_url(redirector, redirect_location) {
let redirect_path;
// Set path to redirect.py, a file on the server that serves
// a redirect. When sw1 sees this URL, it falls back to network.
if (redirector == 'network')
redirector_path = 'redirect.py';
// Set path to 'sw-redirect', to tell the service worker
// to respond with redirect.
else if (redirector == 'serviceworker')
redirector_path = 'sw-redirect';
let redirect_to = base_path() + 'resources/';
// Append "scope2/" to redirect_to, so the redirect falls in scope2.
// Otherwise no change is needed, as the parent "resources/" directory is
// used, and is out-of-scope.
if (redirect_location == 'scope2')
redirect_to += 'scope2/';
// Append the name of the file which serves the worker script.
redirect_to += 'worker_interception_redirect_webworker.py';
return `scope1/${redirector_path}?Redirect=${redirect_to}`
}
promise_test(async t => {
await cleanup();
const service_worker = 'resources/worker-interception-redirect-serviceworker.js';
const registration1 = await navigator.serviceWorker.register(service_worker, {scope: scope1});
await wait_for_state(t, registration1.installing, 'activated');
const registration2 = await navigator.serviceWorker.register(service_worker, {scope: scope2});
await wait_for_state(t, registration2.installing, 'activated');
promise_test(t => {
return cleanup();
}, 'cleanup global state');
}, 'initialize global state');
async function worker_redirect_test(worker_request_url,
worker_expected_url,
expected_main_resource_message,
expected_import_scripts_message,
expected_fetch_message,
description) {
for (const workerType of ['DedicatedWorker', 'SharedWorker']) {
for (const type of ['classic', 'module']) {
promise_test(async t => {
// Create a frame to load the worker from. This way we can remove the
// frame to destroy the worker client when the test is done.
frame = await with_iframe('resources/blank.html');
t.add_cleanup(() => { frame.remove(); });
// Start the worker.
let w;
let port;
if (workerType === 'DedicatedWorker') {
w = new frame.contentWindow.Worker(worker_request_url, {type});
port = w;
} else {
w = new frame.contentWindow.SharedWorker(worker_request_url, {type});
port = w.port;
w.port.start();
}
w.onerror = t.unreached_func('Worker error');
// Expect a message from the worker indicating which service worker
// provided the response for the worker script request, if any.
const data = await get_message_from_worker(port);
// The worker does an importScripts(). Expect a message from the worker
// indicating which service worker provided the response for the
// importScripts(), if any.
const import_scripts_message = await get_message_from_worker(port);
test(() => {
if (type === 'classic') {
assert_equals(import_scripts_message,
expected_import_scripts_message);
} else {
assert_equals(import_scripts_message, 'importScripts failed');
}
}, `${description} (${type} ${workerType}, importScripts())`);
// The worker does a fetch(). Expect a message from the worker
// indicating which service worker provided the response for the
// fetch(), if any.
const fetch_message = await get_message_from_worker(port);
test(() => {
assert_equals(fetch_message, expected_fetch_message);
}, `${description} (${type} ${workerType}, fetch())`);
// Expect a message from the worker indicating |self.location|.
const worker_actual_url = await get_message_from_worker(port);
test(() => {
assert_equals(
worker_actual_url,
(new URL(worker_expected_url, location.href)).toString(),
'location.href');
}, `${description} (${type} ${workerType}, location.href)`);
assert_equals(data, expected_main_resource_message);
}, `${description} (${type} ${workerType})`);
}
}
}
// request to sw1 scope gets network redirect to sw2 scope
worker_redirect_test(
build_worker_url('network', 'scope2'),
'resources/scope2/worker_interception_redirect_webworker.py',
'the worker script was served from network',
'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/scope2/import-scripts-echo.py',
'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/scope2/simple.txt',
'Case #1: network scope1->scope2');
// request to sw1 scope gets network redirect to out-of-scope
worker_redirect_test(
build_worker_url('network', 'out-scope'),
'resources/worker_interception_redirect_webworker.py',
'the worker script was served from network',
'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
'Case #2: network scope1->out-scope');
// request to sw1 scope gets service-worker redirect to sw2 scope
worker_redirect_test(
build_worker_url('serviceworker', 'scope2'),
'resources/subdir/worker_interception_redirect_webworker.py?greeting=sw2%20saw%20the%20request%20for%20the%20worker%20script',
'sw2 saw the request for the worker script',
'sw2 saw importScripts from the worker: /service-workers/service-worker/resources/subdir/import-scripts-echo.py',
'fetch(): sw2 saw the fetch from the worker: /service-workers/service-worker/resources/subdir/simple.txt',
'Case #3: sw scope1->scope2');
// request to sw1 scope gets service-worker redirect to out-of-scope
worker_redirect_test(
build_worker_url('serviceworker', 'out-scope'),
'resources/worker_interception_redirect_webworker.py',
'the worker script was served from network',
'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
'Case #4: sw scope1->out-scope');
</script>
</body>