Skip to content

Commit d6ae6c8

Browse files
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 <[email protected]> Reviewed-by: Minoru Chikamune <[email protected]> Reviewed-by: Kouhei Ueno <[email protected]> Commit-Queue: Yoshisato Yanagisawa <[email protected]> Reviewed-by: Kent Tamura <[email protected]> Cr-Commit-Position: refs/heads/main@{#1171605}
1 parent ad48ce3 commit d6ae6c8

File tree

6 files changed

+378
-0
lines changed

6 files changed

+378
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
A test stuite for the ServiceWorker Static Routing API.
2+
3+
WICG proposal: https://github.com/WICG/proposals/issues/102
4+
Specification PR: https://github.com/w3c/ServiceWorker/pull/1686
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Network
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<!DOCTYPE html>
2+
<title>Simple</title>
3+
Here's a simple html file.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
self.addEventListener('install', e => {
4+
e.registerRouter({
5+
condition: {urlPattern: "*.txt"},
6+
source: "network"
7+
});
8+
self.skipWaiting();
9+
});
10+
11+
self.addEventListener('activate', e => {
12+
e.waitUntil(clients.claim());
13+
});
14+
15+
self.addEventListener('fetch', function(event) {
16+
const url = new URL(event.request.url);
17+
const nonce = url.searchParams.get('nonce');
18+
event.respondWith(new Response(nonce));
19+
});
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
// Copied from
2+
// service-workers/service-worker/resources/testharness-helpers.js to be used under tentative.
3+
4+
// Adapter for testharness.js-style tests with Service Workers
5+
6+
/**
7+
* @param options an object that represents RegistrationOptions except for scope.
8+
* @param options.type a WorkerType.
9+
* @param options.updateViaCache a ServiceWorkerUpdateViaCache.
10+
* @see https://w3c.github.io/ServiceWorker/#dictdef-registrationoptions
11+
*/
12+
function service_worker_unregister_and_register(test, url, scope, options) {
13+
if (!scope || scope.length == 0)
14+
return Promise.reject(new Error('tests must define a scope'));
15+
16+
if (options && options.scope)
17+
return Promise.reject(new Error('scope must not be passed in options'));
18+
19+
options = Object.assign({ scope: scope }, options);
20+
return service_worker_unregister(test, scope)
21+
.then(function() {
22+
return navigator.serviceWorker.register(url, options);
23+
})
24+
.catch(unreached_rejection(test,
25+
'unregister and register should not fail'));
26+
}
27+
28+
// This unregisters the registration that precisely matches scope. Use this
29+
// when unregistering by scope. If no registration is found, it just resolves.
30+
function service_worker_unregister(test, scope) {
31+
var absoluteScope = (new URL(scope, window.location).href);
32+
return navigator.serviceWorker.getRegistration(scope)
33+
.then(function(registration) {
34+
if (registration && registration.scope === absoluteScope)
35+
return registration.unregister();
36+
})
37+
.catch(unreached_rejection(test, 'unregister should not fail'));
38+
}
39+
40+
function service_worker_unregister_and_done(test, scope) {
41+
return service_worker_unregister(test, scope)
42+
.then(test.done.bind(test));
43+
}
44+
45+
function unreached_fulfillment(test, prefix) {
46+
return test.step_func(function(result) {
47+
var error_prefix = prefix || 'unexpected fulfillment';
48+
assert_unreached(error_prefix + ': ' + result);
49+
});
50+
}
51+
52+
// Rejection-specific helper that provides more details
53+
function unreached_rejection(test, prefix) {
54+
return test.step_func(function(error) {
55+
var reason = error.message || error.name || error;
56+
var error_prefix = prefix || 'unexpected rejection';
57+
assert_unreached(error_prefix + ': ' + reason);
58+
});
59+
}
60+
61+
/**
62+
* Adds an iframe to the document and returns a promise that resolves to the
63+
* iframe when it finishes loading. The caller is responsible for removing the
64+
* iframe later if needed.
65+
*
66+
* @param {string} url
67+
* @returns {HTMLIFrameElement}
68+
*/
69+
function with_iframe(url) {
70+
return new Promise(function(resolve) {
71+
var frame = document.createElement('iframe');
72+
frame.className = 'test-iframe';
73+
frame.src = url;
74+
frame.onload = function() { resolve(frame); };
75+
document.body.appendChild(frame);
76+
});
77+
}
78+
79+
function normalizeURL(url) {
80+
return new URL(url, self.location).toString().replace(/#.*$/, '');
81+
}
82+
83+
function wait_for_update(test, registration) {
84+
if (!registration || registration.unregister == undefined) {
85+
return Promise.reject(new Error(
86+
'wait_for_update must be passed a ServiceWorkerRegistration'));
87+
}
88+
89+
return new Promise(test.step_func(function(resolve) {
90+
var handler = test.step_func(function() {
91+
registration.removeEventListener('updatefound', handler);
92+
resolve(registration.installing);
93+
});
94+
registration.addEventListener('updatefound', handler);
95+
}));
96+
}
97+
98+
// Return true if |state_a| is more advanced than |state_b|.
99+
function is_state_advanced(state_a, state_b) {
100+
if (state_b === 'installing') {
101+
switch (state_a) {
102+
case 'installed':
103+
case 'activating':
104+
case 'activated':
105+
case 'redundant':
106+
return true;
107+
}
108+
}
109+
110+
if (state_b === 'installed') {
111+
switch (state_a) {
112+
case 'activating':
113+
case 'activated':
114+
case 'redundant':
115+
return true;
116+
}
117+
}
118+
119+
if (state_b === 'activating') {
120+
switch (state_a) {
121+
case 'activated':
122+
case 'redundant':
123+
return true;
124+
}
125+
}
126+
127+
if (state_b === 'activated') {
128+
switch (state_a) {
129+
case 'redundant':
130+
return true;
131+
}
132+
}
133+
return false;
134+
}
135+
136+
function wait_for_state(test, worker, state) {
137+
if (!worker || worker.state == undefined) {
138+
return Promise.reject(new Error(
139+
'wait_for_state needs a ServiceWorker object to be passed.'));
140+
}
141+
if (worker.state === state)
142+
return Promise.resolve(state);
143+
144+
if (is_state_advanced(worker.state, state)) {
145+
return Promise.reject(new Error(
146+
`Waiting for ${state} but the worker is already ${worker.state}.`));
147+
}
148+
return new Promise(test.step_func(function(resolve, reject) {
149+
worker.addEventListener('statechange', test.step_func(function() {
150+
if (worker.state === state)
151+
resolve(state);
152+
153+
if (is_state_advanced(worker.state, state)) {
154+
reject(new Error(
155+
`The state of the worker becomes ${worker.state} while waiting` +
156+
`for ${state}.`));
157+
}
158+
}));
159+
}));
160+
}
161+
162+
// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url|
163+
// is the service worker script URL. This function:
164+
// - Instantiates a new test with the description specified in |description|.
165+
// The test will succeed if the specified service worker can be successfully
166+
// registered and installed.
167+
// - Creates a new ServiceWorker registration with a scope unique to the current
168+
// document URL. Note that this doesn't allow more than one
169+
// service_worker_test() to be run from the same document.
170+
// - Waits for the new worker to begin installing.
171+
// - Imports tests results from tests running inside the ServiceWorker.
172+
function service_worker_test(url, description) {
173+
// If the document URL is https://example.com/document and the script URL is
174+
// https://example.com/script/worker.js, then the scope would be
175+
// https://example.com/script/scope/document.
176+
var scope = new URL('scope' + window.location.pathname,
177+
new URL(url, window.location)).toString();
178+
promise_test(function(test) {
179+
return service_worker_unregister_and_register(test, url, scope)
180+
.then(function(registration) {
181+
add_completion_callback(function() {
182+
registration.unregister();
183+
});
184+
return wait_for_update(test, registration)
185+
.then(function(worker) {
186+
return fetch_tests_from_worker(worker);
187+
});
188+
});
189+
}, description);
190+
}
191+
192+
function base_path() {
193+
return location.pathname.replace(/\/[^\/]*$/, '/');
194+
}
195+
196+
function test_login(test, origin, username, password, cookie) {
197+
return new Promise(function(resolve, reject) {
198+
with_iframe(
199+
origin + base_path() +
200+
'resources/fetch-access-control-login.html')
201+
.then(test.step_func(function(frame) {
202+
var channel = new MessageChannel();
203+
channel.port1.onmessage = test.step_func(function() {
204+
frame.remove();
205+
resolve();
206+
});
207+
frame.contentWindow.postMessage(
208+
{username: username, password: password, cookie: cookie},
209+
origin, [channel.port2]);
210+
}));
211+
});
212+
}
213+
214+
function test_websocket(test, frame, url) {
215+
return new Promise(function(resolve, reject) {
216+
var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']);
217+
var openCalled = false;
218+
ws.addEventListener('open', test.step_func(function(e) {
219+
assert_equals(ws.readyState, 1, "The WebSocket should be open");
220+
openCalled = true;
221+
ws.close();
222+
}), true);
223+
224+
ws.addEventListener('close', test.step_func(function(e) {
225+
assert_true(openCalled, "The WebSocket should be closed after being opened");
226+
resolve();
227+
}), true);
228+
229+
ws.addEventListener('error', reject);
230+
});
231+
}
232+
233+
function login_https(test) {
234+
var host_info = get_host_info();
235+
return test_login(test, host_info.HTTPS_REMOTE_ORIGIN,
236+
'username1s', 'password1s', 'cookie1')
237+
.then(function() {
238+
return test_login(test, host_info.HTTPS_ORIGIN,
239+
'username2s', 'password2s', 'cookie2');
240+
});
241+
}
242+
243+
function websocket(test, frame) {
244+
return test_websocket(test, frame, get_websocket_url());
245+
}
246+
247+
function get_websocket_url() {
248+
return 'wss://{{host}}:{{ports[wss][0]}}/echo';
249+
}
250+
251+
// The navigator.serviceWorker.register() method guarantees that the newly
252+
// installing worker is available as registration.installing when its promise
253+
// resolves. However some tests test installation using a <link> element where
254+
// it is possible for the installing worker to have already become the waiting
255+
// or active worker. So this method is used to get the newest worker when these
256+
// tests need access to the ServiceWorker itself.
257+
function get_newest_worker(registration) {
258+
if (registration.installing)
259+
return registration.installing;
260+
if (registration.waiting)
261+
return registration.waiting;
262+
if (registration.active)
263+
return registration.active;
264+
}
265+
266+
function register_using_link(script, options) {
267+
var scope = options.scope;
268+
var link = document.createElement('link');
269+
link.setAttribute('rel', 'serviceworker');
270+
link.setAttribute('href', script);
271+
link.setAttribute('scope', scope);
272+
document.getElementsByTagName('head')[0].appendChild(link);
273+
return new Promise(function(resolve, reject) {
274+
link.onload = resolve;
275+
link.onerror = reject;
276+
})
277+
.then(() => navigator.serviceWorker.getRegistration(scope));
278+
}
279+
280+
function with_sandboxed_iframe(url, sandbox) {
281+
return new Promise(function(resolve) {
282+
var frame = document.createElement('iframe');
283+
frame.sandbox = sandbox;
284+
frame.src = url;
285+
frame.onload = function() { resolve(frame); };
286+
document.body.appendChild(frame);
287+
});
288+
}
289+
290+
// Registers, waits for activation, then unregisters on a sample scope.
291+
//
292+
// This can be used to wait for a period of time needed to register,
293+
// activate, and then unregister a service worker. When checking that
294+
// certain behavior does *NOT* happen, this is preferable to using an
295+
// arbitrary delay.
296+
async function wait_for_activation_on_sample_scope(t, window_or_workerglobalscope) {
297+
const script = '/service-workers/service-worker/resources/empty-worker.js';
298+
const scope = 'resources/there/is/no/there/there?' + Date.now();
299+
let registration = await window_or_workerglobalscope.navigator.serviceWorker.register(script, { scope });
300+
await wait_for_state(t, registration.installing, 'activated');
301+
await registration.unregister();
302+
}
303+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>Static Router: simply skip fetch handler if pattern matches</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<script src="resources/test-helpers.sub.js"></script>
7+
<body>
8+
<script>
9+
const SCRIPT = 'resources/static-router-sw.js';
10+
const SCOPE = 'resources/';
11+
const HTML_FILE = 'resources/simple.html';
12+
const TXT_FILE = 'resources/direct.txt';
13+
14+
// Register a service worker, then create an iframe at url.
15+
function iframeTest(url, callback, name) {
16+
return promise_test(async t => {
17+
const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
18+
add_completion_callback(() => reg.unregister());
19+
await wait_for_state(t, reg.installing, 'activated');
20+
const iframe = await with_iframe(url);
21+
const iwin = iframe.contentWindow;
22+
t.add_cleanup(() => iframe.remove());
23+
await callback(t, iwin);
24+
}, name);
25+
}
26+
27+
function randomString() {
28+
let result = "";
29+
for (let i = 0; i < 5; i++) {
30+
result += String.fromCharCode(97 + Math.floor(Math.random() * 26));
31+
}
32+
return result;
33+
}
34+
35+
iframeTest(HTML_FILE, async (t, iwin) => {
36+
const rnd = randomString();
37+
const response = await iwin.fetch('?nonce=' + rnd);
38+
assert_equals(await response.text(), rnd);
39+
}, 'Subresource load not matched with the condition');
40+
41+
iframeTest(TXT_FILE, async (t, iwin) => {
42+
const rnd = randomString();
43+
const response = await iwin.fetch('?nonce=' + rnd);
44+
assert_equals(await response.text(), "Network\n");
45+
}, 'Subresource load matched with the condition');
46+
47+
</script>
48+
</body>

0 commit comments

Comments
 (0)