-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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}
- Loading branch information
1 parent
ad48ce3
commit d6ae6c8
Showing
6 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
4 changes: 4 additions & 0 deletions
4
service-workers/service-worker/tentative/static-router/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
1 change: 1 addition & 0 deletions
1
service-workers/service-worker/tentative/static-router/resources/direct.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Network |
3 changes: 3 additions & 0 deletions
3
service-workers/service-worker/tentative/static-router/resources/simple.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
19 changes: 19 additions & 0 deletions
19
service-workers/service-worker/tentative/static-router/resources/static-router-sw.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
}); |
303 changes: 303 additions & 0 deletions
303
service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
48 changes: 48 additions & 0 deletions
48
service-workers/service-worker/tentative/static-router/static-router-subresource.https.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |