Skip to content

Commit

Permalink
WPT: ServiceWorker static routing API for subresource loads.
Browse files Browse the repository at this point in the history
This CL adds the Web Platform Tests to test ServiceWorker static
routing API for subresources.

WICG proposal: WICG/proposals#102
Spec PR: w3c/ServiceWorker#1686

Change-Id: I7379d85b5a2208f248878abe9d1a920ad97d47ab
Bug: 1371756
  • Loading branch information
yoshisatoyanagisawa authored and chromium-wpt-export-bot committed Jul 18, 2023
1 parent ad48ce3 commit a98cc8c
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
A test stuite for the ServiceWorker Static Routing API.

WICG proposal: https://github.com/WICG/proposals/issues/102
Specification PR: https://github.com/w3c/ServiceWorker/pull/1686
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Network
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!DOCTYPE html>
<title>Simple</title>
Here's a simple html file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

self.addEventListener('install', e => {
e.registerRouter({
condition: {urlPattern: "*.txt"},
source: "network"
});
self.skipWaiting();
});

self.addEventListener('activate', e => {
e.waitUntil(clients.claim());
});

self.addEventListener('fetch', function(event) {
const url = new URL(event.request.url);
const nonce = url.searchParams.get('nonce');
event.respondWith(new Response(nonce));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
// Copied from
// service-workers/service-worker/resources/testharness-helpers.js to be used under tentative.

// Adapter for testharness.js-style tests with Service Workers

/**
* @param options an object that represents RegistrationOptions except for scope.
* @param options.type a WorkerType.
* @param options.updateViaCache a ServiceWorkerUpdateViaCache.
* @see https://w3c.github.io/ServiceWorker/#dictdef-registrationoptions
*/
function service_worker_unregister_and_register(test, url, scope, options) {
if (!scope || scope.length == 0)
return Promise.reject(new Error('tests must define a scope'));

if (options && options.scope)
return Promise.reject(new Error('scope must not be passed in options'));

options = Object.assign({ scope: scope }, options);
return service_worker_unregister(test, scope)
.then(function() {
return navigator.serviceWorker.register(url, options);
})
.catch(unreached_rejection(test,
'unregister and register should not fail'));
}

// This unregisters the registration that precisely matches scope. Use this
// when unregistering by scope. If no registration is found, it just resolves.
function service_worker_unregister(test, scope) {
var absoluteScope = (new URL(scope, window.location).href);
return navigator.serviceWorker.getRegistration(scope)
.then(function(registration) {
if (registration && registration.scope === absoluteScope)
return registration.unregister();
})
.catch(unreached_rejection(test, 'unregister should not fail'));
}

function service_worker_unregister_and_done(test, scope) {
return service_worker_unregister(test, scope)
.then(test.done.bind(test));
}

function unreached_fulfillment(test, prefix) {
return test.step_func(function(result) {
var error_prefix = prefix || 'unexpected fulfillment';
assert_unreached(error_prefix + ': ' + result);
});
}

// Rejection-specific helper that provides more details
function unreached_rejection(test, prefix) {
return test.step_func(function(error) {
var reason = error.message || error.name || error;
var error_prefix = prefix || 'unexpected rejection';
assert_unreached(error_prefix + ': ' + reason);
});
}

/**
* Adds an iframe to the document and returns a promise that resolves to the
* iframe when it finishes loading. The caller is responsible for removing the
* iframe later if needed.
*
* @param {string} url
* @returns {HTMLIFrameElement}
*/
function with_iframe(url) {
return new Promise(function(resolve) {
var frame = document.createElement('iframe');
frame.className = 'test-iframe';
frame.src = url;
frame.onload = function() { resolve(frame); };
document.body.appendChild(frame);
});
}

function normalizeURL(url) {
return new URL(url, self.location).toString().replace(/#.*$/, '');
}

function wait_for_update(test, registration) {
if (!registration || registration.unregister == undefined) {
return Promise.reject(new Error(
'wait_for_update must be passed a ServiceWorkerRegistration'));
}

return new Promise(test.step_func(function(resolve) {
var handler = test.step_func(function() {
registration.removeEventListener('updatefound', handler);
resolve(registration.installing);
});
registration.addEventListener('updatefound', handler);
}));
}

// Return true if |state_a| is more advanced than |state_b|.
function is_state_advanced(state_a, state_b) {
if (state_b === 'installing') {
switch (state_a) {
case 'installed':
case 'activating':
case 'activated':
case 'redundant':
return true;
}
}

if (state_b === 'installed') {
switch (state_a) {
case 'activating':
case 'activated':
case 'redundant':
return true;
}
}

if (state_b === 'activating') {
switch (state_a) {
case 'activated':
case 'redundant':
return true;
}
}

if (state_b === 'activated') {
switch (state_a) {
case 'redundant':
return true;
}
}
return false;
}

function wait_for_state(test, worker, state) {
if (!worker || worker.state == undefined) {
return Promise.reject(new Error(
'wait_for_state needs a ServiceWorker object to be passed.'));
}
if (worker.state === state)
return Promise.resolve(state);

if (is_state_advanced(worker.state, state)) {
return Promise.reject(new Error(
`Waiting for ${state} but the worker is already ${worker.state}.`));
}
return new Promise(test.step_func(function(resolve, reject) {
worker.addEventListener('statechange', test.step_func(function() {
if (worker.state === state)
resolve(state);

if (is_state_advanced(worker.state, state)) {
reject(new Error(
`The state of the worker becomes ${worker.state} while waiting` +
`for ${state}.`));
}
}));
}));
}

// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url|
// is the service worker script URL. This function:
// - Instantiates a new test with the description specified in |description|.
// The test will succeed if the specified service worker can be successfully
// registered and installed.
// - Creates a new ServiceWorker registration with a scope unique to the current
// document URL. Note that this doesn't allow more than one
// service_worker_test() to be run from the same document.
// - Waits for the new worker to begin installing.
// - Imports tests results from tests running inside the ServiceWorker.
function service_worker_test(url, description) {
// If the document URL is https://example.com/document and the script URL is
// https://example.com/script/worker.js, then the scope would be
// https://example.com/script/scope/document.
var scope = new URL('scope' + window.location.pathname,
new URL(url, window.location)).toString();
promise_test(function(test) {
return service_worker_unregister_and_register(test, url, scope)
.then(function(registration) {
add_completion_callback(function() {
registration.unregister();
});
return wait_for_update(test, registration)
.then(function(worker) {
return fetch_tests_from_worker(worker);
});
});
}, description);
}

function base_path() {
return location.pathname.replace(/\/[^\/]*$/, '/');
}

function test_login(test, origin, username, password, cookie) {
return new Promise(function(resolve, reject) {
with_iframe(
origin + base_path() +
'resources/fetch-access-control-login.html')
.then(test.step_func(function(frame) {
var channel = new MessageChannel();
channel.port1.onmessage = test.step_func(function() {
frame.remove();
resolve();
});
frame.contentWindow.postMessage(
{username: username, password: password, cookie: cookie},
origin, [channel.port2]);
}));
});
}

function test_websocket(test, frame, url) {
return new Promise(function(resolve, reject) {
var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']);
var openCalled = false;
ws.addEventListener('open', test.step_func(function(e) {
assert_equals(ws.readyState, 1, "The WebSocket should be open");
openCalled = true;
ws.close();
}), true);

ws.addEventListener('close', test.step_func(function(e) {
assert_true(openCalled, "The WebSocket should be closed after being opened");
resolve();
}), true);

ws.addEventListener('error', reject);
});
}

function login_https(test) {
var host_info = get_host_info();
return test_login(test, host_info.HTTPS_REMOTE_ORIGIN,
'username1s', 'password1s', 'cookie1')
.then(function() {
return test_login(test, host_info.HTTPS_ORIGIN,
'username2s', 'password2s', 'cookie2');
});
}

function websocket(test, frame) {
return test_websocket(test, frame, get_websocket_url());
}

function get_websocket_url() {
return 'wss://{{host}}:{{ports[wss][0]}}/echo';
}

// The navigator.serviceWorker.register() method guarantees that the newly
// installing worker is available as registration.installing when its promise
// resolves. However some tests test installation using a <link> element where
// it is possible for the installing worker to have already become the waiting
// or active worker. So this method is used to get the newest worker when these
// tests need access to the ServiceWorker itself.
function get_newest_worker(registration) {
if (registration.installing)
return registration.installing;
if (registration.waiting)
return registration.waiting;
if (registration.active)
return registration.active;
}

function register_using_link(script, options) {
var scope = options.scope;
var link = document.createElement('link');
link.setAttribute('rel', 'serviceworker');
link.setAttribute('href', script);
link.setAttribute('scope', scope);
document.getElementsByTagName('head')[0].appendChild(link);
return new Promise(function(resolve, reject) {
link.onload = resolve;
link.onerror = reject;
})
.then(() => navigator.serviceWorker.getRegistration(scope));
}

function with_sandboxed_iframe(url, sandbox) {
return new Promise(function(resolve) {
var frame = document.createElement('iframe');
frame.sandbox = sandbox;
frame.src = url;
frame.onload = function() { resolve(frame); };
document.body.appendChild(frame);
});
}

// Registers, waits for activation, then unregisters on a sample scope.
//
// This can be used to wait for a period of time needed to register,
// activate, and then unregister a service worker. When checking that
// certain behavior does *NOT* happen, this is preferable to using an
// arbitrary delay.
async function wait_for_activation_on_sample_scope(t, window_or_workerglobalscope) {
const script = '/service-workers/service-worker/resources/empty-worker.js';
const scope = 'resources/there/is/no/there/there?' + Date.now();
let registration = await window_or_workerglobalscope.navigator.serviceWorker.register(script, { scope });
await wait_for_state(t, registration.installing, 'activated');
await registration.unregister();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Static Router: simply skip fetch handler if pattern matches</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/test-helpers.sub.js"></script>
<body>
<script>
const SCRIPT = 'resources/static-router-sw.js';
const SCOPE = 'resources/';
const HTML_FILE = 'resources/simple.html';
const TXT_FILE = 'resources/direct.txt';

// Register a service worker, then create an iframe at url.
function iframeTest(url, callback, name) {
return promise_test(async t => {
const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
add_completion_callback(() => reg.unregister());
await wait_for_state(t, reg.installing, 'activated');
const iframe = await with_iframe(url);
const iwin = iframe.contentWindow;
t.add_cleanup(() => iframe.remove());
await callback(t, iwin);
}, name);
}

function randomString() {
let result = "";
for (let i = 0; i < 5; i++) {
result += String.fromCharCode(97 + Math.floor(Math.random() * 26));
}
return result;
}

iframeTest(HTML_FILE, async (t, iwin) => {
const rnd = randomString();
const response = await iwin.fetch('?nonce=' + rnd);
assert_equals(await response.text(), rnd);
}, 'Subresource load not matched with the condition');

iframeTest(TXT_FILE, async (t, iwin) => {
const rnd = randomString();
const response = await iwin.fetch('?nonce=' + rnd);
assert_equals(await response.text(), "Network\n");
}, 'Subresource load matched with the condition');

</script>
</body>

0 comments on commit a98cc8c

Please sign in to comment.