Skip to content

Commit

Permalink
Bug 1843478 [wpt PR 41038] - WPT: ServiceWorker static routing API fo…
Browse files Browse the repository at this point in the history
…r subresource loads., a=testonly

Automatic update from web-platform-tests
WPT: ServiceWorker static routing API for subresource loads.

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
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4664026
Reviewed-by: Shunya Shishido <sisidovski@chromium.org>
Reviewed-by: Minoru Chikamune <chikamune@chromium.org>
Reviewed-by: Kouhei Ueno <kouhei@chromium.org>
Commit-Queue: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1171605}

--

wpt-commits: d6ae6c83c103fdf28aefd9fb3445470c96c0e17b
wpt-pr: 41038
  • Loading branch information
yoshisatoyanagisawa authored and moz-wptsync-bot committed Jul 22, 2023
1 parent 5ae0770 commit b8e90af
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 b8e90af

Please sign in to comment.