diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/service-workers/service-worker/resources | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/service-workers/service-worker/resources')
383 files changed, 10571 insertions, 0 deletions
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/404.py b/testing/web-platform/tests/service-workers/service-worker/resources/404.py new file mode 100644 index 0000000000..1ee4af169e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/404.py @@ -0,0 +1,5 @@ +# iframe does not fire onload event if the response's content-type is not +# text/plain or text/html so this script exists if you want to test a 404 load +# in an iframe. +def main(req, res): + return 404, [(b'Content-Type', b'text/plain')], b"Page not found" diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html new file mode 100644 index 0000000000..1e0c6209bf --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html @@ -0,0 +1,21 @@ +<!doctype html> +<html> +<body> +<script> +function nestedLoaded() { + parent.postMessage({ type: 'NESTED_LOADED' }, '*'); +} + +// dynamically add an about:blank iframe +var f = document.createElement('iframe'); +f.onload = nestedLoaded; +document.body.appendChild(f); + +// Helper routine to make it slightly easier for our parent to find +// the nested frame. +function nested() { + return f.contentWindow; +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html new file mode 100644 index 0000000000..16f7e7c60f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html @@ -0,0 +1,21 @@ +<!doctype html> +<html> +<body> +<script> +function nestedLoaded() { + parent.postMessage({ type: 'NESTED_LOADED' }, '*'); +} + +// Helper routine to make it slightly easier for our parent to find +// the nested frame. +function nested() { + return document.getElementById('nested').contentWindow; +} + +// NOTE: Make sure not to touch the iframe directly here. We want to +// test the case where the initial about:blank document is not +// directly accessed before load. +</script> +<iframe id="nested" onload="nestedLoaded()"></iframe> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py new file mode 100644 index 0000000000..a29ff9d413 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py @@ -0,0 +1,31 @@ +def main(request, response): + if b'nested' in request.GET: + return ( + [(b'Content-Type', b'text/html')], + b'failed: nested frame was not intercepted by the service worker' + ) + + return ([(b'Content-Type', b'text/html')], b""" +<!doctype html> +<html> +<body> +<script> +function nestedLoaded() { + parent.postMessage({ type: 'NESTED_LOADED' }, '*'); +} +</script> +<iframe src="?nested=true" id="nested" onload="nestedLoaded()"></iframe> +<script> +// Helper routine to make it slightly easier for our parent to find +// the nested frame. +function nested() { + return document.getElementById('nested').contentWindow; +} + +// NOTE: Make sure not to touch the iframe directly here. We want to +// test the case where the initial about:blank document is not +// directly accessed before load. +</script> +</body> +</html> +""") diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py new file mode 100644 index 0000000000..30fbbbb535 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py @@ -0,0 +1,49 @@ +def main(request, response): + if b'nested' in request.GET: + return ( + [(b'Content-Type', b'text/html')], + b'failed: nested frame was not intercepted by the service worker' + ) + + return ([(b'Content-Type', b'text/html')], b""" +<!doctype html> +<html> +<body> +<script> +function nestedLoaded() { + parent.postMessage({ type: 'NESTED_LOADED' }, '*'); +} +</script> +<iframe src="?nested=true&ping=true" id="nested" onload="nestedLoaded()"></iframe> +<script> +// Helper routine to make it slightly easier for our parent to find +// the nested frame. +function nested() { + return document.getElementById('nested').contentWindow; +} + +// This modifies the nested iframe immediately and does not wait for it to +// load. This effectively modifies the global for the initial about:blank +// document. Any modifications made here should be preserved after the +// frame loads because the global should be re-used. +let win = nested(); +if (win.location.href !== 'about:blank') { + parent.postMessage({ + type: 'NESTED_LOADED', + result: 'failed: nested iframe does not have an initial about:blank URL' + }, '*'); +} else { + win.navigator.serviceWorker.addEventListener('message', evt => { + if (evt.data.type === 'PING') { + evt.source.postMessage({ + type: 'PONG', + location: win.location.toString() + }); + } + }); + win.navigator.serviceWorker.startMessages(); +} +</script> +</body> +</html> +""") diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py new file mode 100644 index 0000000000..04c12a6037 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py @@ -0,0 +1,32 @@ +def main(request, response): + if b'nested' in request.GET: + return ( + [(b'Content-Type', b'text/html')], + b'failed: nested frame was not intercepted by the service worker' + ) + + return ([(b'Content-Type', b'text/html')], b""" +<!doctype html> +<html> +<body> +<script> +function nestedLoaded() { + parent.postMessage({ type: 'NESTED_LOADED' }, '*'); +} + +let popup = window.open('?nested=true'); +popup.onload = nestedLoaded; + +addEventListener('unload', evt => { + popup.close(); +}, { once: true }); + +// Helper routine to make it slightly easier for our parent to find +// the nested popup window. +function nested() { + return popup; +} +</script> +</body> +</html> +""") diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html new file mode 100644 index 0000000000..0122a00aa4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html @@ -0,0 +1,22 @@ +<!doctype html> +<html> +<body> +<script> +function nestedLoaded() { + parent.postMessage({ type: 'NESTED_LOADED' }, '*'); +} +</script> +<iframe id="nested" srcdoc="<div></div>" onload="nestedLoaded()"></iframe> +<script> +// Helper routine to make it slightly easier for our parent to find +// the nested frame. +function nested() { + return document.getElementById('nested').contentWindow; +} + +// NOTE: Make sure not to touch the iframe directly here. We want to +// test the case where the initial about:blank document is not +// directly accessed before load. +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html new file mode 100644 index 0000000000..89509159a4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html @@ -0,0 +1,22 @@ +<!doctype html> +<html> +<body> +<script> +function nestedLoaded() { + parent.postMessage({ type: 'NESTED_LOADED' }, '*'); +} +</script> +<iframe src="empty.html?nested=true" id="nested" onload="nestedLoaded()"></iframe> +<script> +// Helper routine to make it slightly easier for our parent to find +// the nested frame. +function nested() { + return document.getElementById('nested').contentWindow; +} + +// NOTE: Make sure not to touch the iframe directly here. We want to +// test the case where the initial about:blank document is not +// directly accessed before load. +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js new file mode 100644 index 0000000000..f43598e41c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js @@ -0,0 +1,95 @@ +// Helper routine to find a client that matches a particular URL. Note, we +// require that Client to be controlled to avoid false matches with other +// about:blank windows the browser might have. The initial about:blank should +// inherit the controller from its parent. +async function getClientByURL(url) { + let list = await clients.matchAll(); + return list.find(client => client.url === url); +} + +// Helper routine to perform a ping-pong with the given target client. We +// expect the Client to respond with its location URL. +async function pingPong(target) { + function waitForPong() { + return new Promise(resolve => { + self.addEventListener('message', function onMessage(evt) { + if (evt.data.type === 'PONG') { + resolve(evt.data.location); + } + }); + }); + } + + target.postMessage({ type: 'PING' }) + return await waitForPong(target); +} + +addEventListener('fetch', async evt => { + let url = new URL(evt.request.url); + if (!url.searchParams.get('nested')) { + return; + } + + evt.respondWith(async function() { + // Find the initial about:blank document. + const client = await getClientByURL('about:blank'); + if (!client) { + return new Response('failure: could not find about:blank client'); + } + + // If the nested frame is configured to support a ping-pong, then + // ping it now to verify its message listener exists. We also + // verify the Client's idea of its own location URL while we are doing + // this. + if (url.searchParams.get('ping')) { + const loc = await pingPong(client); + if (loc !== 'about:blank') { + return new Response(`failure: got location {$loc}, expected about:blank`); + } + } + + // Finally, allow the nested frame to complete loading. We place the + // Client ID we found for the initial about:blank in the body. + return new Response(client.id); + }()); +}); + +addEventListener('message', evt => { + if (evt.data.type !== 'GET_CLIENT_ID') { + return; + } + + evt.waitUntil(async function() { + let url = new URL(evt.data.url); + + // Find the given Client by its URL. + let client = await getClientByURL(evt.data.url); + if (!client) { + evt.source.postMessage({ + type: 'GET_CLIENT_ID', + result: `failure: could not find ${evt.data.url} client` + }); + return; + } + + // If the Client supports a ping-pong, then do it now to verify + // the message listener exists and its location matches the + // Client object. + if (url.searchParams.get('ping')) { + let loc = await pingPong(client); + if (loc !== evt.data.url) { + evt.source.postMessage({ + type: 'GET_CLIENT_ID', + result: `failure: got location ${loc}, expected ${evt.data.url}` + }); + return; + } + } + + // Finally, send the client ID back. + evt.source.postMessage({ + type: 'GET_CLIENT_ID', + result: client.id + }); + }()); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/basic-module-2.js b/testing/web-platform/tests/service-workers/service-worker/resources/basic-module-2.js new file mode 100644 index 0000000000..189b1c87fe --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/basic-module-2.js @@ -0,0 +1 @@ +export default 'hello again!'; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/basic-module.js b/testing/web-platform/tests/service-workers/service-worker/resources/basic-module.js new file mode 100644 index 0000000000..789a89bc63 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/basic-module.js @@ -0,0 +1 @@ +export default 'hello!'; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/blank.html b/testing/web-platform/tests/service-workers/service-worker/resources/blank.html new file mode 100644 index 0000000000..a3c3a4689a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/blank.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<title>Empty doc</title> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py b/testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py new file mode 100644 index 0000000000..1931c77b67 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py @@ -0,0 +1,20 @@ +import time + +def main(request, response): + headers = [(b'Content-Type', b'application/javascript'), + (b'Cache-Control', b'max-age=0'), + (b'Access-Control-Allow-Origin', b'*')] + + imported_content_type = b'' + if b'imported' in request.GET: + imported_content_type = request.GET[b'imported'] + + imported_content = b'default' + if imported_content_type == b'time': + imported_content = b'%f' % time.time() + + body = b''' + // %s + ''' % (imported_content) + + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker.py new file mode 100644 index 0000000000..10f3bceb4f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker.py @@ -0,0 +1,38 @@ +import time + +def main(request, response): + headers = [(b'Content-Type', b'application/javascript'), + (b'Cache-Control', b'max-age=0')] + + main_content_type = b'' + if b'main' in request.GET: + main_content_type = request.GET[b'main'] + + main_content = b'default' + if main_content_type == b'time': + main_content = b'%f' % time.time() + + imported_request_path = b'' + if b'path' in request.GET: + imported_request_path = request.GET[b'path'] + + imported_request_type = b'' + if b'imported' in request.GET: + imported_request_type = request.GET[b'imported'] + + imported_request = b'' + if imported_request_type == b'time': + imported_request = b'?imported=time' + + if b'type' in request.GET and request.GET[b'type'] == b'module': + body = b''' + // %s + import '%sbytecheck-worker-imported-script.py%s'; + ''' % (main_content, imported_request_path, imported_request) + else: + body = b''' + // %s + importScripts('%sbytecheck-worker-imported-script.py%s'); + ''' % (main_content, imported_request_path, imported_request) + + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html new file mode 100644 index 0000000000..12ae1a8725 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html @@ -0,0 +1,21 @@ +<!doctype html> +<script> +const baseLocation = window.location; +const workerScript = + `self.onmessage = async (e) => { + const url = new URL(e.data, '${baseLocation}').href; + const response = await fetch(url); + const text = await response.text(); + self.postMessage(text); + };`; +const blob = new Blob([workerScript], { type: 'text/javascript' }); +const blobUrl = URL.createObjectURL(blob); +const worker = new Worker(blobUrl); + +function fetch_in_worker(url) { + return new Promise((resolve) => { + worker.onmessage = (e) => resolve(e.data); + worker.postMessage(url); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html new file mode 100644 index 0000000000..2fa15db61d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html @@ -0,0 +1,16 @@ +<!doctype html> +<script> +// An iframe that starts a nested worker. Our parent frame (the test page) calls +// fetch_in_worker() to ask the nested worker to perform a fetch to see whether +// it's controlled by a service worker. +var worker = new Worker('./claim-nested-worker-fetch-parent-worker.js'); + +function fetch_in_worker(url) { + return new Promise((resolve) => { + worker.onmessage = (event) => { + resolve(event.data); + }; + worker.postMessage(url); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js new file mode 100644 index 0000000000..f5ff7c234b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js @@ -0,0 +1,12 @@ +try { + var worker = new Worker('./claim-worker-fetch-worker.js'); + + self.onmessage = (event) => { + worker.postMessage(event.data); + } + worker.onmessage = (event) => { + self.postMessage(event.data); + }; +} catch (e) { + self.postMessage("Fail: " + e.data); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html new file mode 100644 index 0000000000..ad865b848f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html @@ -0,0 +1,13 @@ +<!doctype html> +<script> +var worker = new SharedWorker('./claim-shared-worker-fetch-worker.js'); + +function fetch_in_shared_worker(url) { + return new Promise((resolve) => { + worker.port.onmessage = (event) => { + resolve(event.data); + }; + worker.port.postMessage(url); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js new file mode 100644 index 0000000000..ddc8bea7af --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js @@ -0,0 +1,8 @@ +self.onconnect = (event) => { + var port = event.ports[0]; + event.ports[0].onmessage = (evt) => { + fetch(evt.data) + .then(response => response.text()) + .then(text => port.postMessage(text)); + }; +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html new file mode 100644 index 0000000000..4150d7e685 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<body> +<script> +var host_info = get_host_info(); + +function send_result(result) { + window.parent.postMessage({message: result}, + host_info['HTTPS_ORIGIN']); +} + +function executeTask(params) { + // Execute task for each parameter + if (params.has('register')) { + var worker_url = decodeURIComponent(params.get('register')); + var scope = decodeURIComponent(params.get('scope')); + navigator.serviceWorker.register(worker_url, {scope: scope}) + .then(r => send_result('registered')); + } else if (params.has('redirected')) { + send_result('redirected'); + } else if (params.has('update')) { + var scope = decodeURIComponent(params.get('update')); + navigator.serviceWorker.getRegistration(scope) + .then(r => r.update()) + .then(() => send_result('updated')); + } else if (params.has('unregister')) { + var scope = decodeURIComponent(params.get('unregister')); + navigator.serviceWorker.getRegistration(scope) + .then(r => r.unregister()) + .then(succeeded => { + if (succeeded) { + send_result('unregistered'); + } else { + send_result('failure: unregister'); + } + }); + } else { + send_result('unknown parameter: ' + params.toString()); + } +} + +var params = new URLSearchParams(location.search.slice(1)); +executeTask(params); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html new file mode 100644 index 0000000000..92c5d15def --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html @@ -0,0 +1,13 @@ +<!doctype html> +<script> +var worker = new Worker('./claim-worker-fetch-worker.js'); + +function fetch_in_worker(url) { + return new Promise((resolve) => { + worker.onmessage = (event) => { + resolve(event.data); + }; + worker.postMessage(url); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js new file mode 100644 index 0000000000..7080181c85 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js @@ -0,0 +1,5 @@ +self.onmessage = (event) => { + fetch(event.data) + .then(response => response.text()) + .then(text => self.postMessage(text)); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker.js new file mode 100644 index 0000000000..1800407947 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker.js @@ -0,0 +1,19 @@ +self.addEventListener('message', function(event) { + self.clients.claim() + .then(function(result) { + if (result !== undefined) { + event.data.port.postMessage( + 'FAIL: claim() should be resolved with undefined'); + return; + } + event.data.port.postMessage('PASS'); + }) + .catch(function(error) { + event.data.port.postMessage('FAIL: exception: ' + error.name); + }); + }); + +self.addEventListener('fetch', function(event) { + if (!/404/.test(event.request.url)) + event.respondWith(new Response('Intercepted!')); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/classic-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/classic-worker.js new file mode 100644 index 0000000000..36a32b1a1f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/classic-worker.js @@ -0,0 +1 @@ +importScripts('./imported-classic-script.js'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/client-id-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/client-id-worker.js new file mode 100644 index 0000000000..ec71b3458b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/client-id-worker.js @@ -0,0 +1,27 @@ +self.onmessage = function(e) { + var port = e.data.port; + var message = []; + + var promise = Promise.resolve() + .then(function() { + // 1st matchAll() + return self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + message.push(client.id); + }); + }); + }) + .then(function() { + // 2nd matchAll() + return self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + message.push(client.id); + }); + }); + }) + .then(function() { + // Send an array containing ids of clients from 1st and 2nd matchAll() + port.postMessage(message); + }); + e.waitUntil(promise); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-frame.html new file mode 100644 index 0000000000..7e186f8ee7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-frame.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<script> + fetch("clientId") + .then(function(response) { + return response.text(); + }) + .then(function(text) { + parent.postMessage({id: text}, "*"); + }); +</script> +<body style="background-color: red;"></body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-worker.js new file mode 100644 index 0000000000..6101d5d8f9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-worker.js @@ -0,0 +1,92 @@ +importScripts("worker-testharness.js"); +importScripts("test-helpers.sub.js"); +importScripts("/common/get-host-info.sub.js") +importScripts("testharness-helpers.js") + +setup({ explicit_done: true }); + +self.onfetch = function(e) { + if (e.request.url.indexOf("client-navigate-frame.html") >= 0) { + return; + } + e.respondWith(new Response(e.clientId)); +}; + +function pass(test, url) { + return { result: test, + url: url }; +} + +function fail(test, reason) { + return { result: "FAILED " + test + " " + reason } +} + +self.onmessage = function(e) { + var port = e.data.port; + var test = e.data.test; + var clientId = e.data.clientId; + var clientUrl = ""; + if (test === "test_client_navigate_success") { + promise_test(function(t) { + this.add_cleanup(() => port.postMessage(pass(test, clientUrl))); + return self.clients.get(clientId) + .then(client => client.navigate("client-navigated-frame.html")) + .then(client => { + clientUrl = client.url; + assert_true(client instanceof WindowClient); + }) + .catch(unreached_rejection(t)); + }, "Return value should be instance of WindowClient"); + done(); + } else if (test === "test_client_navigate_cross_origin") { + promise_test(function(t) { + this.add_cleanup(() => port.postMessage(pass(test, clientUrl))); + var path = new URL('client-navigated-frame.html', self.location.href).pathname; + var url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + path; + return self.clients.get(clientId) + .then(client => client.navigate(url)) + .then(client => { + clientUrl = (client && client.url) || ""; + assert_equals(client, null, + 'cross-origin navigate resolves with null'); + }) + .catch(unreached_rejection(t)); + }, "Navigating to different origin should resolve with null"); + done(); + } else if (test === "test_client_navigate_about_blank") { + promise_test(function(t) { + this.add_cleanup(function() { port.postMessage(pass(test, "")); }); + return self.clients.get(clientId) + .then(client => promise_rejects_js(t, TypeError, client.navigate("about:blank"))) + .catch(unreached_rejection(t)); + }, "Navigating to about:blank should reject with TypeError"); + done(); + } else if (test === "test_client_navigate_mixed_content") { + promise_test(function(t) { + this.add_cleanup(function() { port.postMessage(pass(test, "")); }); + var path = new URL('client-navigated-frame.html', self.location.href).pathname; + // Insecure URL should fail since the frame is owned by a secure parent + // and navigating to http:// would create a mixed-content violation. + var url = get_host_info()['HTTP_REMOTE_ORIGIN'] + path; + return self.clients.get(clientId) + .then(client => promise_rejects_js(t, TypeError, client.navigate(url))) + .catch(unreached_rejection(t)); + }, "Navigating to mixed-content iframe should reject with TypeError"); + done(); + } else if (test === "test_client_navigate_redirect") { + var host_info = get_host_info(); + var url = new URL(host_info['HTTPS_REMOTE_ORIGIN']).toString() + + new URL("client-navigated-frame.html", location).pathname.substring(1); + promise_test(function(t) { + this.add_cleanup(() => port.postMessage(pass(test, clientUrl))); + return self.clients.get(clientId) + .then(client => client.navigate("redirect.py?Redirect=" + url)) + .then(client => { + clientUrl = (client && client.url) || "" + assert_equals(client, null); + }) + .catch(unreached_rejection(t)); + }, "Redirecting to another origin should resolve with null"); + done(); + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/client-navigated-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/client-navigated-frame.html new file mode 100644 index 0000000000..307f7f9ac6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/client-navigated-frame.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<body style="background-color: green;"></body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html new file mode 100644 index 0000000000..00f6acede8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<script> + +// Return a URL of a client when it's successful. +function createAndFetchFromBlobWorker() { + const fetchURL = new URL('get-worker-client-url.txt', window.location).href; + const workerScript = + `self.onmessage = async (e) => { + const response = await fetch(e.data.url); + const text = await response.text(); + self.postMessage({"result": text, "expected": self.location.href}); + };`; + const blob = new Blob([workerScript], { type: 'text/javascript' }); + const blobUrl = URL.createObjectURL(blob); + + const worker = new Worker(blobUrl); + return new Promise((resolve, reject) => { + worker.onmessage = e => resolve(e.data); + worker.onerror = e => reject(e.message); + worker.postMessage({"url": fetchURL}); + }); +} + +</script> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js new file mode 100644 index 0000000000..fd754f8250 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js @@ -0,0 +1,10 @@ +addEventListener('fetch', e => { + if (e.request.url.includes('get-worker-client-url')) { + e.respondWith((async () => { + const clients = await self.clients.matchAll({type: 'worker'}); + if (clients.length != 1) + return new Response('one worker client should exist'); + return new Response(clients[0].url); + })()); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-frame-freeze.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-frame-freeze.html new file mode 100644 index 0000000000..7468a660e9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-frame-freeze.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> + document.addEventListener('freeze', () => { + opener.postMessage('frozen', "*"); + }); + + window.onmessage = (e) => { + if (e.data == 'freeze') { + test_driver.freeze(); + } + }; + opener.postMessage('loaded', '*'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js new file mode 100644 index 0000000000..0a1461b40e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js @@ -0,0 +1,11 @@ +onmessage = function(e) { + if (e.data.cmd == 'GetClientId') { + fetch('clientId') + .then(function(response) { + return response.text(); + }) + .then(function(text) { + e.data.port.postMessage({clientId: text}); + }); + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html new file mode 100644 index 0000000000..4324e6d405 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<script> +fetch('clientId') + .then(function(response) { + return response.text(); + }) + .then(function(text) { + parent.postMessage({clientId: text}, '*'); + }); + +onmessage = function(e) { + if (e.data == 'StartWorker') { + var w = new Worker('clients-get-client-types-frame-worker.js'); + w.postMessage({cmd:'GetClientId', port:e.ports[0]}, [e.ports[0]]); + } +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js new file mode 100644 index 0000000000..fadef97037 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js @@ -0,0 +1,10 @@ +onconnect = function(e) { + var port = e.ports[0]; + fetch('clientId') + .then(function(response) { + return response.text(); + }) + .then(function(text) { + port.postMessage({clientId: text}); + }); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js new file mode 100644 index 0000000000..0a1461b40e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js @@ -0,0 +1,11 @@ +onmessage = function(e) { + if (e.data.cmd == 'GetClientId') { + fetch('clientId') + .then(function(response) { + return response.text(); + }) + .then(function(text) { + e.data.port.postMessage({clientId: text}); + }); + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html new file mode 100644 index 0000000000..e16bb1116d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js"></script> +<script> +var host_info = get_host_info(); +var scope = 'blank.html?clients-get'; +var script = 'clients-get-worker.js'; + +var registration; +var worker; +var wait_for_worker_promise = navigator.serviceWorker.getRegistration(scope) + .then(function(reg) { + if (reg) + return reg.unregister(); + }) + .then(function() { + return navigator.serviceWorker.register(script, {scope: scope}); + }) + .then(function(reg) { + registration = reg; + worker = reg.installing; + return new Promise(function(resolve) { + worker.addEventListener('statechange', function() { + if (worker.state == 'activated') + resolve(); + }); + }); + }); + +window.addEventListener('message', function(e) { + var cross_origin_client_ids = []; + cross_origin_client_ids.push(e.data.clientId); + wait_for_worker_promise + .then(function() { + return with_iframe(scope); + }) + .then(function(iframe) { + add_completion_callback(function() { iframe.remove(); }); + navigator.serviceWorker.onmessage = function(e) { + registration.unregister(); + window.parent.postMessage( + { type: 'clientId', value: e.data }, host_info['HTTPS_ORIGIN'] + ); + }; + registration.active.postMessage({clientIds: cross_origin_client_ids}); + }); +}); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-frame.html new file mode 100644 index 0000000000..27143d4b99 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-frame.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + + fetch("clientId") + .then(function(response) { + return response.text(); + }) + .then(function(text) { + parent.postMessage({clientId: text}, "*"); + }); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-other-origin.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-other-origin.html new file mode 100644 index 0000000000..6342fe04f4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-other-origin.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js"></script> +<script> +var host_info = get_host_info(); +var SCOPE = 'blank.html?clients-get'; +var SCRIPT = 'clients-get-worker.js'; + +var registration; +var worker; +var wait_for_worker_promise = navigator.serviceWorker.getRegistration(SCOPE) + .then(function(reg) { + if (reg) + return reg.unregister(); + }) + .then(function() { + return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE}); + }) + .then(function(reg) { + registration = reg; + worker = reg.installing; + return new Promise(function(resolve) { + worker.addEventListener('statechange', function() { + if (worker.state == 'activated') + resolve(); + }); + }); + }); + +function send_result(result) { + window.parent.postMessage( + {result: result}, + host_info['HTTPS_ORIGIN']); +} + +window.addEventListener("message", on_message, false); + +function on_message(e) { + if (e.origin != host_info['HTTPS_ORIGIN']) { + console.error('invalid origin: ' + e.origin); + return; + } + if (e.data.message == 'get_client_id') { + var otherOriginClientId = e.data.clientId; + wait_for_worker_promise + .then(function() { + return with_iframe(SCOPE); + }) + .then(function(iframe) { + var channel = new MessageChannel(); + channel.port1.onmessage = function(e) { + navigator.serviceWorker.getRegistration(SCOPE) + .then(function(reg) { + reg.unregister(); + send_result(e.data); + }); + }; + iframe.contentWindow.navigator.serviceWorker.controller.postMessage( + {port:channel.port2, clientId: otherOriginClientId, + message: 'get_other_client_id'}, [channel.port2]); + }) + } +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js new file mode 100644 index 0000000000..5a46ff9cf4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js @@ -0,0 +1,60 @@ +let savedPort = null; +let savedResultingClientId = null; + +async function getTestingPage() { + const clientList = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); + for (let c of clientList) { + if (c.url.endsWith('clients-get.https.html')) { + c.focus(); + return c; + } + } + return null; +} + +async function destroyResultingClient(testingPage) { + const destroyedPromise = new Promise(resolve => { + self.addEventListener('message', e => { + if (e.data.msg == 'resultingClientDestroyed') { + resolve(); + } + }, {once: true}); + }); + testingPage.postMessage({ msg: 'destroyResultingClient' }); + return destroyedPromise; +} + +self.addEventListener('fetch', async (e) => { + let { resultingClientId } = e; + savedResultingClientId = resultingClientId; + + if (e.request.url.endsWith('simple.html?fail')) { + e.waitUntil((async () => { + const testingPage = await getTestingPage(); + await destroyResultingClient(testingPage); + testingPage.postMessage({ msg: 'resultingClientDestroyedAck', + resultingDestroyedClientId: savedResultingClientId }); + })()); + return; + } + + e.respondWith(fetch(e.request)); +}); + +self.addEventListener('message', (e) => { + let { msg, resultingClientId } = e.data; + e.waitUntil((async () => { + if (msg == 'getIsResultingClientUndefined') { + const client = await self.clients.get(resultingClientId); + let isUndefined = typeof client == 'undefined'; + e.source.postMessage({ msg: 'getIsResultingClientUndefined', + isResultingClientUndefined: isUndefined }); + return; + } + if (msg == 'getResultingClientId') { + e.source.postMessage({ msg: 'getResultingClientId', + resultingClientId: savedResultingClientId }); + return; + } + })()); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-worker.js new file mode 100644 index 0000000000..8effa56c98 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-worker.js @@ -0,0 +1,41 @@ +// This worker is designed to expose information about clients that is only available from Service Worker contexts. +// +// In the case of the `onfetch` handler, it provides the `clientId` property of +// the `event` object. In the case of the `onmessage` handler, it provides the +// Client instance attributes of the requested clients. +self.onfetch = function(e) { + if (/\/clientId$/.test(e.request.url)) { + e.respondWith(new Response(e.clientId)); + return; + } +}; + +self.onmessage = function(e) { + var client_ids = e.data.clientIds; + var message = []; + + e.waitUntil(Promise.all( + client_ids.map(function(client_id) { + return self.clients.get(client_id); + })) + .then(function(clients) { + // No matching client for a given id or a matched client is off-origin + // from the service worker. + if (clients.length == 1 && clients[0] == undefined) { + e.source.postMessage(clients[0]); + } else { + clients.forEach(function(client) { + if (client instanceof Client) { + message.push([client.visibilityState, + client.focused, + client.url, + client.type, + client.frameType]); + } else { + message.push(client); + } + }); + e.source.postMessage(message); + } + })); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html new file mode 100644 index 0000000000..ee89a0d8b3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<script> +const workerScript = ` + self.onmessage = (e) => { + self.postMessage("Worker is ready."); + }; +`; +const blob = new Blob([workerScript], { type: 'text/javascript' }); +const blobUrl = URL.createObjectURL(blob); +const worker = new Worker(blobUrl); + +function waitForWorker() { + return new Promise(resolve => { + worker.onmessage = resolve; + worker.postMessage("Ping to worker."); + }); +} +</script> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js new file mode 100644 index 0000000000..5a3f04d33a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js @@ -0,0 +1,3 @@ +onmessage = function(e) { + postMessage(e.data); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html new file mode 100644 index 0000000000..7607b035de --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<title>Empty doc</title> +<!-- + Change the page URL using the History API to ensure that ServiceWorkerClient + uses the creation URL. +--> +<body onload="history.pushState({}, 'title', 'url-modified-via-pushstate.html')"> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js new file mode 100644 index 0000000000..1ae72fb894 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js @@ -0,0 +1,4 @@ +onconnect = function(e) { + var port = e.ports[0]; + port.postMessage('started'); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js new file mode 100644 index 0000000000..f1559aca39 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js @@ -0,0 +1,11 @@ +importScripts('test-helpers.sub.js'); + +var page_url = normalizeURL('../clients-matchall-on-evaluation.https.html'); + +self.clients.matchAll({includeUncontrolled: true}) + .then(function(clients) { + clients.forEach(function(client) { + if (client.url == page_url) + client.postMessage('matched'); + }); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-worker.js new file mode 100644 index 0000000000..13e111a2f9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-worker.js @@ -0,0 +1,40 @@ +self.onmessage = function(e) { + var port = e.data.port; + var options = e.data.options; + + e.waitUntil(self.clients.matchAll(options) + .then(function(clients) { + var message = []; + clients.forEach(function(client) { + var frame_type = client.frameType; + if (client.url.indexOf('clients-matchall-include-uncontrolled.https.html') > -1 && + client.frameType == 'auxiliary') { + // The test tab might be opened using window.open() by the test framework. + // In that case, just pretend it's top-level! + frame_type = 'top-level'; + } + if (e.data.includeLifecycleState) { + message.push({visibilityState: client.visibilityState, + focused: client.focused, + url: client.url, + lifecycleState: client.lifecycleState, + type: client.type, + frameType: frame_type}); + } else { + message.push([client.visibilityState, + client.focused, + client.url, + client.type, + frame_type]); + } + }); + // Sort by url + if (!e.data.disableSort) { + message.sort(function(a, b) { return a[2] > b[2] ? 1 : -1; }); + } + port.postMessage(message); + }) + .catch(e => { + port.postMessage('clients.matchAll() rejected: ' + e); + })); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt b/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt new file mode 100644 index 0000000000..1cd89bb14d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt @@ -0,0 +1 @@ +plaintext diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt.headers b/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt.headers new file mode 100644 index 0000000000..f7985fd9bd --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt.headers @@ -0,0 +1,3 @@ +Content-Type: text/plain +Access-Control-Allow-Origin: * + diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/cors-denied.txt b/testing/web-platform/tests/service-workers/service-worker/resources/cors-denied.txt new file mode 100644 index 0000000000..ff333bd97d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/cors-denied.txt @@ -0,0 +1,2 @@ +this file is served without Access-Control-Allow-Origin headers so it should not +be readable from cross-origin. diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/create-blob-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/create-blob-url-worker.js new file mode 100644 index 0000000000..57e4882c24 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/create-blob-url-worker.js @@ -0,0 +1,22 @@ +const childWorkerScript = ` + self.onmessage = async (e) => { + const response = await fetch(e.data); + const text = await response.text(); + self.postMessage(text); + }; +`; +const blob = new Blob([childWorkerScript], { type: 'text/javascript' }); +const blobUrl = URL.createObjectURL(blob); +const childWorker = new Worker(blobUrl); + +// When a message comes from the parent frame, sends a resource url to the child +// worker. +self.onmessage = (e) => { + childWorker.postMessage(e.data); +}; + +// When a message comes from the child worker, sends a content of fetch() to the +// parent frame. +childWorker.onmessage = (e) => { + self.postMessage(e.data); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html new file mode 100644 index 0000000000..b51c451750 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html @@ -0,0 +1,19 @@ +<!doctype html> +<script> +const workerUrl = '../out-of-scope/sample-synthesized-worker.js?dedicated'; +const worker = new Worker(workerUrl); +const workerPromise = new Promise(resolve => { + worker.onmessage = e => { + // `e.data` is 'worker loading intercepted by service worker' when a worker + // is intercepted by a service worker. + resolve(e.data); + } + worker.onerror = _ => { + resolve('worker loading was not intercepted by service worker'); + } +}); + +function getWorkerPromise() { + return workerPromise; +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/echo-content.py b/testing/web-platform/tests/service-workers/service-worker/resources/echo-content.py new file mode 100644 index 0000000000..70ae4b6025 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/echo-content.py @@ -0,0 +1,16 @@ +# This is a copy of fetch/api/resources/echo-content.py since it's more +# convenient in this directory due to service worker's path restriction. +from wptserve.utils import isomorphic_encode + +def main(request, response): + + headers = [(b"X-Request-Method", isomorphic_encode(request.method)), + (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")), + (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")), + + # Avoid any kind of content sniffing on the response. + (b"Content-Type", b"text/plain")] + + content = request.body + + return headers, content diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/echo-cookie-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/echo-cookie-worker.py new file mode 100644 index 0000000000..561f64a35a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/echo-cookie-worker.py @@ -0,0 +1,24 @@ +def main(request, response): + headers = [(b"Content-Type", b"text/javascript")] + + values = [] + for key in request.cookies: + for cookie in request.cookies.get_list(key): + values.append(b'"%s": "%s"' % (key, cookie.value)) + + # Update the counter to change the script body for every request to trigger + # update of the service worker. + key = request.GET[b'key'] + counter = request.server.stash.take(key) + if counter is None: + counter = 0 + counter += 1 + request.server.stash.put(key, counter) + + body = b""" +// %d +self.addEventListener('message', e => { + e.source.postMessage({%s}) +});""" % (counter, b','.join(values)) + + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js new file mode 100644 index 0000000000..bbbd35fb4f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js @@ -0,0 +1,3 @@ +addEventListener('message', evt => { + evt.source.postMessage(evt.data); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js new file mode 100644 index 0000000000..ffcdb75128 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js @@ -0,0 +1,14 @@ +// This worker intercepts a request for EMBED/OBJECT and responds with a +// response that indicates that interception occurred. The tests expect +// that interception does not occur. +self.addEventListener('fetch', e => { + if (e.request.url.indexOf('embedded-content-from-server.html') != -1) { + e.respondWith(fetch('embedded-content-from-service-worker.html')); + return; + } + + if (e.request.url.indexOf('green.png') != -1) { + e.respondWith(Promise.reject('network error to show interception occurred')); + return; + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html new file mode 100644 index 0000000000..7b8b257203 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>iframe for embed-and-object-are-not-intercepted test</title> +<body> +<embed type="image/png" src="/images/green.png"></embed> +<script> +// Our parent (the root frame of the test) will examine this to get the result. +var test_promise = new Promise(resolve => { + if (!navigator.serviceWorker.controller) + resolve('FAIL: this iframe is not controlled'); + + const elem = document.querySelector('embed'); + elem.addEventListener('load', e => { + resolve('request was not intercepted'); + }); + elem.addEventListener('error', e => { + resolve('FAIL: request was intercepted'); + }); + }); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html new file mode 100644 index 0000000000..39149915cc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>iframe for embed-and-object-are-not-intercepted test</title> +<body> +<script> +// The EMBED element will call this with the result about whether the EMBED +// request was intercepted by the service worker. +var report_result; + +// Our parent (the root frame of the test) will examine this to get the result. +var test_promise = new Promise(resolve => { + report_result = resolve; + }); +</script> + +<embed src="embedded-content-from-server.html"></embed> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html new file mode 100644 index 0000000000..5e86f67735 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>iframe for embed-and-object-are-not-intercepted test</title> +<body> +<script> +// The EMBED element will call this with the result about whether the EMBED +// request was intercepted by the service worker. +var report_result; + +// Our parent (the root frame of the test) will examine this to get the result. +var test_promise = new Promise(resolve => { + report_result = resolve; + }); + +let el = document.createElement('embed'); +el.src = "/common/blank.html"; +el.addEventListener('load', _ => { + window[0].location = "/service-workers/service-worker/resources/embedded-content-from-server.html"; +}, { once: true }); +document.body.appendChild(el); +</script> + +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-server.html b/testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-server.html new file mode 100644 index 0000000000..ff50a9c752 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-server.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>embed for embed-and-object-are-not-intercepted test</title> +<script> +window.parent.report_result('request for embedded content was not intercepted'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html new file mode 100644 index 0000000000..2e2b923608 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>embed for embed-and-object-are-not-intercepted test</title> +<script> +window.parent.report_result('request for embedded content was intercepted by service worker'); +</script> + diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/empty-but-slow-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/empty-but-slow-worker.js new file mode 100644 index 0000000000..92abac7a38 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/empty-but-slow-worker.js @@ -0,0 +1,8 @@ +addEventListener('fetch', evt => { + if (evt.request.url.endsWith('slow')) { + // Performance.now() might be a bit better here, but Date.now() has + // better compat in workers right now. + let start = Date.now(); + while(Date.now() - start < 2000); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/empty-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/empty-worker.js new file mode 100644 index 0000000000..49ceb2648a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/empty-worker.js @@ -0,0 +1 @@ +// Do nothing. diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/empty.h2.js b/testing/web-platform/tests/service-workers/service-worker/resources/empty.h2.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/empty.h2.js diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/empty.html b/testing/web-platform/tests/service-workers/service-worker/resources/empty.html new file mode 100644 index 0000000000..6feb11946b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/empty.html @@ -0,0 +1,6 @@ +<!doctype html> +<html> +<body> +hello world +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/empty.js b/testing/web-platform/tests/service-workers/service-worker/resources/empty.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/empty.js diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/enable-client-message-queue.html b/testing/web-platform/tests/service-workers/service-worker/resources/enable-client-message-queue.html new file mode 100644 index 0000000000..512bd14bc6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/enable-client-message-queue.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<script> + // The state variable is used by handle_message to record the time + // at which a message was handled. It's updated by the scripts + // loaded by the <script> tags at the bottom of the file as well as + // by the event listener added here. + var state = 'init'; + addEventListener('DOMContentLoaded', () => state = 'loaded'); + + // We expect to get three ping messages from the service worker. + const expected = ['init', 'install', 'start']; + let promises = {}; + let resolvers = {}; + expected.forEach(name => { + promises[name] = new Promise(resolve => resolvers[name] = resolve); + }); + + // Once all messages have been dispatched, the state in which each + // of them was dispatched is recorded in the draft. At that point + // the draft becomes the final report. + var draft = {}; + var report = Promise.all(Object.values(promises)).then(() => window.draft); + + // This message handler is installed by the 'install' script. + function handle_message(event) { + const data = event.data.data; + draft[data] = state; + resolvers[data](); + } +</script> + +<!-- + The controlling service worker will delay the response to these + fetch requests until the test instructs it how to reply. Note that + the event loop keeps spinning while the parser is blocked. +--> +<script src="empty.js?key=install"></script> +<script src="empty.js?key=start"></script> +<script src="empty.js?key=finish"></script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/end-to-end-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/end-to-end-worker.js new file mode 100644 index 0000000000..d45a50556a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/end-to-end-worker.js @@ -0,0 +1,7 @@ +onmessage = function(e) { + var message = e.data; + if (typeof message === 'object' && 'port' in message) { + var response = 'Ack for: ' + message.from; + message.port.postMessage(response); + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/events-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/events-worker.js new file mode 100644 index 0000000000..80a2188677 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/events-worker.js @@ -0,0 +1,12 @@ +var eventsSeen = []; + +function handler(event) { eventsSeen.push(event.type); } + +['activate', 'install'].forEach(function(type) { + self.addEventListener(type, handler); + }); + +onmessage = function(e) { + var message = e.data; + message.port.postMessage({events: eventsSeen}); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js b/testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js new file mode 100644 index 0000000000..8a975b0d2e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js @@ -0,0 +1,210 @@ +// This worker calls waitUntil() and respondWith() asynchronously and +// reports back to the test whether they threw. +// +// These test cases are confusing. Bear in mind that the event is active +// (calling waitUntil() is allowed) if: +// * The pending promise count is not 0, or +// * The event dispatch flag is set. + +// Controlled by 'init'/'done' messages. +var resolveLockPromise; +var port; + +self.addEventListener('message', function(event) { + var waitPromise; + var resolveTestPromise; + + switch (event.data.step) { + case 'init': + event.waitUntil(new Promise((res) => { resolveLockPromise = res; })); + port = event.data.port; + break; + case 'done': + resolveLockPromise(); + break; + + // Throws because waitUntil() is called in a task after event dispatch + // finishes. + case 'no-current-extension-different-task': + async_task_waituntil(event).then(reportResultExpecting('InvalidStateError')); + break; + + // OK because waitUntil() is called in a microtask that runs after the + // event handler runs, while the event dispatch flag is still set. + case 'no-current-extension-different-microtask': + async_microtask_waituntil(event).then(reportResultExpecting('OK')); + break; + + // OK because the second waitUntil() is called while the first waitUntil() + // promise is still pending. + case 'current-extension-different-task': + event.waitUntil(new Promise((res) => { resolveTestPromise = res; })); + async_task_waituntil(event).then(reportResultExpecting('OK')).then(resolveTestPromise); + break; + + // OK because all promises involved resolve "immediately", so the second + // waitUntil() is called during the microtask checkpoint at the end of + // event dispatching, when the event dispatch flag is still set. + case 'during-event-dispatch-current-extension-expired-same-microtask-turn': + waitPromise = Promise.resolve(); + event.waitUntil(waitPromise); + waitPromise.then(() => { return sync_waituntil(event); }) + .then(reportResultExpecting('OK')) + break; + + // OK for the same reason as above. + case 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra': + waitPromise = Promise.resolve(); + event.waitUntil(waitPromise); + waitPromise.then(() => { return async_microtask_waituntil(event); }) + .then(reportResultExpecting('OK')) + break; + + + // OK because the pending promise count is decremented in a microtask + // queued upon fulfillment of the first waitUntil() promise, so the second + // waitUntil() is called while the pending promise count is still + // positive. + case 'after-event-dispatch-current-extension-expired-same-microtask-turn': + waitPromise = makeNewTaskPromise(); + event.waitUntil(waitPromise); + waitPromise.then(() => { return sync_waituntil(event); }) + .then(reportResultExpecting('OK')) + break; + + // Throws because the second waitUntil() is called after the pending + // promise count was decremented to 0. + case 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra': + waitPromise = makeNewTaskPromise(); + event.waitUntil(waitPromise); + waitPromise.then(() => { return async_microtask_waituntil(event); }) + .then(reportResultExpecting('InvalidStateError')) + break; + + // Throws because the second waitUntil() is called in a new task, after + // first waitUntil() promise settled and the event dispatch flag is unset. + case 'current-extension-expired-different-task': + event.waitUntil(Promise.resolve()); + async_task_waituntil(event).then(reportResultExpecting('InvalidStateError')); + break; + + case 'script-extendable-event': + self.dispatchEvent(new ExtendableEvent('nontrustedevent')); + break; + } + + event.source.postMessage('ACK'); + }); + +self.addEventListener('fetch', function(event) { + const path = new URL(event.request.url).pathname; + const step = path.substring(path.lastIndexOf('/') + 1); + let response; + switch (step) { + // OK because waitUntil() is called while the respondWith() promise is still + // unsettled, so the pending promise count is positive. + case 'pending-respondwith-async-waituntil': + var resolveFetch; + response = new Promise((res) => { resolveFetch = res; }); + event.respondWith(response); + async_task_waituntil(event) + .then(reportResultExpecting('OK')) + .then(() => { resolveFetch(new Response('OK')); }); + break; + + // OK because all promises involved resolve "immediately", so waitUntil() is + // called during the microtask checkpoint at the end of event dispatching, + // when the event dispatch flag is still set. + case 'during-event-dispatch-respondwith-microtask-sync-waituntil': + response = Promise.resolve(new Response('RESP')); + event.respondWith(response); + response.then(() => { return sync_waituntil(event); }) + .then(reportResultExpecting('OK')); + break; + + // OK because all promises involved resolve "immediately", so waitUntil() is + // called during the microtask checkpoint at the end of event dispatching, + // when the event dispatch flag is still set. + case 'during-event-dispatch-respondwith-microtask-async-waituntil': + response = Promise.resolve(new Response('RESP')); + event.respondWith(response); + response.then(() => { return async_microtask_waituntil(event); }) + .then(reportResultExpecting('OK')); + break; + + // OK because the pending promise count is decremented in a microtask queued + // upon fulfillment of the respondWith() promise, so waitUntil() is called + // while the pending promise count is still positive. + case 'after-event-dispatch-respondwith-microtask-sync-waituntil': + response = makeNewTaskPromise().then(() => {return new Response('RESP');}); + event.respondWith(response); + response.then(() => { return sync_waituntil(event); }) + .then(reportResultExpecting('OK')); + break; + + + // Throws because waitUntil() is called after the pending promise count was + // decremented to 0. + case 'after-event-dispatch-respondwith-microtask-async-waituntil': + response = makeNewTaskPromise().then(() => {return new Response('RESP');}); + event.respondWith(response); + response.then(() => { return async_microtask_waituntil(event); }) + .then(reportResultExpecting('InvalidStateError')) + break; + } +}); + +self.addEventListener('nontrustedevent', function(event) { + sync_waituntil(event).then(reportResultExpecting('InvalidStateError')); + }); + +function reportResultExpecting(expectedResult) { + return function (result) { + port.postMessage({result : result, expected: expectedResult}); + return result; + }; +} + +function sync_waituntil(event) { + return new Promise((res, rej) => { + try { + event.waitUntil(Promise.resolve()); + res('OK'); + } catch (error) { + res(error.name); + } + }); +} + +function async_microtask_waituntil(event) { + return new Promise((res, rej) => { + Promise.resolve().then(() => { + try { + event.waitUntil(Promise.resolve()); + res('OK'); + } catch (error) { + res(error.name); + } + }); + }); +} + +function async_task_waituntil(event) { + return new Promise((res, rej) => { + setTimeout(() => { + try { + event.waitUntil(Promise.resolve()); + res('OK'); + } catch (error) { + res(error.name); + } + }, 0); + }); +} + +// Returns a promise that settles in a separate task. +function makeNewTaskPromise() { + return new Promise(resolve => { + setTimeout(resolve, 0); + }); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-waituntil.js b/testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-waituntil.js new file mode 100644 index 0000000000..20a9eb023f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-waituntil.js @@ -0,0 +1,87 @@ +var pendingPorts = []; +var portResolves = []; + +onmessage = function(e) { + var message = e.data; + if ('port' in message) { + var resolve = self.portResolves.shift(); + if (resolve) + resolve(message.port); + else + self.pendingPorts.push(message.port); + } +}; + +function fulfillPromise() { + return new Promise(function(resolve) { + // Make sure the oninstall/onactivate callback returns first. + Promise.resolve().then(function() { + var port = self.pendingPorts.shift(); + if (port) + resolve(port); + else + self.portResolves.push(resolve); + }); + }).then(function(port) { + port.postMessage('SYNC'); + return new Promise(function(resolve) { + port.onmessage = function(e) { + if (e.data == 'ACK') + resolve(); + }; + }); + }); +} + +function rejectPromise() { + return new Promise(function(resolve, reject) { + // Make sure the oninstall/onactivate callback returns first. + Promise.resolve().then(reject); + }); +} + +function stripScopeName(url) { + return url.split('/').slice(-1)[0]; +} + +oninstall = function(e) { + switch (stripScopeName(self.location.href)) { + case 'install-fulfilled': + e.waitUntil(fulfillPromise()); + break; + case 'install-rejected': + e.waitUntil(rejectPromise()); + break; + case 'install-multiple-fulfilled': + e.waitUntil(fulfillPromise()); + e.waitUntil(fulfillPromise()); + break; + case 'install-reject-precedence': + // Three "extend lifetime promises" are needed to verify that the user + // agent waits for all promises to settle even in the event of rejection. + // The first promise is fulfilled on demand by the client, the second is + // immediately scheduled for rejection, and the third is fulfilled on + // demand by the client (but only after the first promise has been + // fulfilled). + // + // User agents which simply expose `Promise.all` semantics in this case + // (by entering the "redundant state" following the rejection of the + // second promise but prior to the fulfillment of the third) can be + // identified from the client context. + e.waitUntil(fulfillPromise()); + e.waitUntil(rejectPromise()); + e.waitUntil(fulfillPromise()); + break; + } +}; + +onactivate = function(e) { + switch (stripScopeName(self.location.href)) { + case 'activate-fulfilled': + e.waitUntil(fulfillPromise()); + break; + case 'activate-rejected': + e.waitUntil(rejectPromise()); + break; + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js new file mode 100644 index 0000000000..517f289fbc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js @@ -0,0 +1,5 @@ +importScripts('worker-testharness.js'); + +this.addEventListener('fetch', function(event) { + event.respondWith(new Response('ERROR')); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control-login.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control-login.html new file mode 100644 index 0000000000..ee296807ed --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control-login.html @@ -0,0 +1,16 @@ +<script> +// Set authentication info +window.addEventListener("message", function(evt) { + var port = evt.ports[0]; + document.cookie = 'cookie=' + evt.data.cookie; + var xhr = new XMLHttpRequest(); + xhr.addEventListener('load', function() { + port.postMessage({msg: 'LOGIN FINISHED'}); + }, false); + xhr.open('GET', + './fetch-access-control.py?Auth', + true, + evt.data.username, evt.data.password); + xhr.send(); + }, false); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control.py b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control.py new file mode 100644 index 0000000000..446af87b24 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control.py @@ -0,0 +1,109 @@ +import json +import os +from base64 import decodebytes + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def main(request, response): + headers = [] + headers.append((b'X-ServiceWorker-ServerHeader', b'SetInTheServer')) + + if b"ACAOrigin" in request.GET: + for item in request.GET[b"ACAOrigin"].split(b","): + headers.append((b"Access-Control-Allow-Origin", item)) + + for suffix in [b"Headers", b"Methods", b"Credentials"]: + query = b"ACA%s" % suffix + header = b"Access-Control-Allow-%s" % suffix + if query in request.GET: + headers.append((header, request.GET[query])) + + if b"ACEHeaders" in request.GET: + headers.append((b"Access-Control-Expose-Headers", request.GET[b"ACEHeaders"])) + + if (b"Auth" in request.GET and not request.auth.username) or b"AuthFail" in request.GET: + status = 401 + headers.append((b'WWW-Authenticate', b'Basic realm="Restricted"')) + body = b'Authentication canceled' + return status, headers, body + + if b"PNGIMAGE" in request.GET: + headers.append((b"Content-Type", b"image/png")) + body = decodebytes(b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1B" + b"AACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAhSURBVDhPY3wro/KfgQLABKXJBqMG" + b"jBoAAqMGDLwBDAwAEsoCTFWunmQAAAAASUVORK5CYII=") + return headers, body + + if b"VIDEO" in request.GET: + headers.append((b"Content-Type", b"video/ogg")) + body = open(os.path.join(request.doc_root, u"media", u"movie_5.ogv"), "rb").read() + length = len(body) + # If "PartialContent" is specified, the requestor wants to test range + # requests. For the initial request, respond with "206 Partial Content" + # and don't send the entire content. Then expect subsequent requests to + # have a "Range" header with a byte range. Respond with that range. + if b"PartialContent" in request.GET: + if length < 1: + return 500, headers, b"file is too small for range requests" + start = 0 + end = length - 1 + if b"Range" in request.headers: + range_header = request.headers[b"Range"] + prefix = b"bytes=" + split_header = range_header[len(prefix):].split(b"-") + # The first request might be "bytes=0-". We want to force a range + # request, so just return the first byte. + if split_header[0] == b"0" and split_header[1] == b"": + end = start + # Otherwise, it is a range request. Respect the values sent. + if split_header[0] != b"": + start = int(split_header[0]) + if split_header[1] != b"": + end = int(split_header[1]) + else: + # The request doesn't have a range. Force a range request by + # returning the first byte. + end = start + + headers.append((b"Accept-Ranges", b"bytes")) + headers.append((b"Content-Length", isomorphic_encode(str(end -start + 1)))) + headers.append((b"Content-Range", b"bytes %d-%d/%d" % (start, end, length))) + chunk = body[start:(end + 1)] + return 206, headers, chunk + return headers, body + + username = request.auth.username if request.auth.username else b"undefined" + password = request.auth.password if request.auth.username else b"undefined" + cookie = request.cookies[b'cookie'].value if b'cookie' in request.cookies else b"undefined" + + files = [] + for key, values in request.POST.items(): + assert len(values) == 1 + value = values[0] + if not hasattr(value, u"file"): + continue + data = value.file.read() + files.append({u"key": isomorphic_decode(key), + u"name": value.file.name, + u"type": value.type, + u"error": 0, #TODO, + u"size": len(data), + u"content": data}) + + get_data = {isomorphic_decode(key):isomorphic_decode(request.GET[key]) for key, value in request.GET.items()} + post_data = {isomorphic_decode(key):isomorphic_decode(request.POST[key]) for key, value in request.POST.items() + if not hasattr(request.POST[key], u"file")} + headers_data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()} + + data = {u"jsonpResult": u"success", + u"method": request.method, + u"headers": headers_data, + u"body": isomorphic_decode(request.body), + u"files": files, + u"GET": get_data, + u"POST": post_data, + u"username": isomorphic_decode(username), + u"password": isomorphic_decode(password), + u"cookie": isomorphic_decode(cookie)} + + return headers, u"report( %s )" % json.dumps(data) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js new file mode 100644 index 0000000000..17723dcdda --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js @@ -0,0 +1,7 @@ +self.addEventListener('fetch', (event) => { + url = new URL(event.request.url); + if (url.search == '?PNGIMAGE') { + localUrl = new URL(url.pathname + url.search, self.location); + event.respondWith(fetch(localUrl)); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html new file mode 100644 index 0000000000..75d766c193 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html @@ -0,0 +1,70 @@ +<html> +<title>iframe for fetch canvas tainting test</title> +<script> +const NOT_TAINTED = 'NOT_TAINTED'; +const TAINTED = 'TAINTED'; +const LOAD_ERROR = 'LOAD_ERROR'; + +// Creates an image/video element with src=|url| and an optional |cross_origin| +// attibute. Tries to read from the image/video using a canvas element. Returns +// NOT_TAINTED if it could be read, TAINTED if it could not be read, and +// LOAD_ERROR if loading the image/video failed. +function create_test_case_promise(url, cross_origin) { + return new Promise(resolve => { + if (url.indexOf('PNGIMAGE') != -1) { + const img = document.createElement('img'); + if (cross_origin != '') { + img.crossOrigin = cross_origin; + } + img.onload = function() { + try { + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const context = canvas.getContext('2d'); + context.drawImage(img, 0, 0); + context.getImageData(0, 0, 100, 100); + resolve(NOT_TAINTED); + } catch (e) { + resolve(TAINTED); + } + }; + img.onerror = function() { + resolve(LOAD_ERROR); + } + img.src = url; + return; + } + + if (url.indexOf('VIDEO') != -1) { + const video = document.createElement('video'); + video.autoplay = true; + video.muted = true; + if (cross_origin != '') { + video.crossOrigin = cross_origin; + } + video.onplay = function() { + try { + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const context = canvas.getContext('2d'); + context.drawImage(video, 0, 0); + context.getImageData(0, 0, 100, 100); + resolve(NOT_TAINTED); + } catch (e) { + resolve(TAINTED); + } + }; + video.onerror = function() { + resolve(LOAD_ERROR); + } + video.src = url; + return; + } + + resolve('unknown resource type'); + }); +} +</script> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js new file mode 100644 index 0000000000..2aada3669e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js @@ -0,0 +1,241 @@ +// This is the main driver of the canvas tainting tests. +const NOT_TAINTED = 'NOT_TAINTED'; +const TAINTED = 'TAINTED'; +const LOAD_ERROR = 'LOAD_ERROR'; + +let frame; + +// Creates a single promise_test. +function canvas_taint_test(url, cross_origin, expected_result) { + promise_test(t => { + return frame.contentWindow.create_test_case_promise(url, cross_origin) + .then(result => { + assert_equals(result, expected_result); + }); + }, 'url "' + url + '" with crossOrigin "' + cross_origin + '" should be ' + + expected_result); +} + + +// Runs all the tests. The given |params| has these properties: +// * |resource_path|: the relative path to the (image/video) resource to test. +// * |cache|: when true, the service worker bounces responses into +// Cache Storage and back out before responding with them. +function do_canvas_tainting_tests(params) { + const host_info = get_host_info(); + let resource_path = params.resource_path; + if (params.cache) + resource_path += "&cache=true"; + const resource_url = host_info['HTTPS_ORIGIN'] + resource_path; + const remote_resource_url = host_info['HTTPS_REMOTE_ORIGIN'] + resource_path; + + // Set up the service worker and the frame. + promise_test(function(t) { + const SCOPE = 'resources/fetch-canvas-tainting-iframe.html'; + const SCRIPT = 'resources/fetch-rewrite-worker.js'; + const host_info = get_host_info(); + + // login_https() is needed because some test cases use credentials. + return login_https(t) + .then(function() { + return service_worker_unregister_and_register(t, SCRIPT, SCOPE); + }) + .then(function(registration) { + promise_test(() => { + if (frame) + frame.remove(); + return registration.unregister(); + }, 'restore global state'); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(f => { + frame = f; + }); + }, 'initialize global state'); + + // Reject tests. Add '&reject' so the service worker responds with a rejected promise. + // A load error is expected. + canvas_taint_test(resource_url + '&reject', '', LOAD_ERROR); + canvas_taint_test(resource_url + '&reject', 'anonymous', LOAD_ERROR); + canvas_taint_test(resource_url + '&reject', 'use-credentials', LOAD_ERROR); + + // Fallback tests. Add '&ignore' so the service worker does not respond to the fetch + // request, and we fall back to network. + canvas_taint_test(resource_url + '&ignore', '', NOT_TAINTED); + canvas_taint_test(remote_resource_url + '&ignore', '', TAINTED); + canvas_taint_test(remote_resource_url + '&ignore', 'anonymous', LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ignore', + 'anonymous', + NOT_TAINTED); + canvas_taint_test(remote_resource_url + '&ignore', 'use-credentials', LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ignore', + 'use-credentials', + LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ACACredentials=true&ignore', + 'use-credentials', + NOT_TAINTED); + + // Credential tests (with fallback). Add '&Auth' so the server requires authentication. + // Furthermore, add '&ignore' so the service worker falls back to network. + canvas_taint_test(resource_url + '&Auth&ignore', '', NOT_TAINTED); + canvas_taint_test(remote_resource_url + '&Auth&ignore', '', TAINTED); + canvas_taint_test( + remote_resource_url + '&Auth&ignore', 'anonymous', LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&Auth&ignore', + 'use-credentials', + LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ignore', + 'use-credentials', + LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ACACredentials=true&ignore', + 'use-credentials', + NOT_TAINTED); + + // In the following tests, the service worker provides a response. + // Add '&url' so the service worker responds with fetch(url). + // Add '&mode' to configure the fetch request options. + + // Basic response tests. Set &url to the original url. + canvas_taint_test( + resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url), + '', + NOT_TAINTED); + canvas_taint_test( + resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url), + 'anonymous', + NOT_TAINTED); + canvas_taint_test( + resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url), + 'use-credentials', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=same-origin&url=' + + encodeURIComponent(resource_url), + '', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=same-origin&url=' + + encodeURIComponent(resource_url), + 'anonymous', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=same-origin&url=' + + encodeURIComponent(resource_url), + 'use-credentials', + NOT_TAINTED); + + // Opaque response tests. Set &url to the cross-origin URL, and &mode to + // 'no-cors' so we expect an opaque response. + canvas_taint_test( + resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + '', + TAINTED); + canvas_taint_test( + resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + 'anonymous', + LOAD_ERROR); + canvas_taint_test( + resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + 'use-credentials', + LOAD_ERROR); + canvas_taint_test( + remote_resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + '', + TAINTED); + canvas_taint_test( + remote_resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + 'anonymous', + LOAD_ERROR); + canvas_taint_test( + remote_resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + 'use-credentials', + LOAD_ERROR); + + // CORS response tests. Set &url to the cross-origin URL, and &mode + // to 'cors' to attempt a CORS request. + canvas_taint_test( + resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + '', + LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond + // with an Access-Control-Allow-Credentials header. + canvas_taint_test( + resource_url + '&mode=cors&credentials=same-origin&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + '', + NOT_TAINTED); + canvas_taint_test( + resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'anonymous', + NOT_TAINTED); + canvas_taint_test( + resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'use-credentials', + LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond + // with an Access-Control-Allow-Credentials header. + canvas_taint_test( + resource_url + '&mode=cors&url=' + + encodeURIComponent( + remote_resource_url + + '&ACACredentials=true&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'use-credentials', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + '', + LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond + // with an Access-Control-Allow-Credentials header. + canvas_taint_test( + remote_resource_url + '&mode=cors&credentials=same-origin&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + '', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'anonymous', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'use-credentials', + LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond + // with an Access-Control-Allow-Credentials header. + canvas_taint_test( + remote_resource_url + '&mode=cors&url=' + + encodeURIComponent( + remote_resource_url + + '&ACACredentials=true&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'use-credentials', + NOT_TAINTED); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js new file mode 100644 index 0000000000..145952a22c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', (e) => { + e.respondWith(fetch(e.request)); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html new file mode 100644 index 0000000000..d88c5103d3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html @@ -0,0 +1,170 @@ +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js?pipe=sub"></script> +<script> +var path = base_path() + 'fetch-access-control.py'; +var host_info = get_host_info(); +var SUCCESS = 'SUCCESS'; +var FAIL = 'FAIL'; + +function create_test_case_promise(url, with_credentials) { + return new Promise(function(resolve) { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + if (xhr.status == 200) { + resolve(SUCCESS); + } else { + resolve("STATUS" + xhr.status); + } + } + xhr.onerror = function() { + resolve(FAIL); + } + xhr.responseType = 'text'; + xhr.withCredentials = with_credentials; + xhr.open('GET', url, true); + xhr.send(); + }); +} + +window.addEventListener('message', async (evt) => { + var port = evt.ports[0]; + var url = host_info['HTTPS_ORIGIN'] + path; + var remote_url = host_info['HTTPS_REMOTE_ORIGIN'] + path; + var TEST_CASES = [ + // Reject tests + [url + '?reject', false, FAIL], + [url + '?reject', true, FAIL], + [remote_url + '?reject', false, FAIL], + [remote_url + '?reject', true, FAIL], + // Event handler exception tests + [url + '?throw', false, SUCCESS], + [url + '?throw', true, SUCCESS], + [remote_url + '?throw', false, FAIL], + [remote_url + '?throw', true, FAIL], + // Reject(resolve-null) tests + [url + '?resolve-null', false, FAIL], + [url + '?resolve-null', true, FAIL], + [remote_url + '?resolve-null', false, FAIL], + [remote_url + '?resolve-null', true, FAIL], + // Fallback tests + [url + '?ignore', false, SUCCESS], + [url + '?ignore', true, SUCCESS], + [remote_url + '?ignore', false, FAIL, true], // Executed in serial. + [remote_url + '?ignore', true, FAIL, true], // Executed in serial. + [ + remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore', + false, SUCCESS + ], + [ + remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore', + true, FAIL, true // Executed in serial. + ], + [ + remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ACACredentials=true&ignore', + true, SUCCESS + ], + // Credential test (fallback) + [url + '?Auth&ignore', false, SUCCESS], + [url + '?Auth&ignore', true, SUCCESS], + [remote_url + '?Auth&ignore', false, FAIL], + [remote_url + '?Auth&ignore', true, FAIL], + [ + remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore', + false, 'STATUS401' + ], + [ + remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore', + true, FAIL, true // Executed in serial. + ], + [ + remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ACACredentials=true&ignore', + true, SUCCESS + ], + // Basic response + [ + url + '?mode=same-origin&url=' + encodeURIComponent(url), + false, SUCCESS + ], + [ + url + '?mode=same-origin&url=' + encodeURIComponent(url), + false, SUCCESS + ], + [ + remote_url + '?mode=same-origin&url=' + encodeURIComponent(url), + false, SUCCESS + ], + [ + remote_url + '?mode=same-origin&url=' + encodeURIComponent(url), + false, SUCCESS + ], + // Opaque response + [ + url + '?mode=no-cors&url=' + encodeURIComponent(remote_url), + false, FAIL + ], + [ + url + '?mode=no-cors&url=' + encodeURIComponent(remote_url), + false, FAIL + ], + [ + remote_url + '?mode=no-cors&url=' + encodeURIComponent(remote_url), + false, FAIL + ], + [ + remote_url + '?mode=no-cors&url=' + encodeURIComponent(remote_url), + false, FAIL + ], + // CORS response + [ + url + '?mode=cors&url=' + + encodeURIComponent(remote_url + '?ACAOrigin=' + + host_info['HTTPS_ORIGIN']), + false, SUCCESS + ], + [ + url + '?mode=cors&url=' + + encodeURIComponent(remote_url + '?ACAOrigin=' + + host_info['HTTPS_ORIGIN']), + true, FAIL + ], + [ + url + '?mode=cors&url=' + + encodeURIComponent(remote_url + '?ACAOrigin=' + + host_info['HTTPS_ORIGIN'] + + '&ACACredentials=true'), + true, SUCCESS + ], + [ + remote_url + '?mode=cors&url=' + + encodeURIComponent(remote_url + '?ACAOrigin=' + + host_info['HTTPS_ORIGIN']), + false, SUCCESS + ], + [ + remote_url + + '?mode=cors&url=' + + encodeURIComponent(remote_url + '?ACAOrigin=' + + host_info['HTTPS_ORIGIN']), + true, FAIL + ], + [ + remote_url + + '?mode=cors&url=' + + encodeURIComponent(remote_url + '?ACAOrigin=' + + host_info['HTTPS_ORIGIN'] + + '&ACACredentials=true'), + true, SUCCESS + ] + ]; + + let counter = 0; + for (let test of TEST_CASES) { + let result = await create_test_case_promise(test[0], test[1]); + let testName = 'test ' + (++counter) + ': ' + test[0] + ' with credentials ' + test[1] + ' must be ' + test[2]; + port.postMessage({testName: testName, result: result === test[2]}); + } + port.postMessage('done'); + }, false); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html new file mode 100644 index 0000000000..33bf0416d5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html @@ -0,0 +1,16 @@ +<script> +var meta = document.createElement('meta'); +meta.setAttribute('http-equiv', 'Content-Security-Policy'); +meta.setAttribute('content', decodeURIComponent(location.search.substring(1))); +document.head.appendChild(meta); + +function load_image(url) { + return new Promise(function(resolve, reject) { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = resolve; + img.onerror = reject; + img.src = url; + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers new file mode 100644 index 0000000000..5a1c7b941a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers @@ -0,0 +1 @@ +Content-Security-Policy: img-src https://{{host}}:{{ports[https][0]}}; connect-src 'unsafe-inline' 'self' diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-error-worker.js new file mode 100644 index 0000000000..788252cf3b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-error-worker.js @@ -0,0 +1,22 @@ +importScripts("/resources/testharness.js"); + +function doTest(event) +{ + if (!event.request.url.includes("fetch-error-test")) + return; + + let counter = 0; + const stream = new ReadableStream({ pull: controller => { + switch (++counter) { + case 1: + controller.enqueue(new Uint8Array([1])); + return; + default: + // We asynchronously error the stream so that there is ample time to resolve the fetch promise and call text() on the response. + step_timeout(() => controller.error("Sorry"), 50); + } + }}); + event.respondWith(new Response(stream)); +} + +self.addEventListener("fetch", doTest); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js new file mode 100644 index 0000000000..a5a44a57c9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js @@ -0,0 +1,6 @@ +importScripts('/resources/testharness.js'); + +promise_test(async () => { + await new Promise(handler => { step_timeout(handler, 0); }); + self.addEventListener('fetch', () => {}); +}, 'fetch event added asynchronously does not throw'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html new file mode 100644 index 0000000000..bf8a6d5ce5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script> +function fetch_url(url) { + return new Promise(function(resolve, reject) { + var request = new XMLHttpRequest(); + request.addEventListener('load', function(event) { + if (request.status == 200) + resolve(request.response); + else + reject(new Error('fetch_url: ' + request.statusText + " : " + url)); + }); + request.addEventListener('error', function(event) { + reject(new Error('fetch_url encountered an error: ' + url)); + }); + request.addEventListener('abort', function(event) { + reject(new Error('fetch_url was aborted: ' + url)); + }); + request.open('GET', url); + request.send(); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js new file mode 100644 index 0000000000..dc3f1a1e98 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js @@ -0,0 +1,66 @@ +// This worker attempts to call respondWith() asynchronously after the +// fetch event handler finished. It reports back to the test whether +// an exception was thrown. + +// These get reset at the start of a test case. +let reportResult; + +// The test page sends a message to tell us that a new test case is starting. +// We expect a fetch event after this. +self.addEventListener('message', (event) => { + // Ensure tests run mutually exclusive. + if (reportResult) { + event.source.postMessage('testAlreadyRunning'); + return; + } + + const resultPromise = new Promise((resolve) => { + reportResult = resolve; + // Tell the client that everything is initialized and that it's safe to + // proceed with the test without relying on the order of events (which some + // browsers like Chrome may not guarantee). + event.source.postMessage('messageHandlerInitialized'); + }); + + // Keep the worker alive until the test case finishes, and report + // back the result to the test page. + event.waitUntil(resultPromise.then(result => { + reportResult = null; + event.source.postMessage(result); + })); +}); + +// Calls respondWith() and reports back whether an exception occurred. +function tryRespondWith(event) { + try { + event.respondWith(new Response()); + reportResult({didThrow: false}); + } catch (error) { + reportResult({didThrow: true, error: error.name}); + } +} + +function respondWithInTask(event) { + setTimeout(() => { + tryRespondWith(event); + }, 0); +} + +function respondWithInMicrotask(event) { + Promise.resolve().then(() => { + tryRespondWith(event); + }); +} + +self.addEventListener('fetch', function(event) { + const path = new URL(event.request.url).pathname; + const test = path.substring(path.lastIndexOf('/') + 1); + + // If this is a test case, try respondWith() and report back to the test page + // the result. + if (test == 'respondWith-in-task') { + respondWithInTask(event); + } else if (test == 'respondWith-in-microtask') { + respondWithInMicrotask(event); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js new file mode 100644 index 0000000000..53ee149374 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js @@ -0,0 +1,37 @@ +// This worker reports back the final state of FetchEvent.handled (RESOLVED or +// REJECTED) to the test. + +self.addEventListener('message', function(event) { + self.port = event.data.port; +}); + +self.addEventListener('fetch', function(event) { + try { + event.handled.then(() => { + self.port.postMessage('RESOLVED'); + }, () => { + self.port.postMessage('REJECTED'); + }); + } catch (e) { + self.port.postMessage('FAILED'); + return; + } + + const search = new URL(event.request.url).search; + switch (search) { + case '?respondWith-not-called': + break; + case '?respondWith-not-called-and-event-canceled': + event.preventDefault(); + break; + case '?respondWith-called-and-promise-resolved': + event.respondWith(Promise.resolve(new Response('body'))); + break; + case '?respondWith-called-and-promise-resolved-to-invalid-response': + event.respondWith(Promise.resolve('invalid response')); + break; + case '?respondWith-called-and-promise-rejected': + event.respondWith(Promise.reject(new Error('respondWith rejected'))); + break; + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html new file mode 100644 index 0000000000..f6c1919bbc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<script> +function fetch_url(url) { + return new Promise(function(resolve, reject) { + var request = new XMLHttpRequest(); + request.addEventListener('load', function(event) { + resolve(); + }); + request.addEventListener('error', function(event) { + reject(); + }); + request.open('GET', url); + request.send(); + }); +} + +function make_test(testcase) { + var name = testcase.name; + return fetch_url(window.location.href + '?' + name) + .then( + function() { + if (testcase.expect_load) + return Promise.resolve(); + return Promise.reject(new Error( + name + ': expected network error but loaded')); + }, + function() { + if (!testcase.expect_load) + return Promise.resolve(); + return Promise.reject(new Error( + name + ': expected to load but got network error')); + }); +} + +function run_tests() { + var tests = [ + { name: 'prevent-default-and-respond-with', expect_load: true }, + { name: 'prevent-default', expect_load: false }, + { name: 'reject', expect_load: false }, + { name: 'unused-body', expect_load: true }, + { name: 'used-body', expect_load: false }, + { name: 'unused-fetched-body', expect_load: true }, + { name: 'used-fetched-body', expect_load: false }, + { name: 'throw-exception', expect_load: true }, + ].map(make_test); + + Promise.all(tests) + .then(function() { + window.parent.notify_test_done('PASS'); + }) + .catch(function(error) { + window.parent.notify_test_done('FAIL: ' + error.message); + }); +} + +if (!navigator.serviceWorker.controller) + window.parent.notify_test_done('FAIL: no controller'); +else + run_tests(); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js new file mode 100644 index 0000000000..5bfe3a0bbd --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js @@ -0,0 +1,49 @@ +// Test that multiple fetch handlers do not confuse the implementation. +self.addEventListener('fetch', function(event) {}); + +self.addEventListener('fetch', function(event) { + var testcase = new URL(event.request.url).search; + switch (testcase) { + case '?reject': + event.respondWith(Promise.reject()); + break; + case '?prevent-default': + event.preventDefault(); + break; + case '?prevent-default-and-respond-with': + event.preventDefault(); + break; + case '?unused-body': + event.respondWith(new Response('body')); + break; + case '?used-body': + var res = new Response('body'); + res.text(); + event.respondWith(res); + break; + case '?unused-fetched-body': + event.respondWith(fetch('other.html').then(function(res){ + return res; + })); + break; + case '?used-fetched-body': + event.respondWith(fetch('other.html').then(function(res){ + res.text(); + return res; + })); + break; + case '?throw-exception': + throw('boom'); + break; + } + }); + +self.addEventListener('fetch', function(event) {}); + +self.addEventListener('fetch', function(event) { + var testcase = new URL(event.request.url).search; + if (testcase == '?prevent-default-and-respond-with') + event.respondWith(new Response('responding!')); + }); + +self.addEventListener('fetch', function(event) {}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js new file mode 100644 index 0000000000..376bdbed05 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', () => { + // Do nothing. +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html new file mode 100644 index 0000000000..0ebd1ca815 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<script> +function fetch_url(url) { + return new Promise(function(resolve, reject) { + var request = new XMLHttpRequest(); + request.addEventListener('load', function(event) { + resolve(); + }); + request.addEventListener('error', function(event) { + reject(); + }); + request.open('GET', url); + request.send(); + }); +} + +function make_test(testcase) { + var name = testcase.name; + return fetch_url(window.location.href + '?' + name) + .then( + function() { + if (testcase.expect_load) + return Promise.resolve(); + return Promise.reject(new Error( + name + ': expected network error but loaded')); + }, + function() { + if (!testcase.expect_load) + return Promise.resolve(); + return Promise.reject(new Error( + name + ': expected to load but got network error')); + }); +} + +function run_tests() { + var tests = [ + { name: 'response-object', expect_load: true }, + { name: 'response-promise-object', expect_load: true }, + { name: 'other-value', expect_load: false }, + ].map(make_test); + + Promise.all(tests) + .then(function() { + window.parent.notify_test_done('PASS'); + }) + .catch(function(error) { + window.parent.notify_test_done('FAIL: ' + error.message); + }); +} + +if (!navigator.serviceWorker.controller) + window.parent.notify_test_done('FAIL: no controller'); +else + run_tests(); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js new file mode 100644 index 0000000000..712c4b73c9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js @@ -0,0 +1,14 @@ +self.addEventListener('fetch', function(event) { + var testcase = new URL(event.request.url).search; + switch (testcase) { + case '?response-object': + event.respondWith(new Response('body')); + break; + case '?response-promise-object': + event.respondWith(Promise.resolve(new Response('body'))); + break; + case '?other-value': + event.respondWith(new Object()); + break; + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js new file mode 100644 index 0000000000..d3ba8a8df2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js @@ -0,0 +1,7 @@ +'use strict'; + +self.addEventListener('fetch', event => { + if (!event.request.url.match(/body-in-chunk$/)) + return; + event.respondWith(fetch("../../../fetch/api/resources/trickle.py?count=4&delay=50")); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js new file mode 100644 index 0000000000..ff24aed128 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js @@ -0,0 +1,45 @@ +'use strict'; + +addEventListener('fetch', event => { + const url = new URL(event.request.url); + const type = url.searchParams.get('type'); + + if (!type) return; + + if (type === 'string') { + event.respondWith(new Response('PASS')); + } + else if (type === 'blob') { + event.respondWith( + new Response(new Blob(['PASS'])) + ); + } + else if (type === 'buffer-view') { + const encoder = new TextEncoder(); + event.respondWith( + new Response(encoder.encode('PASS')) + ); + } + else if (type === 'buffer') { + const encoder = new TextEncoder(); + event.respondWith( + new Response(encoder.encode('PASS').buffer) + ); + } + else if (type === 'form-data') { + const body = new FormData(); + body.set('result', 'PASS'); + event.respondWith( + new Response(body) + ); + } + else if (type === 'search-params') { + const body = new URLSearchParams(); + body.set('result', 'PASS'); + event.respondWith( + new Response(body, { + headers: { 'Content-Type': 'text/plain' } + }) + ); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js new file mode 100644 index 0000000000..b7307f29f5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js @@ -0,0 +1,28 @@ +let waitUntilResolve; + +let bodyController; + +self.addEventListener('message', evt => { + if (evt.data === 'done') { + bodyController.close(); + waitUntilResolve(); + } +}); + +self.addEventListener('fetch', evt => { + if (!evt.request.url.includes('partial-stream.txt')) { + return; + } + + evt.waitUntil(new Promise(resolve => waitUntilResolve = resolve)); + + let body = new ReadableStream({ + start: controller => { + let encoder = new TextEncoder(); + controller.enqueue(encoder.encode('partial-stream-content')); + bodyController = controller; + }, + }); + + evt.respondWith(new Response(body)); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js new file mode 100644 index 0000000000..f954e3a18a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js @@ -0,0 +1,40 @@ +'use strict'; + +self.addEventListener('fetch', event => { + if (!event.request.url.match(/body-stream$/)) + return; + + var counter = 0; + const encoder = new TextEncoder(); + const stream = new ReadableStream({ pull: controller => { + switch (++counter) { + case 1: + controller.enqueue(encoder.encode('')); + return; + case 2: + controller.enqueue(encoder.encode('chunk #1')); + return; + case 3: + controller.enqueue(encoder.encode(' ')); + return; + case 4: + controller.enqueue(encoder.encode('chunk #2')); + return; + case 5: + controller.enqueue(encoder.encode(' ')); + return; + case 6: + controller.enqueue(encoder.encode('chunk #3')); + return; + case 7: + controller.enqueue(encoder.encode(' ')); + return; + case 8: + controller.enqueue(encoder.encode('chunk #4')); + return; + default: + controller.close(); + } + }}); + event.respondWith(new Response(stream)); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js new file mode 100644 index 0000000000..056351322c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js @@ -0,0 +1,81 @@ +'use strict'; +importScripts("/resources/testharness.js"); + +const map = new Map(); + +self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + if (!url.searchParams.has('stream')) return; + + if (url.searchParams.has('observe-cancel')) { + const id = url.searchParams.get('id'); + if (id === undefined) { + event.respondWith(new Error('error')); + return; + } + event.waitUntil(new Promise(resolve => { + map.set(id, {label: 'pending', resolve}); + })); + + const stream = new ReadableStream({ + pull(c) { + if (url.searchParams.get('enqueue') === 'true') { + url.searchParams.delete('enqueue'); + c.enqueue(new Uint8Array([65])); + } + }, + cancel() { + map.get(id).label = 'cancelled'; + } + }); + event.respondWith(new Response(stream)); + return; + } + + if (url.searchParams.has('query-cancel')) { + const id = url.searchParams.get('id'); + if (id === undefined) { + event.respondWith(new Error('error')); + return; + } + const entry = map.get(id); + if (entry === undefined) { + event.respondWith(new Error('not found')); + return; + } + map.delete(id); + entry.resolve(); + event.respondWith(new Response(entry.label)); + return; + } + + if (url.searchParams.has('use-fetch-stream')) { + event.respondWith(async function() { + const response = await fetch('pass.txt'); + return new Response(response.body); + }()); + return; + } + + const delayEnqueue = url.searchParams.has('delay'); + + const stream = new ReadableStream({ + start(controller) { + const encoder = new TextEncoder(); + + const populate = () => { + controller.enqueue(encoder.encode('PASS')); + controller.close(); + } + + if (delayEnqueue) { + step_timeout(populate, 16); + } + else { + populate(); + } + } + }); + + event.respondWith(new Response(stream)); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html new file mode 100644 index 0000000000..d15454daa5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>respond-with-response-body-with-invalid-chunk</title> +<body></body> +<script> +'use strict'; + +parent.set_fetch_promise(fetch('body-stream-with-invalid-chunk').then(resp => { + const reader = resp.body.getReader(); + const reader_promise = reader.read(); + parent.set_reader_promise(reader_promise); + // Suppress our expected error. + return reader_promise.catch(() => {}); + })); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js new file mode 100644 index 0000000000..0254e24f94 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js @@ -0,0 +1,12 @@ +'use strict'; + +self.addEventListener('fetch', event => { + if (!event.request.url.match(/body-stream-with-invalid-chunk$/)) + return; + const stream = new ReadableStream({start: controller => { + // The argument is intentionally a string, not a Uint8Array. + controller.enqueue('hello'); + }}); + const headers = { 'x-content-type-options': 'nosniff' }; + event.respondWith(new Response(stream, { headers })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js new file mode 100644 index 0000000000..18da049d69 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js @@ -0,0 +1,15 @@ +var result = null; + +self.addEventListener('message', function(event) { + event.data.port.postMessage(result); + }); + +self.addEventListener('fetch', function(event) { + if (!result) + result = 'PASS'; + event.respondWith(new Response()); + }); + +self.addEventListener('fetch', function(event) { + result = 'FAIL: fetch event propagated'; + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-test-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-test-worker.js new file mode 100644 index 0000000000..813f79d1b0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-test-worker.js @@ -0,0 +1,224 @@ +function handleHeaders(event) { + const headers = Array.from(event.request.headers); + event.respondWith(new Response(JSON.stringify(headers))); +} + +function handleString(event) { + event.respondWith(new Response('Test string')); +} + +function handleBlob(event) { + event.respondWith(new Response(new Blob(['Test blob']))); +} + +function handleReferrer(event) { + event.respondWith(new Response(new Blob( + ['Referrer: ' + event.request.referrer]))); +} + +function handleReferrerPolicy(event) { + event.respondWith(new Response(new Blob( + ['ReferrerPolicy: ' + event.request.referrerPolicy]))); +} + +function handleReferrerFull(event) { + event.respondWith(new Response(new Blob( + ['Referrer: ' + event.request.referrer + '\n' + + 'ReferrerPolicy: ' + event.request.referrerPolicy]))); +} + +function handleClientId(event) { + var body; + if (event.clientId !== "") { + body = 'Client ID Found: ' + event.clientId; + } else { + body = 'Client ID Not Found'; + } + event.respondWith(new Response(body)); +} + +function handleResultingClientId(event) { + var body; + if (event.resultingClientId !== "") { + body = 'Resulting Client ID Found: ' + event.resultingClientId; + } else { + body = 'Resulting Client ID Not Found'; + } + event.respondWith(new Response(body)); +} + +function handleNullBody(event) { + event.respondWith(new Response()); +} + +function handleFetch(event) { + event.respondWith(fetch('other.html')); +} + +function handleFormPost(event) { + event.respondWith(new Promise(function(resolve) { + event.request.text() + .then(function(result) { + resolve(new Response(event.request.method + ':' + + event.request.headers.get('Content-Type') + ':' + + result)); + }); + })); +} + +function handleMultipleRespondWith(event) { + var logForMultipleRespondWith = ''; + for (var i = 0; i < 3; ++i) { + logForMultipleRespondWith += '(' + i + ')'; + try { + event.respondWith(new Promise(function(resolve) { + setTimeout(function() { + resolve(new Response(logForMultipleRespondWith)); + }, 0); + })); + } catch (e) { + logForMultipleRespondWith += '[' + e.name + ']'; + } + } +} + +var lastResponseForUsedCheck = undefined; + +function handleUsedCheck(event) { + if (!lastResponseForUsedCheck) { + event.respondWith(fetch('other.html').then(function(response) { + lastResponseForUsedCheck = response; + return response; + })); + } else { + event.respondWith(new Response( + 'bodyUsed: ' + lastResponseForUsedCheck.bodyUsed)); + } +} +function handleFragmentCheck(event) { + var body; + if (event.request.url.indexOf('#') === -1) { + body = 'Fragment Not Found'; + } else { + body = 'Fragment Found :' + + event.request.url.substring(event.request.url.indexOf('#')); + } + event.respondWith(new Response(body)); +} +function handleCache(event) { + event.respondWith(new Response(event.request.cache)); +} +function handleEventSource(event) { + if (event.request.mode === 'navigate') { + return; + } + var data = { + mode: event.request.mode, + cache: event.request.cache, + credentials: event.request.credentials + }; + var body = 'data:' + JSON.stringify(data) + '\n\n'; + event.respondWith(new Response(body, { + headers: { 'Content-Type': 'text/event-stream' } + } + )); +} + +function handleIntegrity(event) { + event.respondWith(new Response(event.request.integrity)); +} + +function handleRequestBody(event) { + event.respondWith(event.request.text().then(text => { + return new Response(text); + })); +} + +function handleKeepalive(event) { + event.respondWith(new Response(event.request.keepalive)); +} + +function handleIsReloadNavigation(event) { + const request = event.request; + const body = + `method = ${request.method}, ` + + `isReloadNavigation = ${request.isReloadNavigation}`; + event.respondWith(new Response(body)); +} + +function handleIsHistoryNavigation(event) { + const request = event.request; + const body = + `method = ${request.method}, ` + + `isHistoryNavigation = ${request.isHistoryNavigation}`; + event.respondWith(new Response(body)); +} + +function handleUseAndIgnore(event) { + const request = event.request; + request.text(); + return; +} + +function handleCloneAndIgnore(event) { + const request = event.request; + request.clone().text(); + return; +} + +var handle_status_count = 0; +function handleStatus(event) { + handle_status_count++; + event.respondWith(async function() { + const res = await fetch(event.request); + const text = await res.text(); + return new Response(`${text}. Request was sent ${handle_status_count} times.`, + {"status": new URL(event.request.url).searchParams.get("status")}); + }()); +} + +self.addEventListener('fetch', function(event) { + var url = event.request.url; + var handlers = [ + { pattern: '?headers', fn: handleHeaders }, + { pattern: '?string', fn: handleString }, + { pattern: '?blob', fn: handleBlob }, + { pattern: '?referrerFull', fn: handleReferrerFull }, + { pattern: '?referrerPolicy', fn: handleReferrerPolicy }, + { pattern: '?referrer', fn: handleReferrer }, + { pattern: '?clientId', fn: handleClientId }, + { pattern: '?resultingClientId', fn: handleResultingClientId }, + { pattern: '?ignore', fn: function() {} }, + { pattern: '?null', fn: handleNullBody }, + { pattern: '?fetch', fn: handleFetch }, + { pattern: '?form-post', fn: handleFormPost }, + { pattern: '?multiple-respond-with', fn: handleMultipleRespondWith }, + { pattern: '?used-check', fn: handleUsedCheck }, + { pattern: '?fragment-check', fn: handleFragmentCheck }, + { pattern: '?cache', fn: handleCache }, + { pattern: '?eventsource', fn: handleEventSource }, + { pattern: '?integrity', fn: handleIntegrity }, + { pattern: '?request-body', fn: handleRequestBody }, + { pattern: '?keepalive', fn: handleKeepalive }, + { pattern: '?isReloadNavigation', fn: handleIsReloadNavigation }, + { pattern: '?isHistoryNavigation', fn: handleIsHistoryNavigation }, + { pattern: '?use-and-ignore', fn: handleUseAndIgnore }, + { pattern: '?clone-and-ignore', fn: handleCloneAndIgnore }, + { pattern: '?status', fn: handleStatus }, + ]; + + var handler = null; + for (var i = 0; i < handlers.length; ++i) { + if (url.indexOf(handlers[i].pattern) != -1) { + handler = handlers[i]; + break; + } + } + + if (handler) { + handler.fn(event); + } else { + event.respondWith(new Response(new Blob( + ['Service Worker got an unexpected request: ' + url]))); + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js new file mode 100644 index 0000000000..5903bab968 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js @@ -0,0 +1,48 @@ +skipWaiting(); + +addEventListener('fetch', event => { + const url = new URL(event.request.url); + + if (url.origin != location.origin) return; + + if (url.pathname.endsWith('/sample.txt')) { + event.respondWith(new Response('intercepted')); + return; + } + + if (url.pathname.endsWith('/sample.txt-inner-fetch')) { + event.respondWith(fetch('sample.txt')); + return; + } + + if (url.pathname.endsWith('/sample.txt-inner-cache')) { + event.respondWith( + caches.open('test-inner-cache').then(cache => + cache.add('sample.txt').then(() => cache.match('sample.txt')) + ) + ); + return; + } + + if (url.pathname.endsWith('/show-notification')) { + // Copy the currect search string onto the icon url + const iconURL = new URL('notification_icon.py', location); + iconURL.search = url.search; + + event.respondWith( + registration.showNotification('test', { + icon: iconURL + }).then(() => registration.getNotifications()).then(notifications => { + for (const n of notifications) n.close(); + return new Response('done'); + }) + ); + return; + } + + if (url.pathname.endsWith('/notification_icon.py')) { + new BroadcastChannel('icon-request').postMessage('yay'); + event.respondWith(new Response('done')); + return; + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html new file mode 100644 index 0000000000..0d9ab6ff90 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html @@ -0,0 +1,66 @@ +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js?pipe=sub"></script> +<script> + var host_info = get_host_info(); + var uri = document.location + '?check-ua-header'; + + var headers = new Headers(); + headers.set('User-Agent', 'custom_ua'); + + // Check the custom UA case + fetch(uri, { headers: headers }).then(function(response) { + return response.text(); + }).then(function(text) { + if (text == 'custom_ua') { + parent.postMessage('PASS', '*'); + } else { + parent.postMessage('withUA FAIL - expected "custom_ua", got "' + text + '"', '*'); + } + }).catch(function(err) { + parent.postMessage('withUA FAIL - unexpected error: ' + err, '*'); + }); + + // Check the default UA case + fetch(uri, {}).then(function(response) { + return response.text(); + }).then(function(text) { + if (text == 'NO_UA') { + parent.postMessage('PASS', '*'); + } else { + parent.postMessage('noUA FAIL - expected "NO_UA", got "' + text + '"', '*'); + } + }).catch(function(err) { + parent.postMessage('noUA FAIL - unexpected error: ' + err, '*'); + }); + + var uri = document.location + '?check-accept-header'; + var headers = new Headers(); + headers.set('Accept', 'hmm'); + + // Check for custom accept header + fetch(uri, { headers: headers }).then(function(response) { + return response.text(); + }).then(function(text) { + if (text === headers.get('Accept')) { + parent.postMessage('PASS', '*'); + } else { + parent.postMessage('custom accept FAIL - expected ' + headers.get('Accept') + + ' got "' + text + '"', '*'); + } + }).catch(function(err) { + parent.postMessage('custom accept FAIL - unexpected error: ' + err, '*'); + }); + + // Check for default accept header + fetch(uri).then(function(response) { + return response.text(); + }).then(function(text) { + if (text === '*/*') { + parent.postMessage('PASS', '*'); + } else { + parent.postMessage('accept FAIL - expected */* got "' + text + '"', '*'); + } + }).catch(function(err) { + parent.postMessage('accept FAIL - unexpected error: ' + err, '*'); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html new file mode 100644 index 0000000000..64a634e9db --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html @@ -0,0 +1,71 @@ +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js?pipe=sub"></script> +<script> +var image_path = base_path() + 'fetch-access-control.py?PNGIMAGE'; +var host_info = get_host_info(); +var results = ''; + +function test1() { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = function() { + test2(); + }; + img.onerror = function() { + results += 'FAIL(1)'; + test2(); + }; + img.src = './sample?url=' + + encodeURIComponent(host_info['HTTPS_ORIGIN'] + image_path); +} + +function test2() { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = function() { + test3(); + }; + img.onerror = function() { + results += 'FAIL(2)'; + test3(); + }; + img.src = './sample?mode=no-cors&url=' + + encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + image_path); +} + +function test3() { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = function() { + results += 'FAIL(3)'; + test4(); + }; + img.onerror = function() { + test4(); + }; + img.src = './sample?mode=no-cors&url=' + + encodeURIComponent(host_info['HTTP_ORIGIN'] + image_path); +} + +function test4() { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = function() { + results += 'FAIL(4)'; + finish(); + }; + img.onerror = function() { + finish(); + }; + img.src = './sample?mode=no-cors&url=' + + encodeURIComponent(host_info['HTTP_REMOTE_ORIGIN'] + image_path); +} + +function finish() { + results += 'finish'; + window.parent.postMessage({results: results}, host_info['HTTPS_ORIGIN']); +} +</script> + +<body onload='test1();'> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html new file mode 100644 index 0000000000..be0b5c8f56 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html @@ -0,0 +1,80 @@ +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js?pipe=sub"></script> +<script> +var image_path = base_path() + 'fetch-access-control.py?PNGIMAGE'; +var host_info = get_host_info(); +var results = ''; + +function test1() { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = function() { + test2(); + }; + img.onerror = function() { + results += 'FAIL(1)'; + test2(); + }; + img.src = host_info['HTTPS_ORIGIN'] + image_path; +} + +function test2() { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = function() { + test3(); + }; + img.onerror = function() { + results += 'FAIL(2)'; + test3(); + }; + img.src = host_info['HTTPS_REMOTE_ORIGIN'] + image_path; +} + +function test3() { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = function() { + results += 'FAIL(3)'; + test4(); + }; + img.onerror = function() { + test4(); + }; + img.src = host_info['HTTP_ORIGIN'] + image_path; +} + +function test4() { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = function() { + results += 'FAIL(4)'; + test5(); + }; + img.onerror = function() { + test5(); + }; + img.src = host_info['HTTP_REMOTE_ORIGIN'] + image_path; +} + +function test5() { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = function() { + finish(); + }; + img.onerror = function() { + results += 'FAIL(5)'; + finish(); + }; + img.src = './sample?generate-png'; +} + +function finish() { + results += 'finish'; + window.parent.postMessage({results: results}, host_info['HTTPS_ORIGIN']); +} +</script> + +<body onload='test1();'> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html new file mode 100644 index 0000000000..2831c381b5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js?pipe=sub"></script> +<script> +var params = get_query_params(location.href); +var SCOPE = 'fetch-mixed-content-iframe-inscope-to-' + params['target'] + '.html'; +var URL = 'fetch-rewrite-worker.js'; +var host_info = get_host_info(); + +window.addEventListener('message', on_message, false); + +navigator.serviceWorker.getRegistration(SCOPE) + .then(function(registration) { + if (registration) + return registration.unregister(); + }) + .then(function() { + return navigator.serviceWorker.register(URL, {scope: SCOPE}); + }) + .then(function(registration) { + return new Promise(function(resolve) { + registration.addEventListener('updatefound', function() { + resolve(registration.installing); + }); + }); + }) + .then(function(worker) { + worker.addEventListener('statechange', on_state_change); + }) + .catch(function(reason) { + window.parent.postMessage({results: 'FAILURE: ' + reason.message}, + host_info['HTTPS_ORIGIN']); + }); + +function on_state_change(event) { + if (event.target.state != 'activated') + return; + var frame = document.createElement('iframe'); + frame.src = SCOPE; + document.body.appendChild(frame); +} + +function on_message(e) { + navigator.serviceWorker.getRegistration(SCOPE) + .then(function(registration) { + if (registration) + return registration.unregister(); + }) + .then(function() { + window.parent.postMessage(e.data, host_info['HTTPS_ORIGIN']); + }) + .catch(function(reason) { + window.parent.postMessage({results: 'FAILURE: ' + reason.message}, + host_info['HTTPS_ORIGIN']); + }); +} + +function get_query_params(url) { + var search = (new URL(url)).search; + if (!search) { + return {}; + } + var ret = {}; + var params = search.substring(1).split('&'); + params.forEach(function(param) { + var element = param.split('='); + ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]); + }); + return ret; +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html new file mode 100644 index 0000000000..504e104356 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html @@ -0,0 +1,20 @@ +<html> +<head> +<title>iframe for css base url test</title> +</head> +<body> +<script> +// Load a stylesheet. Create it dynamically so we can construct the href URL +// dynamically. +const link = document.createElement('link'); +link.rel = 'stylesheet'; +link.type = 'text/css'; +// Add "request-url-path" to the path to help distinguish the request URL from +// the response URL. Add |document.location.search| (chosen by the test main +// page) to tell the service worker how to respond to the request. +link.href = 'request-url-path/fetch-request-css-base-url-style.css' + + document.location.search; +document.head.appendChild(link); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css new file mode 100644 index 0000000000..f14fcaae72 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css @@ -0,0 +1 @@ +body { background-image: url("./sample.png");} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js new file mode 100644 index 0000000000..f3d6a73bdd --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js @@ -0,0 +1,45 @@ +let source; +let resolveDone; +let done = new Promise(resolve => resolveDone = resolve); + +// The page messages this worker to ask for the result. Keep the worker alive +// via waitUntil() until the result is sent. +self.addEventListener('message', event => { + source = event.data.port; + source.postMessage('pong'); + event.waitUntil(done); +}); + +self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + + // For the CSS file, respond in a way that may change the response URL, + // depending on |url.search|. + const cssPath = 'request-url-path/fetch-request-css-base-url-style.css'; + if (url.pathname.indexOf(cssPath) != -1) { + // Respond with a different URL, deleting "request-url-path/". + if (url.search == '?fetch') { + event.respondWith(fetch('fetch-request-css-base-url-style.css?fetch')); + } + // Respond with new Response(). + else if (url.search == '?newResponse') { + const styleString = 'body { background-image: url("./sample.png");}'; + const headers = {'content-type': 'text/css'}; + event.respondWith(new Response(styleString, headers)); + } + } + + // The image request indicates what the base URL of the CSS was. Message the + // result back to the test page. + else if (url.pathname.indexOf('sample.png') != -1) { + // For some reason |source| is undefined here when running the test manually + // in Firefox. The test author experimented with both using Client + // (event.source) and MessagePort to try to get the test to pass, but + // failed. + source.postMessage({ + url: event.request.url, + referrer: event.request.referrer + }); + resolveDone(); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css new file mode 100644 index 0000000000..9a7545d070 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css @@ -0,0 +1 @@ +#crossOriginCss { color: blue; } diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html new file mode 100644 index 0000000000..3211f78084 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html @@ -0,0 +1 @@ +#crossOriginHtml { color: red; } diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html new file mode 100644 index 0000000000..9a4adedb84 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html @@ -0,0 +1,17 @@ +<style type="text/css"> +#crossOriginCss { color: red; } +#crossOriginHtml { color: blue; } +#sameOriginCss { color: red; } +#sameOriginHtml { color: red; } +#synthetic { color: red; } +</style> +<link href="./cross-origin-css.css?mime=no" rel="stylesheet" type="text/css"> +<link href="./cross-origin-html.css?mime=no" rel="stylesheet" type="text/css"> +<link href="./fetch-request-css-cross-origin-mime-check-same.css" rel="stylesheet" type="text/css"> +<link href="./fetch-request-css-cross-origin-mime-check-same.html" rel="stylesheet" type="text/css"> +<link href="./synthetic.css?mime=no" rel="stylesheet" type="text/css"> +<h1 id=crossOriginCss>I should be blue</h1> +<h1 id=crossOriginHtml>I should be blue</h1> +<h1 id=sameOriginCss>I should be blue</h1> +<h1 id=sameOriginHtml>I should be blue</h1> +<h1 id=synthetic>I should be blue</h1> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css new file mode 100644 index 0000000000..55455bd5da --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css @@ -0,0 +1 @@ +#sameOriginCss { color: blue; } diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html new file mode 100644 index 0000000000..6fad4b9ff0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html @@ -0,0 +1 @@ +#sameOriginHtml { color: blue; } diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html new file mode 100644 index 0000000000..c902366b02 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>iframe: cross-origin CSS via service worker</title> + +<!-- Service worker responds with a cross-origin opaque response. --> +<link href="cross-origin-css.css" rel="stylesheet" type="text/css"> + +<!-- Service worker responds with a cross-origin CORS approved response. --> +<link href="cross-origin-css.css?cors" rel="stylesheet" type="text/css"> + +<!-- Service worker falls back to network. This is a same-origin response. --> +<link href="fetch-request-css-cross-origin-mime-check-same.css" rel="stylesheet" type="text/css"> + +<!-- Service worker responds with a new Response() synthetic response. --> +<link href="synthetic.css" rel="stylesheet" type="text/css"> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js new file mode 100644 index 0000000000..a71e91216c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js @@ -0,0 +1,65 @@ +importScripts('/common/get-host-info.sub.js'); +importScripts('test-helpers.sub.js'); + +const HOST_INFO = get_host_info(); +const REMOTE_ORIGIN = HOST_INFO.HTTPS_REMOTE_ORIGIN; +const BASE_PATH = base_path(); +const CSS_FILE = 'fetch-request-css-cross-origin-mime-check-cross.css'; +const HTML_FILE = 'fetch-request-css-cross-origin-mime-check-cross.html'; + +function add_pipe_header(url_str, header) { + if (url_str.indexOf('?pipe=') == -1) { + url_str += '?pipe='; + } else { + url_str += '|'; + } + url_str += `header${header}`; + return url_str; +} + +self.addEventListener('fetch', function(event) { + const url = new URL(event.request.url); + + const use_mime = + (url.searchParams.get('mime') != 'no'); + const mime_header = '(Content-Type, text/css)'; + + const use_cors = + (url.searchParams.has('cors')); + const cors_header = '(Access-Control-Allow-Origin, *)'; + + const file = url.pathname.substring(url.pathname.lastIndexOf('/') + 1); + + // Respond with a cross-origin CSS resource, using CORS if desired. + if (file == 'cross-origin-css.css') { + let fetch_url = REMOTE_ORIGIN + BASE_PATH + CSS_FILE; + if (use_mime) + fetch_url = add_pipe_header(fetch_url, mime_header); + if (use_cors) + fetch_url = add_pipe_header(fetch_url, cors_header); + const mode = use_cors ? 'cors' : 'no-cors'; + event.respondWith(fetch(fetch_url, {'mode': mode})); + return; + } + + // Respond with a cross-origin CSS resource with an HTML name. This is only + // used in the MIME sniffing test, so MIME is never added. + if (file == 'cross-origin-html.css') { + const fetch_url = REMOTE_ORIGIN + BASE_PATH + HTML_FILE; + event.respondWith(fetch(fetch_url, {mode: 'no-cors'})); + return; + } + + // Respond with synthetic CSS. + if (file == 'synthetic.css') { + let headers = {}; + if (use_mime) { + headers['Content-Type'] = 'text/css'; + } + + event.respondWith(new Response("#synthetic { color: blue; }", {headers})); + return; + } + + // Otherwise, fallback to network. + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html new file mode 100644 index 0000000000..d117d0f55e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html @@ -0,0 +1,32 @@ +<script> +function xhr(url) { + return new Promise(function(resolve, reject) { + var request = new XMLHttpRequest(); + request.addEventListener( + 'error', + function() { reject(new Error()); }); + request.addEventListener( + 'load', + function(event) { resolve(request.response); }); + request.open('GET', url); + request.send(); + }); +} + +function load_image(url, cross_origin) { + return new Promise(function(resolve, reject) { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = function() { + resolve(); + }; + img.onerror = function() { + reject(new Error()); + }; + if (cross_origin != '') { + img.crossOrigin = cross_origin; + } + img.src = url; + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js new file mode 100644 index 0000000000..3b028b24bd --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js @@ -0,0 +1,13 @@ +var requests = []; + +self.addEventListener('message', function(event) { + event.data.port.postMessage({requests: requests}); + requests = []; + }); + +self.addEventListener('fetch', function(event) { + requests.push({ + url: event.request.url, + mode: event.request.mode + }); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html new file mode 100644 index 0000000000..07a084257a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html @@ -0,0 +1,13 @@ +<script src="/common/get-host-info.sub.js"></script> +<script type="text/javascript"> + var hostInfo = get_host_info(); + var makeLink = function(id, url) { + var link = document.createElement('link'); + link.rel = 'import' + link.id = id; + link.href = url; + document.documentElement.appendChild(link); + }; + makeLink('same', hostInfo.HTTPS_ORIGIN + '/sample-dir/same.html'); + makeLink('other', hostInfo.HTTPS_REMOTE_ORIGIN + '/sample-dir/other.html'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js new file mode 100644 index 0000000000..110727bd52 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js @@ -0,0 +1,30 @@ +importScripts('/common/get-host-info.sub.js'); +var host_info = get_host_info(); + +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample-dir') == -1) { + return; + } + var result = 'mode=' + event.request.mode + + ' credentials=' + event.request.credentials; + if (url == host_info.HTTPS_ORIGIN + '/sample-dir/same.html') { + event.respondWith(new Response( + result + + '<link id="same-same" rel="import" ' + + 'href="' + host_info.HTTPS_ORIGIN + '/sample-dir/same-same.html">' + + '<link id="same-other" rel="import" ' + + ' href="' + host_info.HTTPS_REMOTE_ORIGIN + + '/sample-dir/same-other.html">')); + } else if (url == host_info.HTTPS_REMOTE_ORIGIN + '/sample-dir/other.html') { + event.respondWith(new Response( + result + + '<link id="other-same" rel="import" ' + + ' href="' + host_info.HTTPS_ORIGIN + '/sample-dir/other-same.html">' + + '<link id="other-other" rel="import" ' + + ' href="' + host_info.HTTPS_REMOTE_ORIGIN + + '/sample-dir/other-other.html">')); + } else { + event.respondWith(new Response(result)); + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html new file mode 100644 index 0000000000..e6e9380ba6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html @@ -0,0 +1 @@ +<script src="./fetch-request-no-freshness-headers-script.py"></script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py new file mode 100644 index 0000000000..bf8df154a8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py @@ -0,0 +1,6 @@ +def main(request, response): + headers = [] + # Sets an ETag header to check the cache revalidation behavior. + headers.append((b"ETag", b"abc123")) + headers.append((b"Content-Type", b"text/javascript")) + return headers, b"/* empty script */" diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js new file mode 100644 index 0000000000..2bd59d7392 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js @@ -0,0 +1,18 @@ +var requests = []; + +self.addEventListener('message', function(event) { + event.data.port.postMessage({requests: requests}); + }); + +self.addEventListener('fetch', function(event) { + var url = event.request.url; + var headers = []; + for (var header of event.request.headers) { + headers.push(header); + } + requests.push({ + url: url, + headers: headers + }); + event.respondWith(fetch(event.request)); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html new file mode 100644 index 0000000000..ffd76bfc49 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html @@ -0,0 +1,35 @@ +<script> +function xhr(url) { + return new Promise(function(resolve, reject) { + var request = new XMLHttpRequest(); + request.addEventListener( + 'error', + function(event) { reject(event); }); + request.addEventListener( + 'load', + function(event) { resolve(request.response); }); + request.open('GET', url); + request.send(); + }); +} + +function load_image(url) { + return new Promise(function(resolve, reject) { + var img = document.createElement('img'); + document.body.appendChild(img); + img.onload = resolve; + img.onerror = reject; + img.src = url; + }); +} + +function load_audio(url) { + return new Promise(function(resolve, reject) { + var audio = document.createElement('audio'); + document.body.appendChild(audio); + audio.oncanplay = resolve; + audio.onerror = reject; + audio.src = url; + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html new file mode 100644 index 0000000000..86e9f4bb35 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html @@ -0,0 +1,87 @@ +<script src="test-helpers.sub.js?pipe=sub"></script> +<body> +<script> + +function load_image(url, cross_origin) { + const img = document.createElement('img'); + if (cross_origin != '') { + img.crossOrigin = cross_origin; + } + img.src = url; +} + +function load_script(url, cross_origin) { + const script = document.createElement('script'); + script.src = url; + if (cross_origin != '') { + script.crossOrigin = cross_origin; + } + document.body.appendChild(script); +} + +function load_css(url, cross_origin) { + const link = document.createElement('link'); + link.rel = 'stylesheet' + link.href = url; + link.type = 'text/css'; + if (cross_origin != '') { + link.crossOrigin = cross_origin; + } + document.body.appendChild(link); +} + +function load_font(url) { + const fontFace = new FontFace('test', 'url(' + url + ')'); + fontFace.load(); +} + +function load_css_image(url, type) { + const div = document.createElement('div'); + document.body.appendChild(div); + div.style[type] = 'url(' + url + ')'; +} + +function load_css_image_set(url, type) { + const div = document.createElement('div'); + document.body.appendChild(div); + div.style[type] = 'image-set(url(' + url + ') 1x)'; + if (!div.style[type]) { + div.style[type] = '-webkit-image-set(url(' + url + ') 1x)'; + } +} + +function load_script_with_integrity(url, integrity) { + const script = document.createElement('script'); + script.src = url; + script.integrity = integrity; + document.body.appendChild(script); +} + +function load_css_with_integrity(url, integrity) { + const link = document.createElement('link'); + link.rel = 'stylesheet' + link.href = url; + link.type = 'text/css'; + link.integrity = integrity; + document.body.appendChild(link); +} + +function load_audio(url, cross_origin) { + const audio = document.createElement('audio'); + if (cross_origin != '') { + audio.crossOrigin = cross_origin; + } + audio.src = url; + document.body.appendChild(audio); +} + +function load_video(url, cross_origin) { + const video = document.createElement('video'); + if (cross_origin != '') { + video.crossOrigin = cross_origin; + } + video.src = url; + document.body.appendChild(video); +} +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js new file mode 100644 index 0000000000..983cccb8db --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js @@ -0,0 +1,26 @@ +const requests = []; +let port = undefined; + +self.onmessage = e => { + const message = e.data; + if ('port' in message) { + port = message.port; + port.postMessage({ready: true}); + } +}; + +self.addEventListener('fetch', e => { + const url = e.request.url; + if (!url.includes('sample?test')) { + return; + } + port.postMessage({ + url: url, + mode: e.request.mode, + redirect: e.request.redirect, + credentials: e.request.credentials, + integrity: e.request.integrity, + destination: e.request.destination + }); + e.respondWith(Promise.reject()); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html new file mode 100644 index 0000000000..b3ddec1a70 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html @@ -0,0 +1,208 @@ +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/test-helpers.sub.js?pipe=sub"></script> +<script> +var host_info = get_host_info(); + +function get_boundary(headers) { + var reg = new RegExp('multipart\/form-data; boundary=(.*)'); + for (var i = 0; i < headers.length; ++i) { + if (headers[i][0] != 'content-type') { + continue; + } + var regResult = reg.exec(headers[i][1]); + if (!regResult) { + continue; + } + return regResult[1]; + } + return ''; +} + +function xhr_send(url_base, method, data, with_credentials) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + resolve(JSON.parse(xhr.response)); + }; + xhr.onerror = function() { + reject('XHR should succeed.'); + }; + xhr.responseType = 'text'; + if (with_credentials) { + xhr.withCredentials = true; + } + xhr.open(method, url_base + '/sample?test', true); + xhr.send(data); + }); +} + +function get_sorted_header_name_list(headers) { + var header_names = []; + var idx, name; + + for (idx = 0; idx < headers.length; ++idx) { + name = headers[idx][0]; + // The `Accept-Language` header is optional; its presence should not + // influence test results. + // + // > 4. If request’s header list does not contain `Accept-Language`, user + // > agents should append `Accept-Language`/an appropriate value to + // > request's header list. + // + // https://fetch.spec.whatwg.org/#fetching + if (name === 'accept-language') { + continue; + } + + header_names.push(name); + } + header_names.sort(); + return header_names; +} + +function get_header_test() { + return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', false) + .then(function(response) { + assert_array_equals( + get_sorted_header_name_list(response.headers), + ["accept"], + 'event.request has the expected headers for same-origin GET.'); + }); +} + +function post_header_test() { + return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', '', false) + .then(function(response) { + assert_array_equals( + get_sorted_header_name_list(response.headers), + ["accept", "content-type"], + 'event.request has the expected headers for same-origin POST.'); + }); +} + +function cross_origin_get_header_test() { + return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', false) + .then(function(response) { + assert_array_equals( + get_sorted_header_name_list(response.headers), + ["accept"], + 'event.request has the expected headers for cross-origin GET.'); + }); +} + +function cross_origin_post_header_test() { + return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'POST', '', false) + .then(function(response) { + assert_array_equals( + get_sorted_header_name_list(response.headers), + ["accept", "content-type"], + 'event.request has the expected headers for cross-origin POST.'); + }); +} + +function string_test() { + return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', 'test string', false) + .then(function(response) { + assert_equals(response.method, 'POST'); + assert_equals(response.body, 'test string'); + }); +} + +function blob_test() { + return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', new Blob(['test blob']), + false) + .then(function(response) { + assert_equals(response.method, 'POST'); + assert_equals(response.body, 'test blob'); + }); +} + +function custom_method_test() { + return xhr_send(host_info['HTTPS_ORIGIN'], 'XXX', 'test string xxx', false) + .then(function(response) { + assert_equals(response.method, 'XXX'); + assert_equals(response.body, 'test string xxx'); + }); +} + +function options_method_test() { + return xhr_send(host_info['HTTPS_ORIGIN'], 'OPTIONS', 'test string xxx', false) + .then(function(response) { + assert_equals(response.method, 'OPTIONS'); + assert_equals(response.body, 'test string xxx'); + }); +} + +function form_data_test() { + var formData = new FormData(); + formData.append('sample string', '1234567890'); + formData.append('sample blob', new Blob(['blob content'])); + formData.append('sample file', new File(['file content'], 'file.dat')); + return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', formData, false) + .then(function(response) { + assert_equals(response.method, 'POST'); + var boundary = get_boundary(response.headers); + var expected_body = + '--' + boundary + '\r\n' + + 'Content-Disposition: form-data; name="sample string"\r\n' + + '\r\n' + + '1234567890\r\n' + + '--' + boundary + '\r\n' + + 'Content-Disposition: form-data; name="sample blob"; ' + + 'filename="blob"\r\n' + + 'Content-Type: application/octet-stream\r\n' + + '\r\n' + + 'blob content\r\n' + + '--' + boundary + '\r\n' + + 'Content-Disposition: form-data; name="sample file"; ' + + 'filename="file.dat"\r\n' + + 'Content-Type: application/octet-stream\r\n' + + '\r\n' + + 'file content\r\n' + + '--' + boundary + '--\r\n'; + assert_equals(response.body, expected_body, "form data response content is as expected"); + }); +} + +function mode_credentials_test() { + return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', false) + .then(function(response){ + assert_equals(response.mode, 'cors'); + assert_equals(response.credentials, 'same-origin'); + return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', true); + }) + .then(function(response){ + assert_equals(response.mode, 'cors'); + assert_equals(response.credentials, 'include'); + return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', false); + }) + .then(function(response){ + assert_equals(response.mode, 'cors'); + assert_equals(response.credentials, 'same-origin'); + return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', true); + }) + .then(function(response){ + assert_equals(response.mode, 'cors'); + assert_equals(response.credentials, 'include'); + }); +} + +function data_url_test() { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + resolve(xhr.response); + }; + xhr.onerror = function() { + reject('XHR should succeed.'); + }; + xhr.responseType = 'text'; + xhr.open('GET', 'data:text/html,Foobar', true); + xhr.send(); + }) + .then(function(data) { + assert_equals(data, 'Foobar'); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js new file mode 100644 index 0000000000..b8d3db99bc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js @@ -0,0 +1,19 @@ +"use strict"; + +self.onfetch = event => { + if (event.request.url.endsWith("non-existent-stream-1.txt")) { + const rs1 = new ReadableStream(); + event.respondWith(new Response(rs1)); + rs1.cancel(1); + } else if (event.request.url.endsWith("non-existent-stream-2.txt")) { + const rs2 = new ReadableStream({ + start(controller) { controller.error(1) } + }); + event.respondWith(new Response(rs2)); + } else if (event.request.url.endsWith("non-existent-stream-3.txt")) { + const rs3 = new ReadableStream({ + pull(controller) { controller.error(1) } + }); + event.respondWith(new Response(rs3)); + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html new file mode 100644 index 0000000000..900762ffc6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<title>Service Worker: Synchronous XHR is intercepted iframe</title> +<script> +'use strict'; + +function performSyncXHR(url) { + var syncXhr = new XMLHttpRequest(); + syncXhr.open('GET', url, false); + syncXhr.send(); + + return syncXhr; +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js new file mode 100644 index 0000000000..0d24ffc1f3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js @@ -0,0 +1,41 @@ +'use strict'; + +self.onfetch = function(event) { + if (event.request.url.indexOf('non-existent-file.txt') !== -1) { + event.respondWith(new Response('Response from service worker')); + } else if (event.request.url.indexOf('/iframe_page') !== -1) { + event.respondWith(new Response( + '<!DOCTYPE html>\n' + + '<script>\n' + + 'function performSyncXHROnWorker(url) {\n' + + ' return new Promise((resolve) => {\n' + + ' var worker =\n' + + ' new Worker(\'./worker_script\');\n' + + ' worker.addEventListener(\'message\', (msg) => {\n' + + ' resolve(msg.data);\n' + + ' });\n' + + ' worker.postMessage({\n' + + ' url: url\n' + + ' });\n' + + ' });\n' + + '}\n' + + '</script>', + { + headers: [['content-type', 'text/html']] + })); + } else if (event.request.url.indexOf('/worker_script') !== -1) { + event.respondWith(new Response( + 'self.onmessage = (msg) => {' + + ' const syncXhr = new XMLHttpRequest();' + + ' syncXhr.open(\'GET\', msg.data.url, false);' + + ' syncXhr.send();' + + ' self.postMessage({' + + ' status: syncXhr.status,' + + ' responseText: syncXhr.responseText' + + ' });' + + '}', + { + headers: [['content-type', 'application/javascript']] + })); + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js new file mode 100644 index 0000000000..070e572f40 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js @@ -0,0 +1,7 @@ +'use strict'; + +self.onfetch = function(event) { + if (event.request.url.indexOf('non-existent-file.txt') !== -1) { + event.respondWith(new Response('Response from service worker')); + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js new file mode 100644 index 0000000000..4e428374bc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js @@ -0,0 +1,22 @@ +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample?test') == -1) { + return; + } + event.respondWith(new Promise(function(resolve) { + var headers = []; + for (var header of event.request.headers) { + headers.push(header); + } + event.request.text() + .then(function(result) { + resolve(new Response(JSON.stringify({ + method: event.request.method, + mode: event.request.mode, + credentials: event.request.credentials, + headers: headers, + body: result + }))); + }); + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html new file mode 100644 index 0000000000..5f09efe28d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<body></body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html new file mode 100644 index 0000000000..c26eebee49 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html @@ -0,0 +1,53 @@ +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js?pipe=sub"></script> +<script> +var host_info = get_host_info(); + +function xhr_send(method, data) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + resolve(xhr); + }; + xhr.onerror = function() { + reject('XHR should succeed.'); + }; + xhr.responseType = 'text'; + xhr.open(method, './sample?test', true); + xhr.send(data); + }); +} + +function coalesce_headers_test() { + return xhr_send('POST', 'test string') + .then(function(xhr) { + window.parent.postMessage({results: xhr.getResponseHeader('foo')}, + host_info['HTTPS_ORIGIN']); + + return new Promise(function(resolve) { + window.addEventListener('message', function handle(evt) { + if (evt.data !== 'ACK') { + return; + } + + window.removeEventListener('message', handle); + resolve(); + }); + }); + }); +} + +window.addEventListener('message', function(evt) { + var port; + + if (evt.data !== 'START') { + return; + } + + port = evt.ports[0]; + + coalesce_headers_test() + .then(function() { port.postMessage({results: 'finish'}); }) + .catch(function(e) { port.postMessage({results: 'failure:' + e}); }); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js new file mode 100644 index 0000000000..0301b12c18 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample?test') == -1) { + return; + } + event.respondWith(new Promise(function(resolve) { + var headers = new Headers; + headers.append('foo', 'foo'); + headers.append('foo', 'bar'); + resolve(new Response('hello world', {'headers': headers})); + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.html new file mode 100644 index 0000000000..6d27cf19e5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset="utf-8"> + +<script> + const params =new URLSearchParams(location.search); + const mode = params.get("mode") || "cors"; + const path = params.get('path'); + const bufferPromise = + new Promise(resolve => + fetch(path, {mode}) + .then(response => resolve(response.arrayBuffer())) + .catch(() => resolve(new Uint8Array()))); + + const entryPromise = new Promise(resolve => { + new PerformanceObserver(entries => { + const byName = entries.getEntriesByType("resource").find(e => e.name.includes(path)); + if (byName) + resolve(byName); + }).observe({entryTypes: ["resource"]}); + }); + + Promise.all([bufferPromise, entryPromise]).then(([buffer, entry]) => { + parent.postMessage({ + buffer, + entry: entry.toJSON(), + }, '*'); + }); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.js new file mode 100644 index 0000000000..775efc0bbd --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.js @@ -0,0 +1,35 @@ +self.addEventListener('fetch', event => { + const path = event.request.url.match(/\/(?<name>[^\/]+)$/); + switch (path?.groups?.name) { + case 'constructed': + event.respondWith(new Response(new Uint8Array([1, 2, 3]))); + break; + case 'forward': + event.respondWith(fetch('/common/text-plain.txt')); + break; + case 'stream': + event.respondWith((async() => { + const res = await fetch('/common/text-plain.txt'); + const body = await res.body; + const reader = await body.getReader(); + const stream = new ReadableStream({ + async start(controller) { + while (true) { + const {done, value} = await reader.read(); + if (done) + break; + + controller.enqueue(value); + } + controller.close(); + reader.releaseLock(); + } + }); + return new Response(stream); + })()); + break; + default: + event.respondWith(fetch(event.request)); + break; + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js new file mode 100644 index 0000000000..64c99c95d8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js @@ -0,0 +1,4 @@ +// This script is intended to be served with the `Referrer-Policy` header as +// defined in the corresponding `.headers` file. + +importScripts('fetch-rewrite-worker.js'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers new file mode 100644 index 0000000000..5ae4265418 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers @@ -0,0 +1,2 @@ +Content-Type: application/javascript +Referrer-Policy: origin diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js new file mode 100644 index 0000000000..20a8066527 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js @@ -0,0 +1,166 @@ +// By default, this worker responds to fetch events with +// respondWith(fetch(request)). Additionally, if the request has a &url +// parameter, it fetches the provided URL instead. Because it forwards fetch +// events to this other URL, it is called the "fetch rewrite" worker. +// +// The worker also looks for other params on the request to do more custom +// behavior, like falling back to network or throwing an error. + +function get_query_params(url) { + var search = (new URL(url)).search; + if (!search) { + return {}; + } + var ret = {}; + var params = search.substring(1).split('&'); + params.forEach(function(param) { + var element = param.split('='); + ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]); + }); + return ret; +} + +function get_request_init(base, params) { + var init = {}; + init['method'] = params['method'] || base['method']; + init['mode'] = params['mode'] || base['mode']; + if (init['mode'] == 'navigate') { + init['mode'] = 'same-origin'; + } + init['credentials'] = params['credentials'] || base['credentials']; + init['redirect'] = params['redirect-mode'] || base['redirect']; + return init; +} + +self.addEventListener('fetch', function(event) { + var params = get_query_params(event.request.url); + var init = get_request_init(event.request, params); + var url = params['url']; + if (params['ignore']) { + return; + } + if (params['throw']) { + throw new Error('boom'); + } + if (params['reject']) { + event.respondWith(new Promise(function(resolve, reject) { + reject(); + })); + return; + } + if (params['resolve-null']) { + event.respondWith(new Promise(function(resolve) { + resolve(null); + })); + return; + } + if (params['generate-png']) { + var binary = atob( + 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAA' + + 'RnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAhSURBVDhPY3wro/Kf' + + 'gQLABKXJBqMGjBoAAqMGDLwBDAwAEsoCTFWunmQAAAAASUVORK5CYII='); + var array = new Uint8Array(binary.length); + for(var i = 0; i < binary.length; i++) { + array[i] = binary.charCodeAt(i); + }; + event.respondWith(new Response(new Blob([array], {type: 'image/png'}))); + return; + } + if (params['check-ua-header']) { + var ua = event.request.headers.get('User-Agent'); + if (ua) { + // We have a user agent! + event.respondWith(new Response(new Blob([ua]))); + } else { + // We don't have a user-agent! + event.respondWith(new Response(new Blob(["NO_UA"]))); + } + return; + } + if (params['check-accept-header']) { + var accept = event.request.headers.get('Accept'); + if (accept) { + event.respondWith(new Response(accept)); + } else { + event.respondWith(new Response('NO_ACCEPT')); + } + return; + } + event.respondWith(new Promise(function(resolve, reject) { + var request = event.request; + if (url) { + request = new Request(url, init); + } else if (params['change-request']) { + request = new Request(request, init); + } + const response_promise = params['navpreload'] ? event.preloadResponse + : fetch(request); + response_promise.then(function(response) { + var expectedType = params['expected_type']; + if (expectedType && response.type !== expectedType) { + // Resolve a JSON object with a failure instead of rejecting + // in order to distinguish this from a NetworkError, which + // may be expected even if the type is correct. + resolve(new Response(JSON.stringify({ + result: 'failure', + detail: 'got ' + response.type + ' Response.type instead of ' + + expectedType + }))); + } + + var expectedRedirected = params['expected_redirected']; + if (typeof expectedRedirected !== 'undefined') { + var expected_redirected = (expectedRedirected === 'true'); + if(response.redirected !== expected_redirected) { + // This is simply determining how to pass an error to the outer + // test case(fetch-request-redirect.https.html). + var execptedResolves = params['expected_resolves']; + if (execptedResolves === 'true') { + // Reject a JSON object with a failure since promise is expected + // to be resolved. + reject(new Response(JSON.stringify({ + result: 'failure', + detail: 'got '+ response.redirected + + ' Response.redirected instead of ' + + expectedRedirected + }))); + } else { + // Resolve a JSON object with a failure since promise is + // expected to be rejected. + resolve(new Response(JSON.stringify({ + result: 'failure', + detail: 'got '+ response.redirected + + ' Response.redirected instead of ' + + expectedRedirected + }))); + } + } + } + + if (params['clone']) { + response = response.clone(); + } + + // |cache| means to bounce responses through Cache Storage and back. + if (params['cache']) { + var cacheName = "cached-fetches-" + performance.now() + "-" + + event.request.url; + var cache; + var cachedResponse; + return self.caches.open(cacheName).then(function(opened) { + cache = opened; + return cache.put(request, response); + }).then(function() { + return cache.match(request); + }).then(function(cached) { + cachedResponse = cached; + return self.caches.delete(cacheName); + }).then(function() { + resolve(cachedResponse); + }); + } else { + resolve(response); + } + }, reject) + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers new file mode 100644 index 0000000000..123053b38c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript +Service-Worker-Allowed: / diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-variants-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-variants-worker.js new file mode 100644 index 0000000000..b950b9a18a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-variants-worker.js @@ -0,0 +1,35 @@ +importScripts('/common/get-host-info.sub.js'); +importScripts('test-helpers.sub.js'); +importScripts('/resources/testharness.js'); + +const storedResponse = new Response(new Blob(['a simple text file'])) +const absolultePath = `${base_path()}/simple.txt` + +self.addEventListener('fetch', event => { + const search = new URLSearchParams(new URL(event.request.url).search.substr(1)) + const variant = search.get('variant') + const delay = search.get('delay') + if (!variant) + return + + switch (variant) { + case 'forward': + event.respondWith(fetch(event.request.url)) + break + case 'redirect': + event.respondWith(fetch(`/xhr/resources/redirect.py?location=${base_path()}/simple.txt`)) + break + case 'delay-before-fetch': + event.respondWith( + new Promise(resolve => { + step_timeout(() => fetch(event.request.url).then(resolve), delay) + })) + break + case 'delay-after-fetch': + event.respondWith(new Promise(resolve => { + fetch(event.request.url) + .then(response => step_timeout(() => resolve(response), delay)) + })) + break + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js new file mode 100644 index 0000000000..92a96ff88f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js @@ -0,0 +1,31 @@ +var activatePromiseResolve; + +addEventListener('activate', function(evt) { + evt.waitUntil(new Promise(function(resolve) { + activatePromiseResolve = resolve; + })); +}); + +addEventListener('message', async function(evt) { + switch (evt.data) { + case 'CLAIM': + evt.waitUntil(new Promise(async resolve => { + await clients.claim(); + evt.source.postMessage('CLAIMED'); + resolve(); + })); + break; + case 'ACTIVATE': + if (typeof activatePromiseResolve !== 'function') { + throw new Error('Not activating!'); + } + activatePromiseResolve(); + break; + default: + throw new Error('Unknown message!'); + } +}); + +addEventListener('fetch', function(evt) { + evt.respondWith(new Response('Hello world')); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/form-poster.html b/testing/web-platform/tests/service-workers/service-worker/resources/form-poster.html new file mode 100644 index 0000000000..cd11a30a5e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/form-poster.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<meta name="referrer" content="origin"> +<form method="POST" id="form"></form> +<script> +function onLoad() { + const params = new URLSearchParams(self.location.search); + const form = document.getElementById('form'); + form.action = params.get('target'); + form.submit(); +} +self.addEventListener('load', onLoad); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/frame-for-getregistrations.html b/testing/web-platform/tests/service-workers/service-worker/resources/frame-for-getregistrations.html new file mode 100644 index 0000000000..7fc35f1891 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/frame-for-getregistrations.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<title>Service Worker: frame for getRegistrations()</title> +<script> +var scope = 'scope-for-getregistrations'; +var script = 'empty-worker.js'; +var registration; + +navigator.serviceWorker.register(script, { scope: scope }) + .then(function(r) { registration = r; window.parent.postMessage('ready', '*'); }) + +self.onmessage = function(e) { + if (e.data == 'unregister') { + registration.unregister() + .then(function() { + e.ports[0].postMessage('unregistered'); + }); + } +}; +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js new file mode 100644 index 0000000000..f0e6c7beca --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js @@ -0,0 +1,107 @@ +// This worker expects a fetch event for a navigation and messages back the +// result of clients.get(event.resultingClientId). + +// Resolves when the test finishes. +let testFinishPromise; +let resolveTestFinishPromise; +let rejectTestFinishPromise; + +// Resolves to clients.get(event.resultingClientId) from the fetch event. +let getPromise; +let resolveGetPromise; +let rejectGetPromise; + +let resultingClientId; + +function startTest() { + testFinishPromise = new Promise((resolve, reject) => { + resolveTestFinishPromise = resolve; + rejectTestFinishPromise = reject; + }); + + getPromise = new Promise((resolve, reject) => { + resolveGetPromise = resolve; + rejectGetPromise = reject; + }); +} + +async function describeGetPromiseResult(promise) { + const result = {}; + + await promise.then( + (client) => { + result.promiseState = 'fulfilled'; + if (client === undefined) { + result.promiseValue = 'undefinedValue'; + } else if (client instanceof Client) { + result.promiseValue = 'client'; + result.client = { + id: client.id, + url: client.url + }; + } else { + result.promiseValue = 'unknown'; + } + }, + (error) => { + result.promiseState = 'rejected'; + }); + + return result; +} + +async function handleGetResultingClient(event) { + // Note that this message can arrive before |resultingClientId| is populated. + const result = await describeGetPromiseResult(getPromise); + // |resultingClientId| must be populated by now. + result.queriedId = resultingClientId; + event.source.postMessage(result); +}; + +async function handleGetClient(event) { + const id = event.data.id; + const result = await describeGetPromiseResult(self.clients.get(id)); + result.queriedId = id; + event.source.postMessage(result); +}; + +self.addEventListener('message', (event) => { + if (event.data.command == 'startTest') { + startTest(); + event.waitUntil(testFinishPromise); + event.source.postMessage('ok'); + return; + } + + if (event.data.command == 'finishTest') { + resolveTestFinishPromise(); + event.source.postMessage('ok'); + return; + } + + if (event.data.command == 'getResultingClient') { + event.waitUntil(handleGetResultingClient(event)); + return; + } + + if (event.data.command == 'getClient') { + event.waitUntil(handleGetClient(event)); + return; + } +}); + +async function handleFetch(event) { + try { + resultingClientId = event.resultingClientId; + const client = await self.clients.get(resultingClientId); + resolveGetPromise(client); + } catch (error) { + rejectGetPromise(error); + } +} + +self.addEventListener('fetch', (event) => { + if (event.request.mode != 'navigate') + return; + event.waitUntil(handleFetch(event)); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html new file mode 100644 index 0000000000..bcab35364d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html @@ -0,0 +1,25 @@ +<!doctype html> +<title>register, unregister, and report result to opener</title> +<body> +<script> +'use strict'; + +if (!navigator.serviceWorker) { + window.opener.postMessage('FAIL: navigator.serviceWorker is undefined', '*'); +} else { + navigator.serviceWorker.register('empty-worker.js', {scope: 'scope-register'}) + .then( + registration => { + registration.unregister().then(() => { + window.opener.postMessage('OK', '*'); + }); + }, + error => { + window.opener.postMessage('FAIL: ' + error.name, '*'); + }) + .catch(error => { + window.opener.postMessage('ERROR: ' + error.name, '*'); + }); +} +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html b/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html new file mode 100644 index 0000000000..3a61d7bb89 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<meta charset="utf-8" /> +<script> + const url = new URL(new URLSearchParams(location.search.substr(1)).get('url'), location.href); + const before = performance.now(); + fetch(url) + .then(r => r.text()) + .then(() => + parent.postMessage({ + before, + after: performance.now(), + entry: performance.getEntriesByName(url)[0].toJSON() + })); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-image.html b/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-image.html new file mode 100644 index 0000000000..ce78840cb2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-image.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<img src="square"> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js b/testing/web-platform/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js new file mode 100644 index 0000000000..d8a94ad46b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js @@ -0,0 +1,19 @@ +function prototypeChain(global) { + let result = []; + while (global !== null) { + let thrown = false; + let next = Object.getPrototypeOf(global); + try { + Object.setPrototypeOf(global, {}); + result.push('mutable'); + } catch (e) { + result.push('immutable'); + } + global = next; + } + return result; +} + +self.onmessage = function(e) { + e.data.postMessage(prototypeChain(self)); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py b/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py new file mode 100644 index 0000000000..8f0b68e5a3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py @@ -0,0 +1,6 @@ +def main(request, response): + # This script generates a worker script for static imports from module + # service workers. + headers = [(b'Content-Type', b'text/javascript')] + body = b"import './echo-cookie-worker.py?key=%s'" % request.GET[b'key'] + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js new file mode 100644 index 0000000000..f5eac9508c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js @@ -0,0 +1 @@ +importScripts(`echo-cookie-worker.py${location.search}`); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-mime-type-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/import-mime-type-worker.py new file mode 100644 index 0000000000..b6e82f31d3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-mime-type-worker.py @@ -0,0 +1,10 @@ +def main(request, response): + if b'mime' in request.GET: + return ( + [(b'Content-Type', b'application/javascript')], + b"importScripts('./mime-type-worker.py?mime=%s');" % request.GET[b'mime'] + ) + return ( + [(b'Content-Type', b'application/javascript')], + b"importScripts('./mime-type-worker.py');" + ) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-relative.xsl b/testing/web-platform/tests/service-workers/service-worker/resources/import-relative.xsl new file mode 100644 index 0000000000..063a62d031 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-relative.xsl @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> + +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:import href="xslt-pass.xsl"/> +</xsl:stylesheet> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js new file mode 100644 index 0000000000..e9899d8e72 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js @@ -0,0 +1,8 @@ +// This worker imports a script that returns 200 on the first request and 404 +// on the second request, and a script that is updated every time when +// requesting it. +const params = new URLSearchParams(location.search); +const key = params.get('Key'); +const additional_key = params.get('AdditionalKey'); +importScripts(`update-worker.py?Key=${key}&Mode=not_found`, + `update-worker.py?Key=${additional_key}&Mode=normal`); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js new file mode 100644 index 0000000000..b569346035 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js @@ -0,0 +1,6 @@ +// This worker imports a script that returns 200 on the first request and 404 +// on the second request. The resulting body also changes each time it is +// requested. +const params = new URLSearchParams(location.search); +const key = params.get('Key'); +importScripts(`update-worker.py?Key=${key}&Mode=not_found`); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404.js new file mode 100644 index 0000000000..19c7a4b8e5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404.js @@ -0,0 +1 @@ +importScripts('404.py'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js new file mode 100644 index 0000000000..b432854db8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js @@ -0,0 +1 @@ +importScripts('https://{{domains[www1]}}:{{ports[https][0]}}/service-workers/service-worker/resources/import-scripts-version.py'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js new file mode 100644 index 0000000000..fdabdafc63 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js @@ -0,0 +1 @@ +importScripts('data:text/javascript,'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js new file mode 100644 index 0000000000..0fdcb0fcf8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js @@ -0,0 +1,10 @@ +importScripts('/resources/testharness.js'); + +let echo1 = null; +let echo2 = null; +let arg1 = 'import-scripts-get.py?output=echo1&msg=test1'; +let arg2 = 'import-scripts-get.py?output=echo2&msg=test2'; + +importScripts(arg1, arg2); +assert_equals(echo1, 'test1'); +assert_equals(echo2, 'test2'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-echo.py b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-echo.py new file mode 100644 index 0000000000..d38d660e65 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-echo.py @@ -0,0 +1,6 @@ +def main(req, res): + return ([ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')], + b'echo_output = "%s";\n' % req.GET[b'msg']) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-get.py b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-get.py new file mode 100644 index 0000000000..ab7b84e3e3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-get.py @@ -0,0 +1,6 @@ +def main(req, res): + return ([ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')], + b'%s = "%s";\n' % (req.GET[b'output'], req.GET[b'msg'])) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js new file mode 100644 index 0000000000..d4f1f3e26d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js @@ -0,0 +1,49 @@ +const badMimeTypes = [ + null, // no MIME type + 'text/plain', +]; + +const validMimeTypes = [ + 'application/ecmascript', + 'application/javascript', + 'application/x-ecmascript', + 'application/x-javascript', + 'text/ecmascript', + 'text/javascript', + 'text/javascript1.0', + 'text/javascript1.1', + 'text/javascript1.2', + 'text/javascript1.3', + 'text/javascript1.4', + 'text/javascript1.5', + 'text/jscript', + 'text/livescript', + 'text/x-ecmascript', + 'text/x-javascript', +]; + +function importScriptsWithMimeType(mimeType) { + importScripts(`./mime-type-worker.py${mimeType ? '?mime=' + mimeType : ''}`); +} + +importScripts('/resources/testharness.js'); + +for (const mimeType of badMimeTypes) { + test(() => { + assert_throws_dom( + 'NetworkError', + () => { importScriptsWithMimeType(mimeType); }, + `importScripts with ${mimeType ? 'bad' : 'no'} MIME type ${mimeType || ''} throws NetworkError`, + ); + }, `Importing script with ${mimeType ? 'bad' : 'no'} MIME type ${mimeType || ''}`); +} + +for (const mimeType of validMimeTypes) { + test(() => { + try { + importScriptsWithMimeType(mimeType); + } catch { + assert_unreached(`importScripts with MIME type ${mimeType} should not throw`); + } + }, `Importing script with valid JavaScript MIME type ${mimeType}`); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js new file mode 100644 index 0000000000..56c04f0946 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js @@ -0,0 +1 @@ +// empty script diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js new file mode 100644 index 0000000000..f612ab8e6a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js @@ -0,0 +1,7 @@ +// This worker imports a script that returns 200 on the first request and a +// redirect on the second request. The resulting body also changes each time it +// is requested. +const params = new URLSearchParams(location.search); +const key = params.get('Key'); +importScripts(`update-worker.py?Key=${key}&Mode=redirect&` + + `Redirect=update-worker.py?Key=${key}%26Mode=normal`); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js new file mode 100644 index 0000000000..d02a45349c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js @@ -0,0 +1 @@ +importScripts('redirect.py?Redirect=import-scripts-version.py'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js new file mode 100644 index 0000000000..b3b9bc46a0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js @@ -0,0 +1,15 @@ +importScripts('/resources/testharness.js'); + +let version = null; +importScripts('import-scripts-version.py'); +// Once imported, the stored script should be loaded for subsequent importScripts. +const expected_version = version; + +version = null; +importScripts('import-scripts-version.py'); +assert_equals(expected_version, version, 'second import'); + +version = null; +importScripts('import-scripts-version.py', 'import-scripts-version.py', + 'import-scripts-version.py'); +assert_equals(expected_version, version, 'multiple imports'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js new file mode 100644 index 0000000000..e01664662e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js @@ -0,0 +1,31 @@ +importScripts('/resources/testharness.js'); + +let echo_output = null; + +// Tests importing a script that sets |echo_output| to the query string. +function test_import(str) { + echo_output = null; + importScripts('import-scripts-echo.py?msg=' + str); + assert_equals(echo_output, str); +} + +test_import('root'); +test_import('root-and-message'); + +self.addEventListener('install', () => { + test_import('install'); + test_import('install-and-message'); + }); + +self.addEventListener('message', e => { + var error = null; + echo_output = null; + + try { + importScripts('import-scripts-echo.py?msg=' + e.data); + } catch (e) { + error = e && e.name; + } + + e.source.postMessage({ error: error, value: echo_output }); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-version.py b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-version.py new file mode 100644 index 0000000000..cde28544e6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-version.py @@ -0,0 +1,17 @@ +import datetime +import time + +epoch = datetime.datetime(1970, 1, 1) + +def main(req, res): + # Artificially delay response time in order to ensure uniqueness of + # computed value + time.sleep(0.1) + + now = (datetime.datetime.now() - epoch).total_seconds() + + return ([ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')], + u'version = "%s";\n' % now) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/imported-classic-script.js b/testing/web-platform/tests/service-workers/service-worker/resources/imported-classic-script.js new file mode 100644 index 0000000000..5fc5204051 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/imported-classic-script.js @@ -0,0 +1 @@ +const imported = 'A classic script.'; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/imported-module-script.js b/testing/web-platform/tests/service-workers/service-worker/resources/imported-module-script.js new file mode 100644 index 0000000000..56d196df04 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/imported-module-script.js @@ -0,0 +1 @@ +export const imported = 'A module script.'; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/indexeddb-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/indexeddb-worker.js new file mode 100644 index 0000000000..9add476838 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/indexeddb-worker.js @@ -0,0 +1,57 @@ +self.addEventListener('message', function(e) { + var message = e.data; + if (message.action === 'create') { + e.waitUntil(deleteDB() + .then(doIndexedDBTest) + .then(function() { + message.port.postMessage({ type: 'created' }); + }) + .catch(function(reason) { + message.port.postMessage({ type: 'error', value: reason }); + })); + } else if (message.action === 'cleanup') { + e.waitUntil(deleteDB() + .then(function() { + message.port.postMessage({ type: 'done' }); + }) + .catch(function(reason) { + message.port.postMessage({ type: 'error', value: reason }); + })); + } + }); + +function deleteDB() { + return new Promise(function(resolve, reject) { + var delete_request = indexedDB.deleteDatabase('db'); + + delete_request.onsuccess = resolve; + delete_request.onerror = reject; + }); +} + +function doIndexedDBTest(port) { + return new Promise(function(resolve, reject) { + var open_request = indexedDB.open('db'); + + open_request.onerror = reject; + open_request.onupgradeneeded = function() { + var db = open_request.result; + db.createObjectStore('store'); + }; + open_request.onsuccess = function() { + var db = open_request.result; + var tx = db.transaction('store', 'readwrite'); + var store = tx.objectStore('store'); + store.put('value', 'key'); + + tx.onerror = function() { + db.close(); + reject(tx.error); + }; + tx.oncomplete = function() { + db.close(); + resolve(); + }; + }; + }); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/install-event-type-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/install-event-type-worker.js new file mode 100644 index 0000000000..1c94ae21ea --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/install-event-type-worker.js @@ -0,0 +1,9 @@ +importScripts('worker-testharness.js'); + +self.oninstall = function(event) { + assert_true(event instanceof ExtendableEvent, 'instance of ExtendableEvent'); + assert_true(event instanceof InstallEvent, 'instance of InstallEvent'); + assert_equals(event.type, 'install', '`type` property value'); + assert_false(event.cancelable, '`cancelable` property value'); + assert_false(event.bubbles, '`bubbles` property value'); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/install-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/install-worker.html new file mode 100644 index 0000000000..ed20cd4dca --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/install-worker.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<body> +<p>Loading...</p> +<script> +async function install() { + let script; + for (const q of location.search.slice(1).split('&')) { + if (q.split('=')[0] === 'script') { + script = q.split('=')[1]; + } + } + const scope = location.href; + const reg = await navigator.serviceWorker.register(script, {scope}); + await navigator.serviceWorker.ready; + location.reload(); +} + +install(); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js b/testing/web-platform/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js new file mode 100644 index 0000000000..a3f239b654 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js @@ -0,0 +1,59 @@ +'use strict'; + +// This file checks additional interface requirements, on top of the basic IDL +// that is validated in service-workers/idlharness.any.js + +importScripts('/resources/testharness.js'); + +test(function() { + var req = new Request('http://{{host}}/', + {method: 'POST', + headers: [['Content-Type', 'Text/Html']]}); + assert_equals( + new ExtendableEvent('ExtendableEvent').type, + 'ExtendableEvent', 'Type of ExtendableEvent should be ExtendableEvent'); + assert_throws_js(TypeError, function() { + new FetchEvent('FetchEvent'); + }, 'FetchEvent constructor with one argument throws'); + assert_throws_js(TypeError, function() { + new FetchEvent('FetchEvent', {}); + }, 'FetchEvent constructor with empty init dict throws'); + assert_throws_js(TypeError, function() { + new FetchEvent('FetchEvent', {request: null}); + }, 'FetchEvent constructor with null request member throws'); + assert_equals( + new FetchEvent('FetchEvent', {request: req}).type, + 'FetchEvent', 'Type of FetchEvent should be FetchEvent'); + assert_equals( + new FetchEvent('FetchEvent', {request: req}).cancelable, + false, 'Default FetchEvent.cancelable should be false'); + assert_equals( + new FetchEvent('FetchEvent', {request: req}).bubbles, + false, 'Default FetchEvent.bubbles should be false'); + assert_equals( + new FetchEvent('FetchEvent', {request: req}).clientId, + '', 'Default FetchEvent.clientId should be the empty string'); + assert_equals( + new FetchEvent('FetchEvent', {request: req, cancelable: false}).cancelable, + false, 'FetchEvent.cancelable should be false'); + assert_equals( + new FetchEvent('FetchEvent', {request: req, clientId : 'test-client-id'}).clientId, 'test-client-id', + 'FetchEvent.clientId with option {clientId : "test-client-id"} should be "test-client-id"'); + assert_equals( + new FetchEvent('FetchEvent', {request : req}).request.url, + 'http://{{host}}/', + 'FetchEvent.request.url should return the value it was initialized to'); + assert_equals( + new FetchEvent('FetchEvent', {request : req}).isReload, + undefined, + 'FetchEvent.isReload should not exist'); + + }, 'Event constructors'); + +test(() => { + assert_false('XMLHttpRequest' in self); + }, 'xhr is not exposed'); + +test(() => { + assert_false('createObjectURL' in self.URL); + }, 'URL.createObjectURL is not exposed') diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html new file mode 100644 index 0000000000..04a9cb515e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html @@ -0,0 +1,28 @@ +<script src="test-helpers.sub.js"></script> +<script> + +function xhr_send(method, data) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + if (xhr.getResponseHeader('Content-Type') !== null) { + reject('Content-Type must be null.'); + } + resolve(); + }; + xhr.onerror = function() { + reject('XHR must succeed.'); + }; + xhr.responseType = 'text'; + xhr.open(method, './sample?test', true); + xhr.send(data); + }); +} + +window.addEventListener('message', function(evt) { + var port = evt.ports[0]; + xhr_send('POST', 'test string') + .then(function() { port.postMessage({results: 'finish'}); }) + .catch(function(e) { port.postMessage({results: 'failure:' + e}); }); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js new file mode 100644 index 0000000000..865dc30d42 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js @@ -0,0 +1,10 @@ +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample?test') == -1) { + return; + } + event.respondWith(new Promise(function(resolve) { + // null byte in blob type + resolve(new Response(new Blob([],{type: 'a\0b'}))); + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py new file mode 100644 index 0000000000..05977c6ab0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py @@ -0,0 +1,9 @@ +import time +def main(request, response): + response.headers.set(b"Content-Type", b"application/javascript") + response.headers.set(b"Transfer-encoding", b"chunked") + response.write_status_headers() + + time.sleep(1) + + response.writer.write(b"XX\r\n\r\n") diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py new file mode 100644 index 0000000000..a8edd06b8d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py @@ -0,0 +1,2 @@ +def main(request, response): + return [(b"Content-Type", b"application/javascript"), (b"Transfer-encoding", b"chunked")], b"XX\r\n\r\n" diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html new file mode 100644 index 0000000000..8f0e6baca1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html @@ -0,0 +1,25 @@ +<script src="test-helpers.sub.js"></script> +<script> + +function xhr_send(method, data) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + reject('XHR must fail.'); + }; + xhr.onerror = function() { + resolve(); + }; + xhr.responseType = 'text'; + xhr.open(method, './sample?test', true); + xhr.send(data); + }); +} + +window.addEventListener('message', function(evt) { + var port = evt.ports[0]; + xhr_send('POST', 'test string') + .then(function() { port.postMessage({results: 'finish'}); }) + .catch(function(e) { port.postMessage({results: 'failure:' + e}); }); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-worker.js new file mode 100644 index 0000000000..850874b811 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample?test') == -1) { + return; + } + event.respondWith(new Promise(function(resolve) { + var headers = new Headers; + headers.append('foo', 'foo'); + headers.append('foo', 'b\0r'); // header value with a null byte + resolve(new Response('hello world', {'headers': headers})); + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html new file mode 100644 index 0000000000..cf2fa8d14f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html @@ -0,0 +1,23 @@ +<script> +function xhr_send(method, data) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + resolve(); + }; + xhr.onerror = function() { + reject('XHR must succeed.'); + }; + xhr.responseType = 'text'; + xhr.open(method, './sample?test', true); + xhr.send(data); + }); +} + +window.addEventListener('message', function(evt) { + var port = evt.ports[0]; + xhr_send('POST', 'test string') + .then(function() { port.postMessage({results: 'finish'}); }) + .catch(function(e) { port.postMessage({results: 'failure:' + e}); }); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js new file mode 100644 index 0000000000..d9ecca277b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample?test') == -1) { + return; + } + + event.respondWith(new Promise(function(resolve) { + var headers = new Headers; + headers.append('TEST', 'ßÀ¿'); // header value holds the Latin1 (ISO8859-1) string. + resolve(new Response('hello world', {'headers': headers})); + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/load_worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/load_worker.js new file mode 100644 index 0000000000..18c673bebc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/load_worker.js @@ -0,0 +1,29 @@ +function run_test(data, sender) { + if (data === 'xhr') { + const xhr = new XMLHttpRequest(); + xhr.open('GET', 'synthesized-response.txt', true); + xhr.responseType = 'text'; + xhr.send(); + xhr.onload = evt => sender.postMessage(xhr.responseText); + xhr.onerror = () => sender.postMessage('XHR failed!'); + } else if (data === 'fetch') { + fetch('synthesized-response.txt') + .then(response => response.text()) + .then(data => sender.postMessage(data)) + .catch(error => sender.postMessage('Fetch failed!')); + } else if (data === 'importScripts') { + importScripts('synthesized-response.js'); + // |message| is provided by 'synthesized-response.js'; + sender.postMessage(message); + } else { + sender.postMessage('Unexpected message! ' + data); + } +} + +// Entry point for dedicated workers. +self.onmessage = evt => run_test(evt.data, self); + +// Entry point for shared workers. +self.onconnect = evt => { + evt.ports[0].onmessage = e => run_test(e.data, evt.ports[0]); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/loaded.html b/testing/web-platform/tests/service-workers/service-worker/resources/loaded.html new file mode 100644 index 0000000000..0cabce69f8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/loaded.html @@ -0,0 +1,9 @@ +<script> +addEventListener('load', function() { + opener.postMessage({ type: 'LOADED' }, '*'); +}); + +addEventListener('pageshow', function() { + opener.postMessage({ type: 'PAGESHOW' }, '*'); +}); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html new file mode 100644 index 0000000000..b1e554d220 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html @@ -0,0 +1,130 @@ +<!DOCTYPE html> +<html> +<script> + +const fetchURL = new URL('sample.js', window.location).href; + +const frameControllerText = +`<script> + let t = null; + try { + if (navigator.serviceWorker.controller) { + t = navigator.serviceWorker.controller.scriptURL; + } + } catch (e) { + t = e.message; + } finally { + parent.postMessage({ data: t }, '*'); + } +</` + `script>`; + +const frameFetchText = +`<script> + fetch('${fetchURL}', { mode: 'no-cors' }).then(response => { + return response.text(); + }).then(text => { + parent.postMessage({ data: text }, '*'); + }).catch(e => { + parent.postMessage({ data: e.message }, '*'); + }); +</` + `script>`; + +const workerControllerText = +`let t = navigator.serviceWorker.controller + ? navigator.serviceWorker.controller.scriptURL + : null; +self.postMessage(t);`; + +const workerFetchText = +`fetch('${fetchURL}', { mode: 'no-cors' }).then(response => { + return response.text(); +}).then(text => { + self.postMessage(text); +}).catch(e => { + self.postMessage(e.message); +});` + +function getChildText(opts) { + if (opts.child === 'iframe') { + if (opts.check === 'controller') { + return frameControllerText; + } + + if (opts.check === 'fetch') { + return frameFetchText; + } + + throw('unexpected feature to check: ' + opts.check); + } + + if (opts.child === 'worker') { + if (opts.check === 'controller') { + return workerControllerText; + } + + if (opts.check === 'fetch') { + return workerFetchText; + } + + throw('unexpected feature to check: ' + opts.check); + } + + throw('unexpected child type ' + opts.child); +} + +function makeURL(opts) { + let mimetype = opts.child === 'iframe' ? 'text/html' + : 'text/javascript'; + + if (opts.scheme === 'blob') { + let blob = new Blob([getChildText(opts)], { type: mimetype }); + return URL.createObjectURL(blob); + } + + if (opts.scheme === 'data') { + return `data:${mimetype},${getChildText(opts)}`; + } + + throw(`unexpected URL scheme ${opts.scheme}`); +} + +function testWorkerChild(url) { + let w = new Worker(url); + return new Promise((resolve, reject) => { + w.onmessage = resolve; + w.onerror = evt => { + reject(evt.message); + } + }); +} + +function testIframeChild(url) { + let frame = document.createElement('iframe'); + frame.src = url; + document.body.appendChild(frame); + + return new Promise(resolve => { + addEventListener('message', evt => { + resolve(evt.data); + }, { once: true }); + }); +} + +function testURL(opts, url) { + if (opts.child === 'worker') { + return testWorkerChild(url); + } + + if (opts.child === 'iframe') { + return testIframeChild(url); + } + + throw(`unexpected child type ${opts.child}`); +} + +function checkChildController(opts) { + let url = makeURL(opts); + return testURL(opts, url); +} +</script> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js new file mode 100644 index 0000000000..4b7aad0f58 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js @@ -0,0 +1,5 @@ +addEventListener('fetch', evt => { + if (evt.request.url.includes('sample')) { + evt.respondWith(new Response('intercepted')); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/location-setter.html b/testing/web-platform/tests/service-workers/service-worker/resources/location-setter.html new file mode 100644 index 0000000000..f0ced06ec2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/location-setter.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<meta name="referrer" content="origin"> +<script> +function onLoad() { + const params = new URLSearchParams(self.location.search); + self.location = params.get('target'); +} +self.addEventListener('load', onLoad); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/malformed-http-response.asis b/testing/web-platform/tests/service-workers/service-worker/resources/malformed-http-response.asis new file mode 100644 index 0000000000..bc3c68d46d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/malformed-http-response.asis @@ -0,0 +1 @@ +HAHAHA THIS IS NOT HTTP AND THE BROWSER SHOULD CONSIDER IT A NETWORK ERROR diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/malformed-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/malformed-worker.py new file mode 100644 index 0000000000..319b6e277b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/malformed-worker.py @@ -0,0 +1,14 @@ +def main(request, response): + headers = [(b"Content-Type", b"application/javascript")] + + body = {u'parse-error': u'var foo = function() {;', + u'undefined-error': u'foo.bar = 42;', + u'uncaught-exception': u'throw new DOMException("AbortError");', + u'caught-exception': u'try { throw new Error; } catch(e) {}', + u'import-malformed-script': u'importScripts("malformed-worker.py?parse-error");', + u'import-no-such-script': u'importScripts("no-such-script.js");', + u'top-level-await': u'await Promise.resolve(1);', + u'instantiation-error': u'import nonexistent from "./imported-module-script.js";', + u'instantiation-error-and-top-level-await': u'import nonexistent from "./imported-module-script.js"; await Promise.resolve(1);'}[request.url_parts.query] + + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/message-vs-microtask.html b/testing/web-platform/tests/service-workers/service-worker/resources/message-vs-microtask.html new file mode 100644 index 0000000000..2c45c59a47 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/message-vs-microtask.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<script> + let draft = []; + var resolve_manual_promise; + let manual_promise = + new Promise(resolve => resolve_manual_promise = resolve).then(() => draft.push('microtask')); + + let resolve_message_promise; + let message_promise = new Promise(resolve => resolve_message_promise = resolve); + function handle_message(event) { + draft.push('message'); + resolve_message_promise(); + } + + var result = Promise.all([manual_promise, message_promise]).then(() => draft); +</script> + +<script src="empty.js?key=start"></script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/mime-sniffing-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/mime-sniffing-worker.js new file mode 100644 index 0000000000..5c34a7a49e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/mime-sniffing-worker.js @@ -0,0 +1,9 @@ +self.addEventListener('fetch', function(event) { + // Use an empty content-type value to force mime-sniffing. Note, this + // must be passed to the constructor since the mime-type of the Response + // is fixed and cannot be later changed. + var res = new Response('<!DOCTYPE html>\n<h1 id=\'testid\'>test</h1>', { + headers: { 'content-type': '' } + }); + event.respondWith(res); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/mime-type-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/mime-type-worker.py new file mode 100644 index 0000000000..92a602e634 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/mime-type-worker.py @@ -0,0 +1,4 @@ +def main(request, response): + if b'mime' in request.GET: + return [(b'Content-Type', request.GET[b'mime'])], b"" + return [], b"" diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/mint-new-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/mint-new-worker.py new file mode 100644 index 0000000000..ebee4ff8e8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/mint-new-worker.py @@ -0,0 +1,27 @@ +import random + +import time + +body = u''' +onactivate = (e) => e.waitUntil(clients.claim()); +var resolve_wait_until; +var wait_until = new Promise(resolve => { + resolve_wait_until = resolve; + }); +onmessage = (e) => { + if (e.data == 'wait') + e.waitUntil(wait_until); + if (e.data == 'go') + resolve_wait_until(); + };''' + +def main(request, response): + headers = [(b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')] + + skipWaiting = u'' + if b'skip-waiting' in request.GET: + skipWaiting = u'skipWaiting();' + + return headers, u'/* %s %s */ %s %s' % (time.time(), random.random(), skipWaiting, body) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/module-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/module-worker.js new file mode 100644 index 0000000000..385fe71015 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/module-worker.js @@ -0,0 +1 @@ +import * as module from './imported-module-script.js'; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-iframe.html new file mode 100644 index 0000000000..c59b95594f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-iframe.html @@ -0,0 +1,19 @@ +<script> +function load_multipart_image(src) { + return new Promise((resolve, reject) => { + const img = document.createElement('img'); + img.addEventListener('load', () => resolve(img)); + img.addEventListener('error', (e) => reject(new DOMException('load failed', 'NetworkError'))); + img.src = src; + }); +} + +function get_image_data(img) { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + context.drawImage(img, 0, 0); + // When |img.src| is cross origin, this should throw a SecurityError. + const imageData = context.getImageData(0, 0, 1, 1); + return imageData; +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-worker.js new file mode 100644 index 0000000000..a38fe54d34 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-worker.js @@ -0,0 +1,21 @@ +importScripts('/common/get-host-info.sub.js'); +importScripts('test-helpers.sub.js'); + +const host_info = get_host_info(); + +const multipart_image_path = base_path() + 'multipart-image.py'; +const sameorigin_url = host_info['HTTPS_ORIGIN'] + multipart_image_path; +const cross_origin_url = host_info['HTTPS_REMOTE_ORIGIN'] + multipart_image_path; + +self.addEventListener('fetch', event => { + const url = event.request.url; + if (url.indexOf('cross-origin-multipart-image-with-no-cors') >= 0) { + event.respondWith(fetch(cross_origin_url, {mode: 'no-cors'})); + } else if (url.indexOf('cross-origin-multipart-image-with-cors-rejected') >= 0) { + event.respondWith(fetch(cross_origin_url, {mode: 'cors'})); + } else if (url.indexOf('cross-origin-multipart-image-with-cors-approved') >= 0) { + event.respondWith(fetch(cross_origin_url + '?approvecors', {mode: 'cors'})); + } else if (url.indexOf('same-origin-multipart-image') >= 0) { + event.respondWith(fetch(sameorigin_url)); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image.py b/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image.py new file mode 100644 index 0000000000..9a3c035f49 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image.py @@ -0,0 +1,23 @@ +# A request handler that serves a multipart image. + +import os + + +BOUNDARY = b'cutHere' + + +def create_part(path): + with open(path, u'rb') as f: + return b'Content-Type: image/png\r\n\r\n' + f.read() + b'--%s' % BOUNDARY + + +def main(request, response): + content_type = b'multipart/x-mixed-replace; boundary=%s' % BOUNDARY + headers = [(b'Content-Type', content_type)] + if b'approvecors' in request.GET: + headers.append((b'Access-Control-Allow-Origin', b'*')) + + image_path = os.path.join(request.doc_root, u'images') + body = create_part(os.path.join(image_path, u'red.png')) + body = body + create_part(os.path.join(image_path, u'red-16x16.png')) + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigate-window-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/navigate-window-worker.js new file mode 100644 index 0000000000..f9617439fc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigate-window-worker.js @@ -0,0 +1,21 @@ +addEventListener('message', function(evt) { + if (evt.data.type === 'GET_CLIENTS') { + clients.matchAll(evt.data.opts).then(function(clientList) { + var resultList = clientList.map(function(c) { + return { url: c.url, frameType: c.frameType, id: c.id }; + }); + evt.source.postMessage({ type: 'success', detail: resultList }); + }).catch(function(err) { + evt.source.postMessage({ + type: 'failure', + detail: 'matchAll() rejected with "' + err + '"' + }); + }); + return; + } + + evt.source.postMessage({ + type: 'failure', + detail: 'Unexpected message type "' + evt.data.type + '"' + }); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-headers-server.py b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-headers-server.py new file mode 100644 index 0000000000..5b2e044f8b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-headers-server.py @@ -0,0 +1,19 @@ +def main(request, response): + response.status = (200, b"OK") + response.headers.set(b"Content-Type", b"text/html") + return b""" + <script> + self.addEventListener('load', evt => { + self.parent.postMessage({ + origin: '%s', + referer: '%s', + 'sec-fetch-site': '%s', + 'sec-fetch-mode': '%s', + 'sec-fetch-dest': '%s', + }); + }); + </script>""" % (request.headers.get( + b"origin", b"not set"), request.headers.get(b"referer", b"not set"), + request.headers.get(b"sec-fetch-site", b"not set"), + request.headers.get(b"sec-fetch-mode", b"not set"), + request.headers.get(b"sec-fetch-dest", b"not set")) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js new file mode 100644 index 0000000000..39f11baf8c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js @@ -0,0 +1,11 @@ +self.addEventListener('fetch', function(event) { + event.respondWith( + fetch(event.request) + .then( + function(response) { + return response; + }, + function(error) { + return new Response('Error:' + error); + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body.py b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body.py new file mode 100644 index 0000000000..d10329e783 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body.py @@ -0,0 +1,11 @@ +import os + +from wptserve.utils import isomorphic_encode + +filename = os.path.basename(isomorphic_encode(__file__)) + +def main(request, response): + if request.method == u'POST': + return 302, [(b'Location', b'./%s?redirect' % filename)], b'' + + return [(b'Content-Type', b'text/plain')], request.request_path diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html new file mode 100644 index 0000000000..d82571d1a3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js"></script> +<script> +var host_info = get_host_info(); +var SCOPE = 'navigation-redirect-scope1.py'; +var SCRIPT = 'redirect-worker.js'; + +var registration; +var worker; +var wait_for_worker_promise = navigator.serviceWorker.getRegistration(SCOPE) + .then(function(reg) { + if (reg) + return reg.unregister(); + }) + .then(function() { + return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE}); + }) + .then(function(reg) { + registration = reg; + worker = reg.installing; + return new Promise(function(resolve) { + worker.addEventListener('statechange', function() { + if (worker.state == 'activated') + resolve(); + }); + }); + }); + +function send_result(message_id, result) { + window.parent.postMessage( + {id: message_id, result: result}, + host_info['HTTPS_ORIGIN']); +} + +function get_request_infos(worker) { + return new Promise(function(resolve) { + var channel = new MessageChannel(); + channel.port1.onmessage = (msg) => { + resolve(msg.data.requestInfos); + }; + worker.postMessage({command: 'getRequestInfos', port: channel.port2}, + [channel.port2]); + }); +} + +function get_clients(worker, actual_ids) { + return new Promise(function(resolve) { + var channel = new MessageChannel(); + channel.port1.onmessage = (msg) => { + resolve(msg.data.clients); + }; + worker.postMessage({ + command: 'getClients', + actual_ids, + port: channel.port2 + }, [channel.port2]); + }); +} + +window.addEventListener('message', on_message, false); + +function on_message(e) { + if (e.origin != host_info['HTTPS_ORIGIN']) { + console.error('invalid origin: ' + e.origin); + return; + } + const command = e.data.message.command; + if (command == 'wait_for_worker') { + wait_for_worker_promise.then(function() { send_result(e.data.id, 'ok'); }); + } else if (command == 'get_request_infos') { + get_request_infos(worker) + .then(function(data) { + send_result(e.data.id, data); + }); + } else if (command == 'get_clients') { + get_clients(worker, e.data.message.actual_ids) + .then(function(data) { + send_result(e.data.id, data); + }); + } else if (command == 'unregister') { + registration.unregister() + .then(function() { + send_result(e.data.id, 'ok'); + }); + } +} + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py new file mode 100644 index 0000000000..9b90b14695 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py @@ -0,0 +1,22 @@ +def main(request, response): + if b"url" in request.GET: + headers = [(b"Location", request.GET[b"url"])] + return 302, headers, b'' + + status = 200 + + if b"noLocationRedirect" in request.GET: + status = 302 + + return status, [(b"content-type", b"text/html")], b''' +<!DOCTYPE html> +<script> +onmessage = event => { + window.parent.postMessage( + { + id: event.data.id, + result: location.href + }, '*'); +}; +</script> +''' diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py new file mode 100644 index 0000000000..9b90b14695 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py @@ -0,0 +1,22 @@ +def main(request, response): + if b"url" in request.GET: + headers = [(b"Location", request.GET[b"url"])] + return 302, headers, b'' + + status = 200 + + if b"noLocationRedirect" in request.GET: + status = 302 + + return status, [(b"content-type", b"text/html")], b''' +<!DOCTYPE html> +<script> +onmessage = event => { + window.parent.postMessage( + { + id: event.data.id, + result: location.href + }, '*'); +}; +</script> +''' diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py new file mode 100644 index 0000000000..9b90b14695 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py @@ -0,0 +1,22 @@ +def main(request, response): + if b"url" in request.GET: + headers = [(b"Location", request.GET[b"url"])] + return 302, headers, b'' + + status = 200 + + if b"noLocationRedirect" in request.GET: + status = 302 + + return status, [(b"content-type", b"text/html")], b''' +<!DOCTYPE html> +<script> +onmessage = event => { + window.parent.postMessage( + { + id: event.data.id, + result: location.href + }, '*'); +}; +</script> +''' diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html new file mode 100644 index 0000000000..40e27c630d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js"></script> +<script> +var SCOPE = './redirect.py?Redirect=' + encodeURI('http://example.com'); +var SCRIPT = 'navigation-redirect-to-http-worker.js'; +var host_info = get_host_info(); + +navigator.serviceWorker.getRegistration(SCOPE) + .then(function(registration) { + if (registration) + return registration.unregister(); + }) + .then(function() { + return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE}); + }) + .then(function(registration) { + return new Promise(function(resolve) { + registration.addEventListener('updatefound', function() { + resolve(registration.installing); + }); + }); + }) + .then(function(worker) { + worker.addEventListener('statechange', on_state_change); + }) + .catch(function(reason) { + window.parent.postMessage({results: 'FAILURE: ' + reason.message}, + host_info['HTTPS_ORIGIN']); + }); + +function on_state_change(event) { + if (event.target.state != 'activated') + return; + with_iframe(SCOPE, {auto_remove: false}) + .then(function(frame) { + window.parent.postMessage( + {results: frame.contentDocument.body.textContent}, + host_info['HTTPS_ORIGIN']); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js new file mode 100644 index 0000000000..6f2a8ae1d7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js @@ -0,0 +1,22 @@ +importScripts('/resources/testharness.js'); + +self.addEventListener('fetch', function(event) { + event.respondWith(new Promise(function(resolve) { + Promise.resolve() + .then(function() { + assert_equals( + event.request.redirect, 'manual', + 'The redirect mode of navigation request must be manual.'); + return fetch(event.request); + }) + .then(function(response) { + assert_equals( + response.type, 'opaqueredirect', + 'The response type of 302 response must be opaqueredirect.'); + resolve(new Response('OK')); + }) + .catch(function(error) { + resolve(new Response('Failed in SW: ' + error)); + }); + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js new file mode 100644 index 0000000000..79c54088ff --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js @@ -0,0 +1,22 @@ +importScripts("/resources/testharness.js"); +const timings = {} + +const DELAY_ACTIVATION = 500 + +self.addEventListener('activate', event => { + event.waitUntil(new Promise(resolve => { + timings.activateWorkerStart = performance.now() + performance.timeOrigin; + + // This gives us enough time to ensure activation would delay fetch handling + step_timeout(resolve, DELAY_ACTIVATION); + }).then(() => timings.activateWorkerEnd = performance.now() + performance.timeOrigin)); +}) + +self.addEventListener('fetch', event => { + timings.handleFetchEvent = performance.now() + performance.timeOrigin; + event.respondWith(Promise.resolve(new Response(new Blob([` + <script> + parent.postMessage(${JSON.stringify(timings)}, "*") + </script> + `])))); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker.js new file mode 100644 index 0000000000..8539b40066 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker.js @@ -0,0 +1,15 @@ +self.addEventListener('fetch', (event) => { + const url = event.request.url; + + // Network fallback. + if (url.indexOf('network-fallback') >= 0) { + return; + } + + // Don't intercept redirect. + if (url.indexOf('redirect.py') >= 0) { + return; + } + + event.respondWith(fetch(url)); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html new file mode 100644 index 0000000000..fc048e288e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html @@ -0,0 +1,16 @@ +<!doctype html> +<script> +const baseLocation = window.location; +const workerUrl = new URL('create-blob-url-worker.js', baseLocation).href; +const worker = new Worker(workerUrl); + +function fetch_in_worker(url) { + const resourceUrl = new URL(url, baseLocation).href; + return new Promise((resolve) => { + worker.onmessage = (event) => { + resolve(event.data); + }; + worker.postMessage(resourceUrl); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-workers.html b/testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-workers.html new file mode 100644 index 0000000000..f0eafcd3e0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-workers.html @@ -0,0 +1,38 @@ +<!doctype html> +<script> +const baseLocation = window.location; +const parentWorkerScript = ` + const childWorkerScript = 'self.onmessage = async (e) => {' + + ' const response = await fetch(e.data);' + + ' const text = await response.text();' + + ' self.postMessage(text);' + + '};'; + const blob = new Blob([childWorkerScript], { type: 'text/javascript' }); + const blobUrl = URL.createObjectURL(blob); + const childWorker = new Worker(blobUrl); + + // When a message comes from the parent frame, sends a resource url to the + // child worker. + self.onmessage = (e) => { + childWorker.postMessage(e.data); + }; + // When a message comes from the child worker, sends a content of fetch() to + // the parent frame. + childWorker.onmessage = (e) => { + self.postMessage(e.data); + }; +`; +const blob = new Blob([parentWorkerScript], { type: 'text/javascript' }); +const blobUrl = URL.createObjectURL(blob); +const worker = new Worker(blobUrl); + +function fetch_in_worker(url) { + const resourceUrl = new URL(url, baseLocation).href; + return new Promise((resolve) => { + worker.onmessage = (event) => { + resolve(event.data); + }; + worker.postMessage(resourceUrl); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/nested-iframe-parent.html b/testing/web-platform/tests/service-workers/service-worker/resources/nested-iframe-parent.html new file mode 100644 index 0000000000..115ab26e12 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/nested-iframe-parent.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.onmessage = event => parent.postMessage(event.data, '*', event.ports); +</script> +<iframe id='child'></iframe> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/nested-parent.html b/testing/web-platform/tests/service-workers/service-worker/resources/nested-parent.html new file mode 100644 index 0000000000..b4832d461d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/nested-parent.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<meta name="referrer" content="origin"> +<script> +async function onLoad() { + self.addEventListener('message', evt => { + if (self.opener) + self.opener.postMessage(evt.data, '*'); + else + self.top.postMessage(evt.data, '*'); + }, { once: true }); + const params = new URLSearchParams(self.location.search); + const frame = document.createElement('iframe'); + frame.src = params.get('target'); + document.body.appendChild(frame); +} +self.addEventListener('load', onLoad); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html new file mode 100644 index 0000000000..3fad2c9228 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html @@ -0,0 +1,33 @@ +<!doctype html> +<script> +const baseLocation = window.location; +const parentWorkerScript = ` + const workerUrl = + new URL('postmessage-fetched-text.js', '${baseLocation}').href; + const childWorker = new Worker(workerUrl); + + // When a message comes from the parent frame, sends a resource url to the + // child worker. + self.onmessage = (e) => { + childWorker.postMessage(e.data); + }; + // When a message comes from the child worker, sends a content of fetch() to + // the parent frame. + childWorker.onmessage = (e) => { + self.postMessage(e.data); + }; +`; +const blob = new Blob([parentWorkerScript], { type: 'text/javascript' }); +const blobUrl = URL.createObjectURL(blob); +const worker = new Worker(blobUrl); + +function fetch_in_worker(url) { + const resourceUrl = new URL(url, baseLocation).href; + return new Promise((resolve) => { + worker.onmessage = (event) => { + resolve(event.data); + }; + worker.postMessage(resourceUrl); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/nested_load_worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/nested_load_worker.js new file mode 100644 index 0000000000..ef0ed8fc70 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/nested_load_worker.js @@ -0,0 +1,23 @@ +// Entry point for dedicated workers. +self.onmessage = evt => { + try { + const worker = new Worker('load_worker.js'); + worker.onmessage = evt => self.postMessage(evt.data); + worker.postMessage(evt.data); + } catch (err) { + self.postMessage('Unexpected error! ' + err.message); + } +}; + +// Entry point for shared workers. +self.onconnect = evt => { + evt.ports[0].onmessage = e => { + try { + const worker = new Worker('load_worker.js'); + worker.onmessage = e => evt.ports[0].postMessage(e.data); + worker.postMessage(evt.data); + } catch (err) { + evt.ports[0].postMessage('Unexpected error! ' + err.message); + } + }; +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/no-dynamic-import.js b/testing/web-platform/tests/service-workers/service-worker/resources/no-dynamic-import.js new file mode 100644 index 0000000000..ecedd6c5d7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/no-dynamic-import.js @@ -0,0 +1,18 @@ +/** @type {[name: string, url: string][]} */ +const importUrlTests = [ + ["Module URL", "./basic-module.js"], + // In no-dynamic-import-in-module.any.js, this module is also statically imported + ["Another module URL", "./basic-module-2.js"], + [ + "Module data: URL", + "data:text/javascript;charset=utf-8," + + encodeURIComponent(`export default 'hello!';`), + ], +]; + +for (const [name, url] of importUrlTests) { + promise_test( + (t) => promise_rejects_js(t, TypeError, import(url), "Import must reject"), + name + ); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/notification_icon.py b/testing/web-platform/tests/service-workers/service-worker/resources/notification_icon.py new file mode 100644 index 0000000000..71f5a9d488 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/notification_icon.py @@ -0,0 +1,11 @@ +from urllib.parse import parse_qs + +from wptserve.utils import isomorphic_encode + +def main(req, res): + qs_cookie_val = parse_qs(req.url_parts.query).get(u'set-cookie-notification') + + if qs_cookie_val: + res.set_cookie(b'notification', isomorphic_encode(qs_cookie_val[0])) + + return b'not really an icon' diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html new file mode 100644 index 0000000000..5a20a58ab1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>iframe for embed-and-object-are-not-intercepted test</title> +<body> +<object type="image/png" data="/images/green.png"></embed> +<script> +// Our parent (the root frame of the test) will examine this to get the result. +var test_promise = new Promise(resolve => { + if (!navigator.serviceWorker.controller) + resolve('FAIL: this iframe is not controlled'); + + const elem = document.querySelector('object'); + elem.addEventListener('load', e => { + resolve('request was not intercepted'); + }); + elem.addEventListener('error', e => { + resolve('FAIL: request was intercepted'); + }); + }); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html new file mode 100644 index 0000000000..0aeb81951e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>iframe for embed-and-object-are-not-intercepted test</title> +<body> +<script> +// The OBJECT element will call this with the result about whether the OBJECT +// request was intercepted by the service worker. +var report_result; + +// Our parent (the root frame of the test) will examine this to get the result. +var test_promise = new Promise(resolve => { + report_result = resolve; + }); +</script> + +<object data="embedded-content-from-server.html"></object> +</body> + diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html new file mode 100644 index 0000000000..5c8ab79a50 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>iframe for embed-and-object-are-not-intercepted test</title> +<body> +<script> +// The OBJECT element will call this with the result about whether the OBJECT +// request was intercepted by the service worker. +var report_result; + +// Our parent (the root frame of the test) will examine this to get the result. +var test_promise = new Promise(resolve => { + report_result = resolve; + }); + +let el = document.createElement('object'); +el.data = "/common/blank.html"; +el.addEventListener('load', _ => { + window[0].location = "/service-workers/service-worker/resources/embedded-content-from-server.html"; +}, { once: true }); +document.body.appendChild(el); +</script> + +</body> + diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js new file mode 100644 index 0000000000..7c97014fd0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js @@ -0,0 +1,13 @@ +var max_nesting_level = 8; + +self.addEventListener('message', function(event) { + var level = event.data; + if (level < max_nesting_level) + dispatchEvent(new MessageEvent('message', { data: level + 1 })); + throw Error('error at level ' + level); + }); + +self.addEventListener('activate', function(event) { + dispatchEvent(new MessageEvent('message', { data: 1 })); + }); + diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js new file mode 100644 index 0000000000..0bd9d318b2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js @@ -0,0 +1,3 @@ +self.onerror = function(event) { return true; }; + +self.addEventListener('activate', function(event) { throw new Error(); }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js new file mode 100644 index 0000000000..d56c951139 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js @@ -0,0 +1,7 @@ +// Ensure we can handle multiple error handlers. One error handler +// calling preventDefault should cause the event to be treated as +// handled. +self.addEventListener('error', function(event) {}); +self.addEventListener('error', function(event) { event.preventDefault(); }); +self.addEventListener('error', function(event) {}); +self.addEventListener('activate', function(event) { throw new Error(); }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js new file mode 100644 index 0000000000..eb12ae862c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js @@ -0,0 +1,2 @@ +self.addEventListener('error', function(event) {}); +self.addEventListener('activate', function(event) { throw new Error(); }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js new file mode 100644 index 0000000000..1e88ac5c4e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js @@ -0,0 +1,7 @@ +// Ensure we can handle multiple activate handlers. One handler throwing an +// error should cause the event dispatch to be treated as having unhandled +// errors. +self.addEventListener('activate', function(event) {}); +self.addEventListener('activate', function(event) {}); +self.addEventListener('activate', function(event) { throw new Error(); }); +self.addEventListener('activate', function(event) {}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js new file mode 100644 index 0000000000..65b02b12b3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js @@ -0,0 +1,8 @@ +'use strict'; + +self.addEventListener('activate', event => { + event.waitUntil(new Promise(() => { + // Use a promise that never resolves to prevent this service worker from + // advancing past the 'activating' state. + })); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js b/testing/web-platform/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js new file mode 100644 index 0000000000..b905d55598 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js @@ -0,0 +1,10 @@ +'use strict'; + +self.addEventListener('fetch', event => { + if (event.request.url.endsWith('waituntil-forever')) { + event.respondWith(new Promise(() => { + // Use a promise that never resolves to prevent this fetch from + // completing. + })); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js new file mode 100644 index 0000000000..6729ab61a3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js @@ -0,0 +1,12 @@ +var max_nesting_level = 8; + +self.addEventListener('message', function(event) { + var level = event.data; + if (level < max_nesting_level) + dispatchEvent(new MessageEvent('message', { data: level + 1 })); + throw Error('error at level ' + level); + }); + +self.addEventListener('install', function(event) { + dispatchEvent(new MessageEvent('message', { data: 1 })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js new file mode 100644 index 0000000000..c2c499ab1a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js @@ -0,0 +1,3 @@ +self.onerror = function(event) { return true; }; + +self.addEventListener('install', function(event) { throw new Error(); }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js new file mode 100644 index 0000000000..7667c2781d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js @@ -0,0 +1,7 @@ +// Ensure we can handle multiple error handlers. One error handler +// calling preventDefault should cause the event to be treated as +// handled. +self.addEventListener('error', function(event) {}); +self.addEventListener('error', function(event) { event.preventDefault(); }); +self.addEventListener('error', function(event) {}); +self.addEventListener('install', function(event) { throw new Error(); }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js new file mode 100644 index 0000000000..8f56d1bf14 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js @@ -0,0 +1,2 @@ +self.addEventListener('error', function(event) {}); +self.addEventListener('install', function(event) { throw new Error(); }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js new file mode 100644 index 0000000000..cc2f6d7e5e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js @@ -0,0 +1,7 @@ +// Ensure we can handle multiple install handlers. One handler throwing an +// error should cause the event dispatch to be treated as having unhandled +// errors. +self.addEventListener('install', function(event) {}); +self.addEventListener('install', function(event) {}); +self.addEventListener('install', function(event) { throw new Error(); }); +self.addEventListener('install', function(event) {}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js new file mode 100644 index 0000000000..964483f2f4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js @@ -0,0 +1,8 @@ +'use strict'; + +self.addEventListener('install', event => { + event.waitUntil(new Promise(() => { + // Use a promise that never resolves to prevent this service worker from + // advancing past the 'installing' state. + })); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js new file mode 100644 index 0000000000..6cb8f6ede6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js @@ -0,0 +1,5 @@ +self.addEventListener('install', function(event) { + event.waitUntil(new Promise(function(aRequest, aResponse) { + throw new Error(); + })); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js new file mode 100644 index 0000000000..6f439aee94 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js @@ -0,0 +1,8 @@ +'use strict'; + +// Use an infinite loop to prevent this service worker from advancing past the +// 'parsed' state. +let i = 0; +while (true) { + ++i; +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html new file mode 100644 index 0000000000..9c6d8bd504 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<body></body> +<script> +const URL = 'opaque-response?from=opaque-response-being-preloaded-xhr.html'; +function runTest() { + var l = document.createElement('link'); + // Use link rel=preload to try to get the browser to cache the opaque + // response. + l.setAttribute('rel', 'preload'); + l.setAttribute('href', URL); + l.setAttribute('as', 'fetch'); + l.onerror = function() { + parent.done('FAIL: preload failed unexpectedly'); + }; + document.body.appendChild(l); + xhr = new XMLHttpRequest; + xhr.withCredentials = true; + xhr.open('GET', URL); + // opaque-response returns an opaque response from serviceworker and thus + // the XHR must fail because it is not no-cors request. + // Particularly, the XHR must not reuse the opaque response from the + // preload request. + xhr.onerror = function() { + parent.done('PASS'); + }; + xhr.onload = function() { + parent.done('FAIL: ' + xhr.responseText); + }; + xhr.send(); +} +</script> +<body onload="setTimeout(runTest, 100)"></body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js new file mode 100644 index 0000000000..4fbe35df27 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js @@ -0,0 +1,12 @@ +importScripts('/common/get-host-info.sub.js'); + +var remoteUrl = get_host_info()['HTTPS_REMOTE_ORIGIN'] + + '/service-workers/service-worker/resources/sample.js' + +self.addEventListener('fetch', event => { + if (!event.request.url.match(/opaque-response\?from=/)) { + return; + } + + event.respondWith(fetch(remoteUrl, {mode: 'no-cors'})); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html new file mode 100644 index 0000000000..f31ac9b5c4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<body></body> +<script> +const URL = 'opaque-response?from=opaque-response-preloaded-xhr.html'; +function runTest() { + var l = document.createElement('link'); + // Use link rel=preload to try to get the browser to cache the opaque + // response. + l.setAttribute('rel', 'preload'); + l.setAttribute('href', URL); + l.setAttribute('as', 'fetch'); + l.onload = function() { + xhr = new XMLHttpRequest; + xhr.withCredentials = true; + xhr.open('GET', URL); + // opaque-response returns an opaque response from serviceworker and thus + // the XHR must fail because it is not no-cors request. + // Particularly, the XHR must not reuse the opaque response from the + // preload request. + xhr.onerror = function() { + parent.done('PASS'); + }; + xhr.onload = function() { + parent.done('FAIL: ' + xhr.responseText); + }; + xhr.send(); + }; + l.onerror = function() { + parent.done('FAIL: preload failed unexpectedly'); + }; + document.body.appendChild(l); +} +</script> +<body onload="setTimeout(runTest, 100)"></body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-frame.html new file mode 100644 index 0000000000..a57aacec7c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-frame.html @@ -0,0 +1,21 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> +<script> +self.addEventListener('error', evt => { + self.parent.postMessage({ type: 'ErrorEvent', msg: evt.message }, '*'); +}); + +const el = document.createElement('script'); +const params = new URLSearchParams(self.location.search); +el.src = params.get('script'); +el.addEventListener('load', evt => { + runScript(); +}); +document.body.appendChild(el); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-large.js b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-large.js new file mode 100644 index 0000000000..7e1c598efc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-large.js @@ -0,0 +1,41 @@ +function runScript() { + throw new Error("Intentional error."); +} + +function unused() { + // The following string is intended to be relatively large since some + // browsers trigger different code paths based on script size. + return "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a " + + "tortor ut orci bibendum blandit non quis diam. Aenean sit amet " + + "urna sit amet neque malesuada ultricies at vel nisi. Nunc et lacus " + + "est. Nam posuere erat enim, ac fringilla purus pellentesque " + + "cursus. Proin sodales eleifend lorem, eu semper massa scelerisque " + + "ac. Maecenas pharetra leo malesuada vulputate vulputate. Sed at " + + "efficitur odio. In rhoncus neque varius nibh efficitur gravida. " + + "Curabitur vitae dolor enim. Mauris semper lobortis libero sed " + + "congue. Donec felis ante, fringilla eget urna ut, finibus " + + "hendrerit lacus. Donec at interdum diam. Proin a neque vitae diam " + + "egestas euismod. Mauris posuere elementum lorem, eget convallis " + + "nisl elementum et. In ut leo ac neque dapibus pharetra quis ac " + + "velit. Integer pretium lectus non urna vulputate, in interdum mi " + + "lobortis. Sed laoreet ex et metus pharetra blandit. Curabitur " + + "sollicitudin non neque eu varius. Phasellus posuere congue arcu, " + + "in aliquam nunc fringilla a. Morbi id facilisis libero. Phasellus " + + "metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "tortor ut orci bibendum blandit non quis diam. Aenean sit amet " + + "urna sit amet neque malesuada ultricies at vel nisi. Nunc et lacus " + + "est. Nam posuere erat enim, ac fringilla purus pellentesque " + + "cursus. Proin sodales eleifend lorem, eu semper massa scelerisque " + + "ac. Maecenas pharetra leo malesuada vulputate vulputate. Sed at " + + "efficitur odio. In rhoncus neque varius nibh efficitur gravida. " + + "Curabitur vitae dolor enim. Mauris semper lobortis libero sed " + + "congue. Donec felis ante, fringilla eget urna ut, finibus " + + "hendrerit lacus. Donec at interdum diam. Proin a neque vitae diam " + + "egestas euismod. Mauris posuere elementum lorem, eget convallis " + + "nisl elementum et. In ut leo ac neque dapibus pharetra quis ac " + + "velit. Integer pretium lectus non urna vulputate, in interdum mi " + + "lobortis. Sed laoreet ex et metus pharetra blandit. Curabitur " + + "sollicitudin non neque eu varius. Phasellus posuere congue arcu, " + + "in aliquam nunc fringilla a. Morbi id facilisis libero. Phasellus " + + "metus."; +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-small.js b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-small.js new file mode 100644 index 0000000000..8b89098575 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-small.js @@ -0,0 +1,3 @@ +function runScript() { + throw new Error("Intentional error."); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-sw.js b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-sw.js new file mode 100644 index 0000000000..4d882c617d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-sw.js @@ -0,0 +1,37 @@ +importScripts('test-helpers.sub.js'); +importScripts('/common/get-host-info.sub.js'); + +const NAME = 'foo'; +const SAME_ORIGIN_BASE = new URL('./', self.location.href).href; +const CROSS_ORIGIN_BASE = new URL('./', + get_host_info().HTTPS_REMOTE_ORIGIN + base_path()).href; + +const urls = [ + `${SAME_ORIGIN_BASE}opaque-script-small.js`, + `${SAME_ORIGIN_BASE}opaque-script-large.js`, + `${CROSS_ORIGIN_BASE}opaque-script-small.js`, + `${CROSS_ORIGIN_BASE}opaque-script-large.js`, +]; + +self.addEventListener('install', evt => { + evt.waitUntil(async function() { + const c = await caches.open(NAME); + const promises = urls.map(async function(u) { + const r = await fetch(u, { mode: 'no-cors' }); + await c.put(u, r); + }); + await Promise.all(promises); + }()); +}); + +self.addEventListener('fetch', evt => { + const url = new URL(evt.request.url); + if (!url.pathname.includes('opaque-script-small.js') && + !url.pathname.includes('opaque-script-large.js')) { + return; + } + evt.respondWith(async function() { + const c = await caches.open(NAME); + return c.match(evt.request); + }()); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/other.html b/testing/web-platform/tests/service-workers/service-worker/resources/other.html new file mode 100644 index 0000000000..b9f3504387 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/other.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<title>Other</title> +Here's an other html file. diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/override_assert_object_equals.js b/testing/web-platform/tests/service-workers/service-worker/resources/override_assert_object_equals.js new file mode 100644 index 0000000000..835046d472 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/override_assert_object_equals.js @@ -0,0 +1,58 @@ +// .body attribute of Request and Response object are experimental feture. It is +// enabled when --enable-experimental-web-platform-features flag is set. +// Touching this attribute can change the behavior of the objects. To avoid +// touching it while comparing the objects in LayoutTest, we overwrite +// assert_object_equals method. + +(function() { + var original_assert_object_equals = self.assert_object_equals; + function _brand(object) { + return Object.prototype.toString.call(object).match(/^\[object (.*)\]$/)[1]; + } + var assert_request_equals = function(actual, expected, prefix) { + if (typeof actual !== 'object') { + assert_equals(actual, expected, prefix); + return; + } + assert_true(actual instanceof Request, prefix); + assert_true(expected instanceof Request, prefix); + assert_equals(actual.bodyUsed, expected.bodyUsed, prefix + '.bodyUsed'); + assert_equals(actual.method, expected.method, prefix + '.method'); + assert_equals(actual.url, expected.url, prefix + '.url'); + original_assert_object_equals(actual.headers, expected.headers, + prefix + '.headers'); + assert_equals(actual.context, expected.context, prefix + '.context'); + assert_equals(actual.referrer, expected.referrer, prefix + '.referrer'); + assert_equals(actual.mode, expected.mode, prefix + '.mode'); + assert_equals(actual.credentials, expected.credentials, + prefix + '.credentials'); + assert_equals(actual.cache, expected.cache, prefix + '.cache'); + }; + var assert_response_equals = function(actual, expected, prefix) { + if (typeof actual !== 'object') { + assert_equals(actual, expected, prefix); + return; + } + assert_true(actual instanceof Response, prefix); + assert_true(expected instanceof Response, prefix); + assert_equals(actual.bodyUsed, expected.bodyUsed, prefix + '.bodyUsed'); + assert_equals(actual.type, expected.type, prefix + '.type'); + assert_equals(actual.url, expected.url, prefix + '.url'); + assert_equals(actual.status, expected.status, prefix + '.status'); + assert_equals(actual.statusText, expected.statusText, + prefix + '.statusText'); + original_assert_object_equals(actual.headers, expected.headers, + prefix + '.headers'); + }; + var assert_object_equals = function(actual, expected, description) { + var prefix = (description ? description + ': ' : '') + _brand(expected); + if (expected instanceof Request) { + assert_request_equals(actual, expected, prefix); + } else if (expected instanceof Response) { + assert_response_equals(actual, expected, prefix); + } else { + original_assert_object_equals(actual, expected, description); + } + }; + self.assert_object_equals = assert_object_equals; +})(); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html new file mode 100644 index 0000000000..25ddf60145 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8"/> +<meta name="timeout" content="long"> +<title>Service Worker: Partitioned Cookies 3P Credentialless Iframe</title> +<script src="/resources/testharness.js"></script> +<script src="test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +</head> + +<body> +<script> + +// Check workers registered by a credentialless frame can access cookies set in that frame. +promise_test(async t => { + const script = './partitioned-cookies-3p-sw.js'; + const scope = './partitioned-cookies-3p-'; + const absolute_scope = new URL(scope, window.location).href; + + // Set a Partitioned cookie. + document.cookie = '__Host-partitioned=123; Secure; Path=/; SameSite=None; Partitioned;'; + assert_true(document.cookie.includes('__Host-partitioned=123')); + + // Make sure DOM cannot access the unpartitioned cookie. + assert_false(document.cookie.includes('unpartitioned=456')); + + const reg = await service_worker_unregister_and_register(t, script, scope); + await wait_for_state(t, reg.installing, 'activated'); + + let retrieved_registrations = + await navigator.serviceWorker.getRegistrations(); + let filtered_registrations = + retrieved_registrations.filter(reg => reg.scope == absolute_scope); + + // on_message will be reassigned below based on the expected reply from the service worker. + let on_message; + self.addEventListener('message', ev => on_message(ev)); + navigator.serviceWorker.addEventListener('message', evt => { + self.postMessage(evt.data, '*'); + }); + + // First test that the worker script started correctly and message passing is enabled. + let resolve_wait_promise; + let wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + let got; + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'test_message'}); + await wait_promise; + assert_true(got.ok, 'Message passing'); + + // Test that the partitioned cookie is available to this worker via CookieStore API. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'}); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_true( + got.cookies.includes('__Host-partitioned'), + 'Credentialless frame worker can access partitioned cookie via JS'); + assert_false( + got.cookies.includes('unpartitioned'), + 'Credentialless frame worker cannot access unpartitioned cookie via JS'); + + // Test that the partitioned cookie is available to this worker via HTTP. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({ type: 'echo_cookies_http' }); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_true( + got.cookies.includes('__Host-partitioned'), + 'Credentialless frame worker can access partitioned cookie via HTTP'); + assert_false( + got.cookies.includes('unpartitioned'), + 'Credentialless frame worker cannot access unpartitioned cookie via HTTP'); + + // Test that the partitioned cookie is not available to this worker in HTTP + // requests from importScripts. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({ type: 'echo_cookies_import' }); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_true( + got.cookies.includes('__Host-partitioned'), + 'Credentialless frame worker can access partitioned cookie via importScripts'); + assert_false( + got.cookies.includes('unpartitioned'), + 'Credentialless frame worker cannot access unpartitioned cookie via importScripts'); +}); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html new file mode 100644 index 0000000000..00b3412c41 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8"/> +<meta name="timeout" content="long"> +<title>Service Worker: Partitioned Cookies 3P Iframe</title> +<script src="/resources/testharness.js"></script> +<script src="test-helpers.sub.js"></script> +</head> + +<body> +<script> + +promise_test(async t => { + const script = './partitioned-cookies-3p-sw.js'; + const scope = './partitioned-cookies-3p-'; + const absolute_scope = new URL(scope, window.location).href; + + assert_false(document.cookie.includes('__Host-partitioned=123'), 'DOM cannot access partitioned cookie'); + assert_true(document.cookie.includes('unpartitioned=456'), 'DOM can access unpartitioned cookie'); + + const reg = await service_worker_unregister_and_register(t, script, scope); + await wait_for_state(t, reg.installing, 'activated'); + + let retrieved_registrations = + await navigator.serviceWorker.getRegistrations(); + let filtered_registrations = + retrieved_registrations.filter(reg => reg.scope == absolute_scope); + + // on_message will be reassigned below based on the expected reply from the service worker. + let on_message; + self.addEventListener('message', ev => on_message(ev)); + navigator.serviceWorker.addEventListener('message', evt => { + self.postMessage(evt.data, '*'); + }); + + // First test that the worker script started correctly and message passing is enabled. + let resolve_wait_promise; + let wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + let got; + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'test_message'}); + await wait_promise; + assert_true(got.ok, 'Message passing'); + + // Test that the partitioned cookie is not available to this worker via HTTP. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'echo_cookies_http'}); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_false( + got.cookies.includes('__Host-partitioned'), + 'Worker cannot access partitioned cookie via HTTP'); + assert_true( + got.cookies.includes('unpartitioned'), + 'Worker can access unpartitioned cookie via HTTP'); + + // Test that the partitioned cookie is not available to this worker via CookieStore API. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'}); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_false( + got.cookies.includes('__Host-partitioned'), + 'Worker cannot access partitioned cookie via JS'); + assert_true( + got.cookies.includes('unpartitioned'), + 'Worker can access unpartitioned cookie via JS'); + + // Test that the partitioned cookie is not available to this worker in HTTP + // requests from importScripts. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'echo_cookies_import'}); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_false( + got.cookies.includes('__Host-partitioned'), + 'Worker cannot access partitioned cookie via importScripts'); + assert_true( + got.cookies.includes('unpartitioned'), + 'Worker can access unpartitioned cookie via importScripts'); +}); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js new file mode 100644 index 0000000000..767dbf4432 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js @@ -0,0 +1,53 @@ +self.addEventListener('message', ev => ev.waitUntil(onMessage(ev))); + +async function onMessage(event) { + if (!event.data) + return; + switch (event.data.type) { + case 'test_message': + return onTestMessage(event); + case 'echo_cookies_http': + return onEchoCookiesHttp(event); + case 'echo_cookies_js': + return onEchoCookiesJs(event); + case 'echo_cookies_import': + return onEchoCookiesImport(event); + default: + return; + } +} + +// test_message just verifies that the message passing is working. +async function onTestMessage(event) { + event.source.postMessage({ok: true}); +} + +async function onEchoCookiesHttp(event) { + try { + const resp = await fetch( + `${self.origin}/cookies/resources/list.py`, {credentials: 'include'}); + const cookies = await resp.json(); + event.source.postMessage({ok: true, cookies: Object.keys(cookies)}); + } catch (err) { + event.source.postMessage({ok: false}); + } +} + +// echo_cookies returns the names of all of the cookies available to the worker. +async function onEchoCookiesJs(event) { + try { + const cookie_objects = await self.cookieStore.getAll(); + const cookies = cookie_objects.map(c => c.name); + event.source.postMessage({ok: true, cookies}); + } catch (err) { + event.source.postMessage({ok: false}); + } +} + +// Sets `self._cookies` variable, array of the names of cookies available to +// the request. +importScripts(`${self.origin}/cookies/resources/list-cookies-for-script.py`); + +function onEchoCookiesImport(event) { + event.source.postMessage({ok: true, cookies: self._cookies}); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html new file mode 100644 index 0000000000..8e90609da2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8"/> +<meta name="timeout" content="long"> +<title>Service Worker: Partitioned Cookies 3P Window</title> +<script src="/resources/testharness.js"></script> +</head> + +<body> +<script> + +promise_test(async t => { + assert_true( + location.search.includes('origin='), 'First party origin passed'); + const first_party_origin = decodeURIComponent( + location.search.split('origin=')[1]); + const iframe = document.createElement('iframe'); + iframe.src = new URL( + './partitioned-cookies-3p-frame.html', + first_party_origin + location.pathname).href; + document.body.appendChild(iframe); + fetch_tests_from_window(iframe.contentWindow); + + const credentialless_frame = document.createElement('iframe'); + credentialless_frame.credentialless = true; + credentialless_frame.src = new URL( + './partitioned-cookies-3p-credentialless-frame.html', + first_party_origin + location.pathname).href; + document.body.appendChild(credentialless_frame); + fetch_tests_from_window(credentialless_frame.contentWindow); +}); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js new file mode 100644 index 0000000000..767dbf4432 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js @@ -0,0 +1,53 @@ +self.addEventListener('message', ev => ev.waitUntil(onMessage(ev))); + +async function onMessage(event) { + if (!event.data) + return; + switch (event.data.type) { + case 'test_message': + return onTestMessage(event); + case 'echo_cookies_http': + return onEchoCookiesHttp(event); + case 'echo_cookies_js': + return onEchoCookiesJs(event); + case 'echo_cookies_import': + return onEchoCookiesImport(event); + default: + return; + } +} + +// test_message just verifies that the message passing is working. +async function onTestMessage(event) { + event.source.postMessage({ok: true}); +} + +async function onEchoCookiesHttp(event) { + try { + const resp = await fetch( + `${self.origin}/cookies/resources/list.py`, {credentials: 'include'}); + const cookies = await resp.json(); + event.source.postMessage({ok: true, cookies: Object.keys(cookies)}); + } catch (err) { + event.source.postMessage({ok: false}); + } +} + +// echo_cookies returns the names of all of the cookies available to the worker. +async function onEchoCookiesJs(event) { + try { + const cookie_objects = await self.cookieStore.getAll(); + const cookies = cookie_objects.map(c => c.name); + event.source.postMessage({ok: true, cookies}); + } catch (err) { + event.source.postMessage({ok: false}); + } +} + +// Sets `self._cookies` variable, array of the names of cookies available to +// the request. +importScripts(`${self.origin}/cookies/resources/list-cookies-for-script.py`); + +function onEchoCookiesImport(event) { + event.source.postMessage({ok: true, cookies: self._cookies}); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html new file mode 100644 index 0000000000..12b048ee04 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<title>Service Worker: 3P iframe for partitioned service workers</title> +<script src="./test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="./partitioned-utils.js"></script> + +<body> + <script> + // 1p mode will respond to requests for its current controller and + // postMessage when its controller changes. + async function onLoad1pMode(){ + self.addEventListener('message', evt => { + if(!evt.data) + return; + + if (evt.data.type === "get-controller") { + window.parent.postMessage({controller: navigator.serviceWorker.controller}); + } + }); + + navigator.serviceWorker.addEventListener('controllerchange', evt => { + window.parent.postMessage({status: "success", context: "1p"}, '*'); + }); + } + + // 3p mode will tell its SW to claim and then postMessage its results + // automatically. + async function onLoad3pMode() { + reg = await setupServiceWorker(); + + if(navigator.serviceWorker.controller != null){ + //This iframe is already under control of a service worker, testing for + // a controller change will timeout. Return a failure. + window.parent.postMessage({status: "failure", context: "3p"}, '*'); + return; + } + + // Once this client is claimed, let the test know. + navigator.serviceWorker.addEventListener('controllerchange', evt => { + window.parent.postMessage({status: "success", context: "3p"}, '*'); + }); + + // Trigger the SW to claim. + reg.active.postMessage({type: "claim"}); + + } + + const request_url = new URL(window.location.href); + var url_search = request_url.search.substr(1); + + if(url_search == "1p-mode") { + self.addEventListener('load', onLoad1pMode); + } + else if(url_search == "3p-mode") { + self.addEventListener('load', onLoad3pMode); + } + // Else do nothing. + </script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html new file mode 100644 index 0000000000..d05fef48bf --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<title>Service Worker: Innermost nested iframe for partitioned service workers</title> +<script src="./test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="./partitioned-utils.js"></script> + +<body> +Innermost 1p iframe (A2) with 3p ancestor (A1-B-A2-A3): this iframe will +register a service worker when it loads and then add its own iframe (A3) that +will attempt to navigate to a url. ServiceWorker will intercept this navigation +and resolve the ServiceWorker's internal Promise. When +ThirdPartyStoragePartitioning is enabled, this iframe should be partitioned +from the main frame and should not share a ServiceWorker. +<script> + +async function onLoad() { + // Set-up the ServiceWorker for this iframe, defined in: + // service-workers/service-worker/resources/partitioned-utils.js + await setupServiceWorker(); + + // When the SW's iframe finishes it'll post a message. This forwards + // it up to the middle-iframe. + self.addEventListener('message', evt => { + window.parent.postMessage(evt.data, '*'); + }); + + // Now that we have set up the ServiceWorker, we need it to + // intercept a navigation that will resolve its promise. + // To do this, we create an additional iframe to send that + // navigation request to resolve (`resolve.fakehtml`). If we're + // partitioned then there shouldn't be a promise to resolve. Defined + // in: service-workers/service-worker/resources/partitioned-storage-sw.js + const resolve_frame_url = new URL('./partitioned-resolve.fakehtml?FromNestedFrame', self.location); + const frame_resolve = await new Promise(resolve => { + var frame = document.createElement('iframe'); + frame.src = resolve_frame_url; + frame.onload = function() { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +self.addEventListener('load', onLoad); +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html new file mode 100644 index 0000000000..f748e2f78d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<title>Service Worker: Middle nested iframe for partitioned service workers</title> +<script src="./test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="./partitioned-utils.js"></script> + +<body> +Middle of the nested iframes (3p ancestor or B in A1-B-A2). +<script> + +async function onLoad() { + // The innermost iframe will recieve a message from the + // ServiceWorker and pass it to this iframe. We need to + // then pass that message to the main frame to complete + // the test. + self.addEventListener('message', evt => { + window.parent.postMessage(evt.data, '*'); + }); + + // Embed the innermost iframe and set-up the service worker there. + const innermost_iframe_url = new URL('./partitioned-service-worker-nested-iframe-child.html', + get_host_info().HTTPS_ORIGIN + self.location.pathname); + var frame = document.createElement('iframe'); + frame.src = innermost_iframe_url; + document.body.appendChild(frame); +} + +self.addEventListener('load', onLoad); +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html new file mode 100644 index 0000000000..747c058946 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<title>Service Worker: 3P iframe for partitioned service workers</title> +<script src="./test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="./partitioned-utils.js"></script> + +<body> + This iframe will register a service worker when it loads and then will use + getRegistrations to get a handle to the SW. It will then postMessage to the + SW to retrieve the SW's ID. This iframe will then forward that message up, + eventually, to the test. + <script> + + async function onLoad() { + const scope = './partitioned-' + const absoluteScope = new URL(scope, window.location).href; + + await setupServiceWorker(); + + // Once the SW sends us its ID, forward it up to the window. + navigator.serviceWorker.addEventListener('message', evt => { + window.parent.postMessage(evt.data, '*'); + }); + + // Now get the SW with getRegistrations. + const retrieved_registrations = + await navigator.serviceWorker.getRegistrations(); + + // It's possible that other tests have left behind other service workers. + // This steps filters those other SWs out. + const filtered_registrations = + retrieved_registrations.filter(reg => reg.scope == absoluteScope); + + filtered_registrations[0].active.postMessage({type: "get-id"}); + + } + + self.addEventListener('load', onLoad); + </script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html new file mode 100644 index 0000000000..7a2c36693e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<title>Service Worker: 3P iframe for partitioned service workers</title> +<script src="./test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="./partitioned-utils.js"></script> + +<body> + This iframe will register a service worker when it loads and then will use + getRegistrations to get a handle to the SW. It will then postMessage to the + SW to get the SW's clients via matchAll(). This iframe will then forward the + SW's response up, eventually, to the test. + <script> + async function onLoad() { + reg = await setupServiceWorker(); + + // Once the SW sends us its ID, forward it up to the window. + navigator.serviceWorker.addEventListener('message', evt => { + window.parent.postMessage(evt.data, '*'); + }); + + reg.active.postMessage({type: "get-match-all"}); + + } + + self.addEventListener('load', onLoad); + </script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html new file mode 100644 index 0000000000..1b7f671b37 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<title>Service Worker: 3P iframe for partitioned service workers</title> +<script src="./test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="./partitioned-utils.js"></script> + + +<body> +This iframe will register a service worker when it loads and then add its own +iframe that will attempt to navigate to a url that service worker will intercept +and use to resolve the service worker's internal Promise. +<script> + +async function onLoad() { + await setupServiceWorker(); + + // When the SW's iframe finishes it'll post a message. This forwards it up to + // the window. + self.addEventListener('message', evt => { + window.parent.postMessage(evt.data, '*'); + }); + + // Now try to resolve the SW's promise. If we're partitioned then there + // shouldn't be a promise to resolve. + const resolve_frame_url = new URL('./partitioned-resolve.fakehtml?From3pFrame', self.location); + const frame_resolve = await new Promise(resolve => { + var frame = document.createElement('iframe'); + frame.src = resolve_frame_url; + frame.onload = function() { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +self.addEventListener('load', onLoad); +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html new file mode 100644 index 0000000000..86384ce280 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<title>Service Worker: 3P window for partitioned service workers</title> +<script src="./test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> + + +<body> +This page should be opened as a third-party window. It then loads an iframe +specified by the query parameter. Finally it forwards the postMessage from the +iframe up to the opener (the test). + +<script> + +async function onLoad() { + const message_promise = new Promise(resolve => { + self.addEventListener('message', evt => { + resolve(evt.data); + }); + }); + + const search_param = new URLSearchParams(window.location.search); + const iframe_url = search_param.get('target'); + + var frame = document.createElement('iframe'); + frame.src = iframe_url; + frame.style.position = 'absolute'; + document.body.appendChild(frame); + + + await message_promise.then(data => { + // We're done, forward the message and clean up. + window.opener.postMessage(data, '*'); + + frame.remove(); + }); +} + +self.addEventListener('load', onLoad); + +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-storage-sw.js b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-storage-sw.js new file mode 100644 index 0000000000..00f7979810 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-storage-sw.js @@ -0,0 +1,81 @@ +// Holds the promise that the "resolve.fakehtml" call attempts to resolve. +// This is "the SW's promise" that other parts of the test refer to. +var promise; +// Stores the resolve funcution for the current promise. +var pending_resolve_func = null; +// Unique ID to determine which service worker is being used. +const ID = Math.random(); + +function callAndResetResolve() { + var local_resolve = pending_resolve_func; + pending_resolve_func = null; + local_resolve(); +} + +self.addEventListener('fetch', function(event) { + fetchEventHandler(event); +}) + +self.addEventListener('message', (event) => { + event.waitUntil(async function() { + if(!event.data) + return; + + if (event.data.type === "get-id") { + event.source.postMessage({ID: ID}); + } + else if(event.data.type === "get-match-all") { + clients.matchAll({includeUncontrolled: true}).then(clients_list => { + const url_list = clients_list.map(item => item.url); + event.source.postMessage({urls_list: url_list}); + }); + } + else if(event.data.type === "claim") { + await clients.claim(); + } + }()); +}); + +async function fetchEventHandler(event){ + var request_url = new URL(event.request.url); + var url_search = request_url.search.substr(1); + request_url.search = ""; + if ( request_url.href.endsWith('waitUntilResolved.fakehtml') ) { + + if (pending_resolve_func != null) { + // Respond with an error if there is already a pending promise + event.respondWith(Response.error()); + return; + } + + // Create the new promise. + promise = new Promise(function(resolve) { + pending_resolve_func = resolve; + }); + event.waitUntil(promise); + + event.respondWith(new Response(` + <html> + Promise created by ${url_search} + <script>self.parent.postMessage({ ID:${ID}, source: "${url_search}" + }, '*');</script> + </html> + `, {headers: {'Content-Type': 'text/html'}} + )); + + } + else if ( request_url.href.endsWith('resolve.fakehtml') ) { + var has_pending = !!pending_resolve_func; + event.respondWith(new Response(` + <html> + Promise settled for ${url_search} + <script>self.parent.postMessage({ ID:${ID}, has_pending: ${has_pending}, + source: "${url_search}" }, '*');</script> + </html> + `, {headers: {'Content-Type': 'text/html'}})); + + if (has_pending) { + callAndResetResolve(); + } + } +}
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-utils.js b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-utils.js new file mode 100644 index 0000000000..22e90beaec --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-utils.js @@ -0,0 +1,110 @@ +// The resolve function for the current pending event listener's promise. +// It is nulled once the promise is resolved. +var message_event_promise_resolve = null; + +function messageEventHandler(evt) { + if (message_event_promise_resolve) { + local_resolve = message_event_promise_resolve; + message_event_promise_resolve = null; + local_resolve(evt.data); + } +} + +function makeMessagePromise() { + if (message_event_promise_resolve != null) { + // Do not create a new promise until the previous is settled. + return; + } + + return new Promise(resolve => { + message_event_promise_resolve = resolve; + }); +} + +// Loads a url for the frame type and then returns a promise for +// the data that was postMessage'd from the loaded frame. +// If the frame type is 'window' then `url` is encoded into the search param +// as the url the 3p window is meant to iframe. +function loadAndReturnSwData(t, url, frame_type) { + if (frame_type !== 'iframe' && frame_type !== 'window') { + return; + } + + const message_promise = makeMessagePromise(); + + // Create the iframe or window and then return the promise for data. + if ( frame_type === 'iframe' ) { + const frame = with_iframe(url, false); + t.add_cleanup(async () => { + const f = await frame; + f.remove(); + }); + } + else { + // 'window' case. + const search_param = new URLSearchParams(); + search_param.append('target', url); + + const third_party_window_url = new URL( + './resources/partitioned-service-worker-third-party-window.html' + + '?' + search_param, + get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname); + + const w = window.open(third_party_window_url); + t.add_cleanup(() => w.close()); + } + + return message_promise; +} + +// Checks for an existing service worker registration. If not present, +// registers and maintains a service worker. Used in windows or iframes +// that will be partitioned from the main frame. +async function setupServiceWorker() { + + const script = './partitioned-storage-sw.js'; + const scope = './partitioned-'; + + var reg = await navigator.serviceWorker.register(script, { scope: scope }); + + // We should keep track if we installed a worker or not. If we did then we + // need to uninstall it. Otherwise we let the top level test uninstall it + // (If partitioning is not working). + var installed_a_worker = true; + await new Promise(resolve => { + // Check if a worker is already activated. + var worker = reg.active; + // If so, just resolve. + if ( worker ) { + installed_a_worker = false; + resolve(); + return; + } + + //Otherwise check if one is waiting. + worker = reg.waiting; + // If not waiting, grab the installing worker. + if ( !worker ) { + worker = reg.installing; + } + + // Resolve once it's activated. + worker.addEventListener('statechange', evt => { + if (worker.state === 'activated') { + resolve(); + } + }); + }); + + self.addEventListener('unload', async () => { + // If we didn't install a worker then that means the top level test did, and + // that test is therefore responsible for cleaning it up. + if ( !installed_a_worker ) { + return; + } + + await reg.unregister(); + }); + + return reg; +}
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/pass-through-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/pass-through-worker.js new file mode 100644 index 0000000000..5eaf48d588 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/pass-through-worker.js @@ -0,0 +1,3 @@ +addEventListener('fetch', evt => { + evt.respondWith(fetch(evt.request)); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/pass.txt b/testing/web-platform/tests/service-workers/service-worker/resources/pass.txt new file mode 100644 index 0000000000..7ef22e9a43 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/pass.txt @@ -0,0 +1 @@ +PASS diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/performance-timeline-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/performance-timeline-worker.js new file mode 100644 index 0000000000..6c6dfcbd28 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/performance-timeline-worker.js @@ -0,0 +1,62 @@ +importScripts('/resources/testharness.js'); + +promise_test(function(test) { + var durationMsec = 100; + // There are limits to our accuracy here. Timers may fire up to a + // millisecond early due to platform-dependent rounding. In addition + // the performance API introduces some rounding as well to prevent + // timing attacks. + var accuracy = 1.5; + return new Promise(function(resolve) { + performance.mark('startMark'); + setTimeout(resolve, durationMsec); + }).then(function() { + performance.mark('endMark'); + performance.measure('measure', 'startMark', 'endMark'); + var startMark = performance.getEntriesByName('startMark')[0]; + var endMark = performance.getEntriesByName('endMark')[0]; + var measure = performance.getEntriesByType('measure')[0]; + assert_equals(measure.startTime, startMark.startTime); + assert_approx_equals(endMark.startTime - startMark.startTime, + measure.duration, 0.001); + assert_greater_than(measure.duration, durationMsec - accuracy); + assert_equals(performance.getEntriesByType('mark').length, 2); + assert_equals(performance.getEntriesByType('measure').length, 1); + performance.clearMarks('startMark'); + performance.clearMeasures('measure'); + assert_equals(performance.getEntriesByType('mark').length, 1); + assert_equals(performance.getEntriesByType('measure').length, 0); + }); + }, 'User Timing'); + +promise_test(function(test) { + return fetch('sample.txt') + .then(function(resp) { + return resp.text(); + }) + .then(function(text) { + var expectedResources = ['testharness.js', 'sample.txt']; + assert_equals(performance.getEntriesByType('resource').length, expectedResources.length); + for (var i = 0; i < expectedResources.length; i++) { + var entry = performance.getEntriesByType('resource')[i]; + assert_true(entry.name.endsWith(expectedResources[i])); + assert_equals(entry.workerStart, 0); + assert_greater_than(entry.startTime, 0); + assert_greater_than(entry.responseEnd, entry.startTime); + } + return new Promise(function(resolve) { + performance.onresourcetimingbufferfull = _ => { + resolve('bufferfull'); + } + performance.setResourceTimingBufferSize(expectedResources.length); + fetch('sample.txt'); + }); + }) + .then(function(result) { + assert_equals(result, 'bufferfull'); + performance.clearResourceTimings(); + assert_equals(performance.getEntriesByType('resource').length, 0); + }) + }, 'Resource Timing'); + +done(); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-blob-url.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-blob-url.js new file mode 100644 index 0000000000..9095194a4c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-blob-url.js @@ -0,0 +1,5 @@ +self.onmessage = e => { + fetch(e.data) + .then(response => response.text()) + .then(text => e.source.postMessage('Worker reply:' + text)); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js new file mode 100644 index 0000000000..87a4500d75 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js @@ -0,0 +1,24 @@ +var messageHandler = function(port, e) { + var text_decoder = new TextDecoder; + port.postMessage({ + content: text_decoder.decode(e.data), + byteLength: e.data.byteLength + }); + + // Send back the array buffer via Client.postMessage. + port.postMessage(e.data, {transfer: [e.data.buffer]}); + + port.postMessage({ + content: text_decoder.decode(e.data), + byteLength: e.data.byteLength + }); +}; + +self.addEventListener('message', e => { + if (e.ports[0]) { + // Wait for messages sent via MessagePort. + e.ports[0].onmessage = messageHandler.bind(null, e.ports[0]); + return; + } + messageHandler(e.source, e); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-echo-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-echo-worker.js new file mode 100644 index 0000000000..f088ad1278 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-echo-worker.js @@ -0,0 +1,3 @@ +self.addEventListener('message', event => { + event.source.postMessage(event.data); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-fetched-text.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-fetched-text.js new file mode 100644 index 0000000000..9fc67171d0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-fetched-text.js @@ -0,0 +1,5 @@ +self.onmessage = async (e) => { + const response = await fetch(e.data); + const text = await response.text(); + self.postMessage(text); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js new file mode 100644 index 0000000000..7af935f4f8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js @@ -0,0 +1,19 @@ +self.onmessage = function(e) { + e.waitUntil(self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + var messageChannel = new MessageChannel(); + messageChannel.port1.onmessage = + onMessageViaMessagePort.bind(null, messageChannel.port1); + client.postMessage(undefined, [messageChannel.port2]); + }); + })); +}; + +function onMessageViaMessagePort(port, e) { + var message = e.data; + if ('value' in message) { + port.postMessage({ack: 'Acking value: ' + message.value}); + } else if ('done' in message) { + port.postMessage({done: true}); + } +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js new file mode 100644 index 0000000000..c2b0bcb8bf --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js @@ -0,0 +1,9 @@ +if ('DedicatedWorkerGlobalScope' in self && + self instanceof DedicatedWorkerGlobalScope) { + postMessage('dedicated worker script loaded'); +} else if ('SharedWorkerGlobalScope' in self && + self instanceof SharedWorkerGlobalScope) { + self.onconnect = evt => { + evt.ports[0].postMessage('shared worker script loaded'); + }; +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js new file mode 100644 index 0000000000..1791306358 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js @@ -0,0 +1,10 @@ +self.onmessage = function(e) { + e.waitUntil(self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + client.postMessage('Sending message via clients'); + if (!Array.isArray(clients)) + client.postMessage('clients is not an array'); + client.postMessage('quit'); + }); + })); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js new file mode 100644 index 0000000000..d35c1c952b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js @@ -0,0 +1,24 @@ +var messageHandler = function(port, e) { + var text_decoder = new TextDecoder; + port.postMessage({ + content: text_decoder.decode(e.data), + byteLength: e.data.byteLength + }); + + // Send back the array buffer via Client.postMessage. + port.postMessage(e.data, [e.data.buffer]); + + port.postMessage({ + content: text_decoder.decode(e.data), + byteLength: e.data.byteLength + }); +}; + +self.addEventListener('message', e => { + if (e.ports[0]) { + // Wait for messages sent via MessagePort. + e.ports[0].onmessage = messageHandler.bind(null, e.ports[0]); + return; + } + messageHandler(e.source, e); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-worker.js new file mode 100644 index 0000000000..858cf04267 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-worker.js @@ -0,0 +1,19 @@ +var port; + +// Exercise the 'onmessage' handler: +self.onmessage = function(e) { + var message = e.data; + if ('port' in message) { + port = message.port; + } +}; + +// And an event listener: +self.addEventListener('message', function(e) { + var message = e.data; + if ('value' in message) { + port.postMessage('Acking value: ' + message.value); + } else if ('done' in message) { + port.postMessage('quit'); + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js new file mode 100644 index 0000000000..cab6058339 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js @@ -0,0 +1,40 @@ +// This worker is meant to test range requests where the responses come from +// multiple origins. It forwards the first request to a cross-origin URL +// (generating an opaque response). The server is expected to return a 206 +// Partial Content response. Then the worker lets subsequent range requests +// fall back to network (generating same-origin responses). The intent is to try +// to trick the browser into treating the resource as same-origin. +// +// It would also be interesting to do the reverse test where the first request +// goes to the same-origin URL, and subsequent range requests go cross-origin in +// 'no-cors' mode to receive opaque responses. But the service worker cannot do +// this, because in 'no-cors' mode the 'range' HTTP header is disallowed. + +importScripts('/common/get-host-info.sub.js') + +let initial = true; +function is_initial_request() { + const old = initial; + initial = false; + return old; +} + +self.addEventListener('fetch', e => { + const url = new URL(e.request.url); + if (url.search.indexOf('VIDEO') == -1) { + // Fall back for non-video. + return; + } + + // Make the first request go cross-origin. + if (is_initial_request()) { + const cross_origin_url = get_host_info().HTTPS_REMOTE_ORIGIN + + url.pathname + url.search; + const cross_origin_request = new Request(cross_origin_url, + {mode: 'no-cors', headers: e.request.headers}); + e.respondWith(fetch(cross_origin_request)); + return; + } + + // Fall back to same origin for subsequent range requests. + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js new file mode 100644 index 0000000000..7580b0b68a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js @@ -0,0 +1,60 @@ +// This worker is meant to test range requests where the responses are a mix of +// opaque ones and non-opaque ones. It forwards the first request to a +// cross-origin URL (generating an opaque response). The server is expected to +// return a 206 Partial Content response. Then the worker forwards subsequent +// range requests to that URL, with CORS sharing generating a non-opaque +// responses. The intent is to try to trick the browser into treating the +// resource as non-opaque. +// +// It would also be interesting to do the reverse test where the first request +// uses 'cors', and subsequent range requests use 'no-cors' mode. But the +// service worker cannot do this, because in 'no-cors' mode the 'range' HTTP +// header is disallowed. + +importScripts('/common/get-host-info.sub.js') + +let initial = true; +function is_initial_request() { + const old = initial; + initial = false; + return old; +} + +self.addEventListener('fetch', e => { + const url = new URL(e.request.url); + if (url.search.indexOf('VIDEO') == -1) { + // Fall back for non-video. + return; + } + + let cross_origin_url = get_host_info().HTTPS_REMOTE_ORIGIN + + url.pathname + url.search; + + // The first request is no-cors. + if (is_initial_request()) { + const init = { mode: 'no-cors', headers: e.request.headers }; + const cross_origin_request = new Request(cross_origin_url, init); + e.respondWith(fetch(cross_origin_request)); + return; + } + + // Subsequent range requests are cors. + + // Copy headers needed for range requests. + let my_headers = new Headers; + if (e.request.headers.get('accept')) + my_headers.append('accept', e.request.headers.get('accept')); + if (e.request.headers.get('range')) + my_headers.append('range', e.request.headers.get('range')); + + // Add &ACAOrigin to allow CORS. + cross_origin_url += '&ACAOrigin=' + get_host_info().HTTPS_ORIGIN; + // Add &ACAHeaders to allow range requests. + cross_origin_url += '&ACAHeaders=accept,range'; + + // Make the CORS request. + const init = { mode: 'cors', headers: my_headers }; + const cross_origin_request = new Request(cross_origin_url, init); + e.respondWith(fetch(cross_origin_request)); + }); + diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/redirect-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/redirect-worker.js new file mode 100644 index 0000000000..82e21fc26f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/redirect-worker.js @@ -0,0 +1,145 @@ +// We store an empty response for each fetch event request we see +// in this Cache object so we can get the list of urls in the +// message event. +var cacheName = 'urls-' + self.registration.scope; + +var waitUntilPromiseList = []; + +// Sends the requests seen by this worker. The output is: +// { +// requestInfos: [ +// {url: url1, resultingClientId: id1}, +// {url: url2, resultingClientId: id2}, +// ] +// } +async function getRequestInfos(event) { + // Wait for fetch events to finish. + await Promise.all(waitUntilPromiseList); + waitUntilPromiseList = []; + + // Generate the message. + const cache = await caches.open(cacheName); + const requestList = await cache.keys(); + const requestInfos = []; + for (let i = 0; i < requestList.length; i++) { + const response = await cache.match(requestList[i]); + const body = await response.json(); + requestInfos[i] = { + url: requestList[i].url, + resultingClientId: body.resultingClientId + }; + } + await caches.delete(cacheName); + + event.data.port.postMessage({requestInfos}); +} + +// Sends the results of clients.get(id) from this worker. The +// input is: +// { +// actual_ids: {a: id1, b: id2, x: id3} +// } +// +// The output is: +// { +// clients: { +// a: {found: false}, +// b: {found: false}, +// x: { +// id: id3, +// url: url1, +// found: true +// } +// } +// } +async function getClients(event) { + // |actual_ids| is like: + // {a: id1, b: id2, x: id3} + const actual_ids = event.data.actual_ids; + const result = {} + for (let key of Object.keys(actual_ids)) { + const id = actual_ids[key]; + const client = await self.clients.get(id); + if (client === undefined) + result[key] = {found: false}; + else + result[key] = {found: true, url: client.url, id: client.id}; + } + event.data.port.postMessage({clients: result}); +} + +self.addEventListener('message', async function(event) { + if (event.data.command == 'getRequestInfos') { + event.waitUntil(getRequestInfos(event)); + return; + } + + if (event.data.command == 'getClients') { + event.waitUntil(getClients(event)); + return; + } +}); + +function get_query_params(url) { + var search = (new URL(url)).search; + if (!search) { + return {}; + } + var ret = {}; + var params = search.substring(1).split('&'); + params.forEach(function(param) { + var element = param.split('='); + ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]); + }); + return ret; +} + +self.addEventListener('fetch', function(event) { + var waitUntilPromise = caches.open(cacheName).then(function(cache) { + const responseBody = {}; + responseBody['resultingClientId'] = event.resultingClientId; + const headers = new Headers({'Content-Type': 'application/json'}); + const response = new Response(JSON.stringify(responseBody), {headers}); + return cache.put(event.request, response); + }); + event.waitUntil(waitUntilPromise); + + var params = get_query_params(event.request.url); + if (!params['sw']) { + // To avoid races, add the waitUntil() promise to our global list. + // If we get a message event before we finish here, it will wait + // these promises to complete before proceeding to read from the + // cache. + waitUntilPromiseList.push(waitUntilPromise); + return; + } + + event.respondWith(waitUntilPromise.then(async () => { + if (params['sw'] == 'gen') { + return Response.redirect(params['url']); + } else if (params['sw'] == 'gen-manual') { + // Note this differs from Response.redirect() in that relative URLs are + // preserved. + return new Response("", { + status: 301, + headers: {location: params['url']}, + }); + } else if (params['sw'] == 'fetch') { + return fetch(event.request); + } else if (params['sw'] == 'fetch-url') { + return fetch(params['url']); + } else if (params['sw'] == 'follow') { + return fetch(new Request(event.request.url, {redirect: 'follow'})); + } else if (params['sw'] == 'manual') { + return fetch(new Request(event.request.url, {redirect: 'manual'})); + } else if (params['sw'] == 'manualThroughCache') { + const url = event.request.url; + await caches.delete(url) + const cache = await self.caches.open(url); + const response = await fetch(new Request(url, {redirect: 'manual'})); + await cache.put(event.request, response); + return cache.match(url); + } + // unexpected... trigger an interception failure + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/redirect.py b/testing/web-platform/tests/service-workers/service-worker/resources/redirect.py new file mode 100644 index 0000000000..bd559d5d1e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/redirect.py @@ -0,0 +1,27 @@ +from wptserve.utils import isomorphic_decode + +def main(request, response): + if b'Status' in request.GET: + status = int(request.GET[b"Status"]) + else: + status = 302 + + headers = [] + + url = isomorphic_decode(request.GET[b'Redirect']) + headers.append((b"Location", url)) + + if b"ACAOrigin" in request.GET: + for item in request.GET[b"ACAOrigin"].split(b","): + headers.append((b"Access-Control-Allow-Origin", item)) + + for suffix in [b"Headers", b"Methods", b"Credentials"]: + query = b"ACA%s" % suffix + header = b"Access-Control-Allow-%s" % suffix + if query in request.GET: + headers.append((header, request.GET[query])) + + if b"ACEHeaders" in request.GET: + headers.append((b"Access-Control-Expose-Headers", request.GET[b"ACEHeaders"])) + + return status, headers, b"" diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/referer-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/referer-iframe.html new file mode 100644 index 0000000000..295ff45671 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/referer-iframe.html @@ -0,0 +1,39 @@ +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js"></script> +<script> +function check_referer(url, expected_referer) { + return fetch(url) + .then(function(res) { return res.json(); }) + .then(function(headers) { + if (headers['referer'] === expected_referer) { + return Promise.resolve(); + } else { + return Promise.reject('Referer for ' + url + ' must be ' + + expected_referer + ' but got ' + + headers['referer']); + } + }); +} + +window.addEventListener('message', function(evt) { + var host_info = get_host_info(); + var port = evt.ports[0]; + check_referer('request-headers.py?ignore=true', + host_info['HTTPS_ORIGIN'] + + base_path() + 'referer-iframe.html') + .then(function() { + return check_referer( + 'request-headers.py', + host_info['HTTPS_ORIGIN'] + + base_path() + 'referer-iframe.html'); + }) + .then(function() { + return check_referer( + 'request-headers.py?url=request-headers.py', + host_info['HTTPS_ORIGIN'] + + base_path() + 'fetch-rewrite-worker.js'); + }) + .then(function() { port.postMessage({results: 'finish'}); }) + .catch(function(e) { port.postMessage({results: 'failure:' + e}); }); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/referrer-policy-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/referrer-policy-iframe.html new file mode 100644 index 0000000000..9ef3cd19a9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/referrer-policy-iframe.html @@ -0,0 +1,32 @@ +<script src="/common/get-host-info.sub.js"></script> +<script src="test-helpers.sub.js"></script> +<script> +function check_referer(url, expected_referer) { + return fetch(url) + .then(function(res) { return res.json(); }) + .then(function(headers) { + if (headers['referer'] === expected_referer) { + return Promise.resolve(); + } else { + return Promise.reject('Referer for ' + url + ' must be ' + + expected_referer + ' but got ' + + headers['referer']); + } + }); +} + +window.addEventListener('message', function(evt) { + var host_info = get_host_info(); + var port = evt.ports[0]; + check_referer('request-headers.py?ignore=true', + host_info['HTTPS_ORIGIN'] + + base_path() + 'referrer-policy-iframe.html') + .then(function() { + return check_referer( + 'request-headers.py?url=request-headers.py', + host_info['HTTPS_ORIGIN'] + '/'); + }) + .then(function() { port.postMessage({results: 'finish'}); }) + .catch(function(e) { port.postMessage({results: 'failure:' + e}); }); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/register-closed-window-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/register-closed-window-iframe.html new file mode 100644 index 0000000000..117f25477b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/register-closed-window-iframe.html @@ -0,0 +1,19 @@ +<html> +<head> +<script> +window.addEventListener('message', async function(evt) { + if (evt.data === 'START') { + var w = window.open('./'); + var sw = w.navigator.serviceWorker; + w.close(); + w = null; + try { + await sw.register('doesntmatter.js'); + } finally { + parent.postMessage('OK', '*'); + } + } +}); +</script> +</head> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/register-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/register-iframe.html new file mode 100644 index 0000000000..f5a040e41d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/register-iframe.html @@ -0,0 +1,4 @@ +<script type="text/javascript"> +navigator.serviceWorker.register('empty-worker.js', + {scope: 'register-iframe.html'}); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/register-rewrite-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/register-rewrite-worker.html new file mode 100644 index 0000000000..bf06317ad9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/register-rewrite-worker.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<script> +async function onLoad() { + const params = new URLSearchParams(self.location.search); + const scope = self.origin + params.get('scopepath'); + const script = './fetch-rewrite-worker.js'; + const reg = await navigator.serviceWorker.register(script, { scope: scope }); + // In nested cases we may be impacted by partitioning or not depending on + // the browser. With partitioning we will be installing a new worker here, + // but without partitioning the worker will already exist. Handle both cases. + if (reg.installing) { + await new Promise(resolve => { + const worker = reg.installing; + worker.addEventListener('statechange', evt => { + if (worker.state === 'activated') { + resolve(); + } + }); + }); + if (reg.navigationPreload) { + await reg.navigationPreload.enable(); + } + } + if (window.opener) { + window.opener.postMessage({ type: 'SW-REGISTERED' }, '*'); + } else { + window.top.postMessage({ type: 'SW-REGISTERED' }, '*'); + } +} +self.addEventListener('load', onLoad); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js new file mode 100644 index 0000000000..037e6c0fde --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js @@ -0,0 +1,96 @@ +// Registration tests that mostly verify the MIME type. +// +// This file tests every MIME type so it necessarily starts many service +// workers, so it may be slow. +function registration_tests_mime_types(register_method) { + promise_test(function(t) { + var script = 'resources/mime-type-worker.py'; + var scope = 'resources/scope/no-mime-type-worker/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration of no MIME type script should fail.'); + }, 'Registering script with no MIME type'); + + promise_test(function(t) { + var script = 'resources/mime-type-worker.py?mime=text/plain'; + var scope = 'resources/scope/bad-mime-type-worker/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration of plain text script should fail.'); + }, 'Registering script with bad MIME type'); + + /** + * ServiceWorkerContainer.register() should throw a TypeError, according to + * step 17.1 of https://w3c.github.io/ServiceWorker/#importscripts + * + * "[17] If an uncaught runtime script error occurs during the above step, then: + * [17.1] Invoke Reject Job Promise with job and TypeError" + * + * (Where the "uncaught runtime script error" is thrown by an unsuccessful + * importScripts()) + */ + promise_test(function(t) { + var script = 'resources/import-mime-type-worker.py'; + var scope = 'resources/scope/no-mime-type-worker/'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of no MIME type imported script should fail.'); + }, 'Registering script that imports script with no MIME type'); + + promise_test(function(t) { + var script = 'resources/import-mime-type-worker.py?mime=text/plain'; + var scope = 'resources/scope/bad-mime-type-worker/'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of plain text imported script should fail.'); + }, 'Registering script that imports script with bad MIME type'); + + const validMimeTypes = [ + 'application/ecmascript', + 'application/javascript', + 'application/x-ecmascript', + 'application/x-javascript', + 'text/ecmascript', + 'text/javascript', + 'text/javascript1.0', + 'text/javascript1.1', + 'text/javascript1.2', + 'text/javascript1.3', + 'text/javascript1.4', + 'text/javascript1.5', + 'text/jscript', + 'text/livescript', + 'text/x-ecmascript', + 'text/x-javascript' + ]; + + for (const validMimeType of validMimeTypes) { + promise_test(() => { + var script = `resources/mime-type-worker.py?mime=${validMimeType}`; + var scope = 'resources/scope/good-mime-type-worker/'; + + return register_method(script, {scope}).then(registration => { + assert_true( + registration instanceof ServiceWorkerRegistration, + 'Successfully registered.'); + return registration.unregister(); + }); + }, `Registering script with good MIME type ${validMimeType}`); + + promise_test(() => { + var script = `resources/import-mime-type-worker.py?mime=${validMimeType}`; + var scope = 'resources/scope/good-mime-type-worker/'; + + return register_method(script, { scope }).then(registration => { + assert_true( + registration instanceof ServiceWorkerRegistration, + 'Successfully registered.'); + return registration.unregister(); + }); + }, `Registering script that imports script with good MIME type ${validMimeType}`); + } +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-scope.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-scope.js new file mode 100644 index 0000000000..30c424b2b4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-scope.js @@ -0,0 +1,120 @@ +// Registration tests that mostly exercise the scope option. +function registration_tests_scope(register_method) { + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/scope%2fencoded-slash-in-scope'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded slash in the scope should be rejected.'); + }, 'Scope including URL-encoded slash'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/scope%5cencoded-slash-in-scope'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded backslash in the scope should be rejected.'); + }, 'Scope including URL-encoded backslash'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'data:text/html,'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'scope URL scheme is not "http" or "https"'); + }, 'Scope URL scheme is a data: URL'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = new URL('resources', location).href.replace('https:', 'ftp:'); + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'scope URL scheme is not "http" or "https"'); + }, 'Scope URL scheme is an ftp: URL'); + + promise_test(function(t) { + // URL-encoded full-width 'scope'. + var name = '%ef%bd%93%ef%bd%83%ef%bd%8f%ef%bd%90%ef%bd%85'; + var script = 'resources/empty-worker.js'; + var scope = 'resources/' + name + '/escaped-multibyte-character-scope'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + registration.scope, + normalizeURL(scope), + 'URL-encoded multibyte characters should be available.'); + return registration.unregister(); + }); + }, 'Scope including URL-encoded multibyte characters'); + + promise_test(function(t) { + // Non-URL-encoded full-width "scope". + var name = String.fromCodePoint(0xff53, 0xff43, 0xff4f, 0xff50, 0xff45); + var script = 'resources/empty-worker.js'; + var scope = 'resources/' + name + '/non-escaped-multibyte-character-scope'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + registration.scope, + normalizeURL(scope), + 'Non-URL-encoded multibyte characters should be available.'); + return registration.unregister(); + }); + }, 'Scope including non-escaped multibyte characters'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/././scope/self-reference-in-scope'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + registration.scope, + normalizeURL('resources/scope/self-reference-in-scope'), + 'Scope including self-reference should be normalized.'); + return registration.unregister(); + }); + }, 'Scope including self-reference'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/../resources/scope/parent-reference-in-scope'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + registration.scope, + normalizeURL('resources/scope/parent-reference-in-scope'), + 'Scope including parent-reference should be normalized.'); + return registration.unregister(); + }); + }, 'Scope including parent-reference'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/scope////consecutive-slashes-in-scope'; + return register_method(script, {scope: scope}) + .then(function(registration) { + // Although consecutive slashes in the scope are not unified, the + // scope is under the script directory and registration should + // succeed. + assert_equals( + registration.scope, + normalizeURL(scope), + 'Should successfully be registered.'); + return registration.unregister(); + }) + }, 'Scope including consecutive slashes'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'filesystem:' + normalizeURL('resources/scope/filesystem-scope-url'); + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registering with the scope that has same-origin filesystem: URL ' + + 'should fail with TypeError.'); + }, 'Scope URL is same-origin filesystem: URL'); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script-url.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script-url.js new file mode 100644 index 0000000000..55cbe6fa95 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script-url.js @@ -0,0 +1,82 @@ +// Registration tests that mostly exercise the scriptURL parameter. +function registration_tests_script_url(register_method) { + promise_test(function(t) { + var script = 'resources%2fempty-worker.js'; + var scope = 'resources/scope/encoded-slash-in-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded slash in the script URL should be rejected.'); + }, 'Script URL including URL-encoded slash'); + + promise_test(function(t) { + var script = 'resources%2Fempty-worker.js'; + var scope = 'resources/scope/encoded-slash-in-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded slash in the script URL should be rejected.'); + }, 'Script URL including uppercase URL-encoded slash'); + + promise_test(function(t) { + var script = 'resources%5cempty-worker.js'; + var scope = 'resources/scope/encoded-slash-in-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded backslash in the script URL should be rejected.'); + }, 'Script URL including URL-encoded backslash'); + + promise_test(function(t) { + var script = 'resources%5Cempty-worker.js'; + var scope = 'resources/scope/encoded-slash-in-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded backslash in the script URL should be rejected.'); + }, 'Script URL including uppercase URL-encoded backslash'); + + promise_test(function(t) { + var script = 'data:application/javascript,'; + var scope = 'resources/scope/data-url-in-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Data URLs should not be registered as service workers.'); + }, 'Script URL is a data URL'); + + promise_test(function(t) { + var script = 'data:application/javascript,'; + var scope = new URL('resources/scope/data-url-in-script-url', location); + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Data URLs should not be registered as service workers.'); + }, 'Script URL is a data URL and scope URL is not relative'); + + promise_test(function(t) { + var script = 'resources/././empty-worker.js'; + var scope = 'resources/scope/parent-reference-in-script-url'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + get_newest_worker(registration).scriptURL, + normalizeURL('resources/empty-worker.js'), + 'Script URL including self-reference should be normalized.'); + return registration.unregister(); + }); + }, 'Script URL including self-reference'); + + promise_test(function(t) { + var script = 'resources/../resources/empty-worker.js'; + var scope = 'resources/scope/parent-reference-in-script-url'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + get_newest_worker(registration).scriptURL, + normalizeURL('resources/empty-worker.js'), + 'Script URL including parent-reference should be normalized.'); + return registration.unregister(); + }); + }, 'Script URL including parent-reference'); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script.js new file mode 100644 index 0000000000..e5bdaf4291 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script.js @@ -0,0 +1,121 @@ +// Registration tests that mostly exercise the service worker script contents or +// response. +function registration_tests_script(register_method, type) { + promise_test(function(t) { + var script = 'resources/invalid-chunked-encoding.py'; + var scope = 'resources/scope/invalid-chunked-encoding/'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of invalid chunked encoding script should fail.'); + }, 'Registering invalid chunked encoding script'); + + promise_test(function(t) { + var script = 'resources/invalid-chunked-encoding-with-flush.py'; + var scope = 'resources/scope/invalid-chunked-encoding-with-flush/'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of invalid chunked encoding script should fail.'); + }, 'Registering invalid chunked encoding script with flush'); + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?parse-error'; + var scope = 'resources/scope/parse-error'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script including parse error should fail.'); + }, 'Registering script including parse error'); + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?undefined-error'; + var scope = 'resources/scope/undefined-error'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script including undefined error should fail.'); + }, 'Registering script including undefined error'); + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?uncaught-exception'; + var scope = 'resources/scope/uncaught-exception'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script including uncaught exception should fail.'); + }, 'Registering script including uncaught exception'); + + if (type === 'classic') { + promise_test(function(t) { + var script = 'resources/malformed-worker.py?import-malformed-script'; + var scope = 'resources/scope/import-malformed-script'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script importing malformed script should fail.'); + }, 'Registering script importing malformed script'); + } + + if (type === 'module') { + promise_test(function(t) { + var script = 'resources/malformed-worker.py?top-level-await'; + var scope = 'resources/scope/top-level-await'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script with top-level await should fail.'); + }, 'Registering script with top-level await'); + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?instantiation-error'; + var scope = 'resources/scope/instantiation-error'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script with module instantiation error should fail.'); + }, 'Registering script with module instantiation error'); + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?instantiation-error-and-top-level-await'; + var scope = 'resources/scope/instantiation-error-and-top-level-await'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script with module instantiation error and top-level await should fail.'); + }, 'Registering script with module instantiation error and top-level await'); + } + + promise_test(function(t) { + var script = 'resources/no-such-worker.js'; + var scope = 'resources/scope/no-such-worker'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of non-existent script should fail.'); + }, 'Registering non-existent script'); + + if (type === 'classic') { + promise_test(function(t) { + var script = 'resources/malformed-worker.py?import-no-such-script'; + var scope = 'resources/scope/import-no-such-script'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script importing non-existent script should fail.'); + }, 'Registering script importing non-existent script'); + } + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?caught-exception'; + var scope = 'resources/scope/caught-exception'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_true( + registration instanceof ServiceWorkerRegistration, + 'Successfully registered.'); + return registration.unregister(); + }); + }, 'Registering script including caught exception'); + +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-security-error.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-security-error.js new file mode 100644 index 0000000000..c45fbd4578 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-security-error.js @@ -0,0 +1,78 @@ +// Registration tests that mostly exercise SecurityError cases. +function registration_tests_security_error(register_method) { + promise_test(function(t) { + var script = 'resources/registration-worker.js'; + var scope = 'resources'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registering same scope as the script directory without the last ' + + 'slash should fail with SecurityError.'); + }, 'Registering same scope as the script directory without the last slash'); + + promise_test(function(t) { + var script = 'resources/registration-worker.js'; + var scope = 'different-directory/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration scope outside the script directory should fail ' + + 'with SecurityError.'); + }, 'Registration scope outside the script directory'); + + promise_test(function(t) { + var script = 'resources/registration-worker.js'; + var scope = 'http://example.com/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration scope outside domain should fail with SecurityError.'); + }, 'Registering scope outside domain'); + + promise_test(function(t) { + var script = 'http://example.com/worker.js'; + var scope = 'http://example.com/scope/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration script outside domain should fail with SecurityError.'); + }, 'Registering script outside domain'); + + promise_test(function(t) { + var script = 'resources/redirect.py?Redirect=' + + encodeURIComponent('/resources/registration-worker.js'); + var scope = 'resources/scope/redirect/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration of redirected script should fail.'); + }, 'Registering redirected script'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/../scope/parent-reference-in-scope'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Scope not under the script directory should be rejected.'); + }, 'Scope including parent-reference and not under the script directory'); + + promise_test(function(t) { + var script = 'resources////empty-worker.js'; + var scope = 'resources/scope/consecutive-slashes-in-script-url'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Consecutive slashes in the script url should not be unified.'); + }, 'Script URL including consecutive slashes'); + + promise_test(function(t) { + var script = 'filesystem:' + normalizeURL('resources/empty-worker.js'); + var scope = 'resources/scope/filesystem-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registering a script which has same-origin filesystem: URL should ' + + 'fail with TypeError.'); + }, 'Script URL is same-origin filesystem: URL'); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/registration-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-worker.js new file mode 100644 index 0000000000..44d1d2774a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/registration-worker.js @@ -0,0 +1 @@ +// empty for now diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/reject-install-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/reject-install-worker.js new file mode 100644 index 0000000000..41f07fd5db --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/reject-install-worker.js @@ -0,0 +1,3 @@ +self.oninstall = function(event) { + event.waitUntil(Promise.reject()); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/reply-to-message.html b/testing/web-platform/tests/service-workers/service-worker/resources/reply-to-message.html new file mode 100644 index 0000000000..8a70e2ad93 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/reply-to-message.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<script> +window.addEventListener('message', event => { + var port = event.ports[0]; + port.postMessage(event.data); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/request-end-to-end-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/request-end-to-end-worker.js new file mode 100644 index 0000000000..6bd2b72137 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/request-end-to-end-worker.js @@ -0,0 +1,34 @@ +'use strict'; + +onfetch = function(e) { + var headers = {}; + for (var header of e.request.headers) { + var key = header[0], value = header[1]; + headers[key] = value; + } + var append_header_error = ''; + try { + e.request.headers.append('Test-Header', 'TestValue'); + } catch (error) { + append_header_error = error.name; + } + + var request_construct_error = ''; + try { + new Request(e.request, {method: 'GET'}); + } catch (error) { + request_construct_error = error.name; + } + + e.respondWith(new Response(JSON.stringify({ + url: e.request.url, + method: e.request.method, + referrer: e.request.referrer, + headers: headers, + mode: e.request.mode, + credentials: e.request.credentials, + redirect: e.request.redirect, + append_header_error: append_header_error, + request_construct_error: request_construct_error + }))); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/request-headers.py b/testing/web-platform/tests/service-workers/service-worker/resources/request-headers.py new file mode 100644 index 0000000000..6ab148e22e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/request-headers.py @@ -0,0 +1,8 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()} + + return [(b"Content-Type", b"application/json")], json.dumps(data) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html b/testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html new file mode 100644 index 0000000000..384c29b536 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<script src="empty.js"></script> +<script src="sample.js"></script> +<script src="redirect.py?Redirect=empty.js"></script> +<img src="square.png"> +<img src="https://{{hosts[alt][]}}:{{ports[https][0]}}/service-workers/service-worker/resources/square.png"> +<img src="missing.jpg"> +<img src="https://{{hosts[alt][]}}:{{ports[https][0]}}/service-workers/service-worker/resources/missing.jpg"> +<img src='missing.jpg?SWRespondsWithFetch'> +<script src='empty-worker.js'></script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-worker.js new file mode 100644 index 0000000000..b74e8cd6a2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', function(event) { + if (event.request.url.indexOf('sample.js') != -1) { + event.respondWith(new Promise(resolve => { + // Slightly delay the response so we ensure we get a non-zero + // duration. + setTimeout(_ => resolve(new Response('// Empty javascript')), 50); + })); + } + else if (event.request.url.indexOf('missing.jpg?SWRespondsWithFetch') != -1) { + event.respondWith(fetch('sample.txt?SWFetched')); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/respond-then-throw-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/respond-then-throw-worker.js new file mode 100644 index 0000000000..adb48de69e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/respond-then-throw-worker.js @@ -0,0 +1,40 @@ +var syncport = null; + +self.addEventListener('message', function(e) { + if ('port' in e.data) { + if (syncport) { + syncport(e.data.port); + } else { + syncport = e.data.port; + } + } +}); + +function sync() { + return new Promise(function(resolve) { + if (syncport) { + resolve(syncport); + } else { + syncport = resolve; + } + }).then(function(port) { + port.postMessage('SYNC'); + return new Promise(function(resolve) { + port.onmessage = function(e) { + if (e.data === 'ACK') { + resolve(); + } + } + }); + }); +} + + +self.addEventListener('fetch', function(event) { + // In Firefox the result would depend on a race between fetch handling + // and exception handling code. On the assumption that this might be a common + // design error, we explicitly allow the exception to be handled first. + event.respondWith(sync().then(() => new Response('intercepted'))); + + throw("error"); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html new file mode 100644 index 0000000000..7be3148794 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html @@ -0,0 +1,20 @@ +<script> +var callback; + +// Creates a <script> element with |url| source, and returns a promise for the +// result of the executed script. Uses JSONP because some responses to |url| +// are opaque so their body cannot be tested directly. +function getJSONP(url) { + var sc = document.createElement('script'); + sc.src = url; + var promise = new Promise(function(resolve, reject) { + // This callback function is called by appending a script element. + callback = resolve; + sc.addEventListener( + 'error', + function() { reject('Failed to load url:' + url); }); + }); + document.body.appendChild(sc); + return promise; +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js new file mode 100644 index 0000000000..c602109bc6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js @@ -0,0 +1,93 @@ +importScripts('/common/get-host-info.sub.js'); +importScripts('test-helpers.sub.js'); + +function getQueryParams(url) { + var search = (new URL(url)).search; + if (!search) { + return {}; + } + var ret = {}; + var params = search.substring(1).split('&'); + params.forEach(function(param) { + var element = param.split('='); + ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]); + }); + return ret; +} + +function createResponse(params) { + if (params['type'] == 'basic') { + return fetch('respond-with-body-accessed-response.jsonp'); + } + if (params['type'] == 'opaque') { + return fetch(get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() + + 'respond-with-body-accessed-response.jsonp', + {mode: 'no-cors'}); + } + if (params['type'] == 'default') { + return Promise.resolve(new Response('callback(\'OK\');')); + } + + return Promise.reject(new Error('unexpected type :' + params['type'])); +} + +function cloneResponseIfNeeded(params, response) { + if (params['clone'] == '1') { + return response.clone(); + } else if (params['clone'] == '2') { + response.clone(); + return response; + } + return response; +} + +function passThroughCacheIfNeeded(params, request, response) { + return new Promise(function(resolve) { + if (params['passThroughCache'] == 'true') { + var cache_name = request.url; + var cache; + self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function(c) { + cache = c; + return cache.put(request, response); + }) + .then(function() { + return cache.match(request.url); + }) + .then(function(res) { + // Touch .body here to test the behavior after touching it. + res.body; + resolve(res); + }); + } else { + resolve(response); + } + }) +} + +self.addEventListener('fetch', function(event) { + if (event.request.url.indexOf('TestRequest') == -1) { + return; + } + var params = getQueryParams(event.request.url); + event.respondWith( + createResponse(params) + .then(function(response) { + // Touch .body here to test the behavior after touching it. + response.body; + return cloneResponseIfNeeded(params, response); + }) + .then(function(response) { + // Touch .body here to test the behavior after touching it. + response.body; + return passThroughCacheIfNeeded(params, event.request, response); + }) + .then(function(response) { + // Touch .body here to test the behavior after touching it. + response.body; + return response; + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp b/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp new file mode 100644 index 0000000000..b9c28f51f9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp @@ -0,0 +1 @@ +callback('OK'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sample-worker-interceptor.js b/testing/web-platform/tests/service-workers/service-worker/resources/sample-worker-interceptor.js new file mode 100644 index 0000000000..c06f8dd77b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/sample-worker-interceptor.js @@ -0,0 +1,62 @@ +importScripts('/common/get-host-info.sub.js'); + +const text = 'worker loading intercepted by service worker'; +const dedicated_worker_script = `postMessage('${text}');`; +const shared_worker_script = + `onconnect = evt => evt.ports[0].postMessage('${text}');`; + +let source; +let resolveDone; +let done = new Promise(resolve => resolveDone = resolve); + +// The page messages this worker to ask for the result. Keep the worker alive +// via waitUntil() until the result is sent. +self.addEventListener('message', event => { + source = event.data.port; + source.postMessage({id: event.source.id}); + source.onmessage = resolveDone; + event.waitUntil(done); +}); + +self.onfetch = event => { + const url = event.request.url; + const destination = event.request.destination; + + if (source) + source.postMessage({clientId:event.clientId, resultingClientId: event.resultingClientId}); + + // Request handler for a synthesized response. + if (url.indexOf('synthesized') != -1) { + let script_headers = new Headers({ "Content-Type": "text/javascript" }); + if (destination === 'worker') + event.respondWith(new Response(dedicated_worker_script, { 'headers': script_headers })); + else if (destination === 'sharedworker') + event.respondWith(new Response(shared_worker_script, { 'headers': script_headers })); + else + event.respondWith(new Response('Unexpected request! ' + destination)); + return; + } + + // Request handler for a same-origin response. + if (url.indexOf('same-origin') != -1) { + event.respondWith(fetch('postmessage-on-load-worker.js')); + return; + } + + // Request handler for a cross-origin response. + if (url.indexOf('cors') != -1) { + const filename = 'postmessage-on-load-worker.js'; + const path = (new URL(filename, self.location)).pathname; + let new_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + path; + let mode; + if (url.indexOf('no-cors') != -1) { + // Test no-cors mode. + mode = 'no-cors'; + } else { + // Test cors mode. + new_url += '?pipe=header(Access-Control-Allow-Origin,*)'; + mode = 'cors'; + } + event.respondWith(fetch(new_url, { mode: mode })); + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sample.html b/testing/web-platform/tests/service-workers/service-worker/resources/sample.html new file mode 100644 index 0000000000..12a179980d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/sample.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<body>Hello world diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sample.js b/testing/web-platform/tests/service-workers/service-worker/resources/sample.js new file mode 100644 index 0000000000..b8889db05d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/sample.js @@ -0,0 +1 @@ +var hello = "world"; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sample.txt b/testing/web-platform/tests/service-workers/service-worker/resources/sample.txt new file mode 100644 index 0000000000..802992c422 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/sample.txt @@ -0,0 +1 @@ +Hello world diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html new file mode 100644 index 0000000000..239fa73303 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html @@ -0,0 +1,63 @@ +<script> +function with_iframe(url) { + return new Promise(resolve => { + let frame = document.createElement('iframe'); + frame.src = url; + frame.onload = () => { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +function with_sandboxed_iframe(url, sandbox) { + return new Promise(resolve => { + let frame = document.createElement('iframe'); + frame.sandbox = sandbox; + frame.src = url; + frame.onload = () => { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +function fetch_from_worker(url) { + return new Promise(resolve => { + let blob = new Blob([ + `fetch('${url}', {mode: 'no-cors'})` + + " .then(() => { self.postMessage('OK'); });"]); + let worker_url = URL.createObjectURL(blob); + let worker = new Worker(worker_url); + worker.onmessage = resolve; + }); +} + +function run_test(type) { + const base_path = location.href; + switch (type) { + case 'fetch': + return fetch(`${base_path}&test=fetch`, {mode: 'no-cors'}); + case 'fetch-from-worker': + return fetch_from_worker(`${base_path}&test=fetch-from-worker`); + case 'iframe': + return with_iframe(`${base_path}&test=iframe`); + case 'sandboxed-iframe': + return with_sandboxed_iframe(`${base_path}&test=sandboxed-iframe`, + "allow-scripts"); + case 'sandboxed-iframe-same-origin': + return with_sandboxed_iframe( + `${base_path}&test=sandboxed-iframe-same-origin`, + "allow-scripts allow-same-origin"); + default: + return Promise.reject(`Unknown type: ${type}`); + } +} + +window.onmessage = event => { + let id = event.data['id']; + run_test(event.data['type']) + .then(() => { + window.top.postMessage({id: id, result: 'done'}, '*'); + }) + .catch(e => { + window.top.postMessage({id: id, result: 'error: ' + e.toString()}, '*'); + }); +}; +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py new file mode 100644 index 0000000000..0281b6c275 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py @@ -0,0 +1,18 @@ +import os.path + +from wptserve.utils import isomorphic_decode + +def main(request, response): + header = [(b'Content-Type', b'text/html')] + if b'test' in request.GET: + with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), u'sample.js'), u'r') as f: + body = f.read() + return (header, body) + + if b'sandbox' in request.GET: + header.append((b'Content-Security-Policy', + b'sandbox %s' % request.GET[b'sandbox'])) + with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), + u'sandboxed-iframe-fetch-event-iframe.html'), u'r') as f: + body = f.read() + return (header, body) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js new file mode 100644 index 0000000000..4035a8b19b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js @@ -0,0 +1,20 @@ +var requests = []; + +self.addEventListener('message', function(event) { + event.waitUntil(self.clients.matchAll() + .then(function(clients) { + var client_urls = []; + for(var client of clients){ + client_urls.push(client.url); + } + client_urls = client_urls.sort(); + event.data.port.postMessage( + {clients: client_urls, requests: requests}); + requests = []; + })); + }); + +self.addEventListener('fetch', function(event) { + requests.push(event.request.url); + event.respondWith(fetch(event.request)); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html new file mode 100644 index 0000000000..1d682e47ef --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html @@ -0,0 +1,25 @@ +<script> +window.onmessage = function(e) { + const id = e.data['id']; + try { + var sw = window.navigator.serviceWorker; + } catch (e) { + window.top.postMessage({ + id: id, + result: 'navigator.serviceWorker failed: ' + e.name + }, '*'); + return; + } + + window.navigator.serviceWorker.getRegistration() + .then(function() { + window.top.postMessage({id: id, result:'ok'}, '*'); + }) + .catch(function(e) { + window.top.postMessage({ + id: id, + result: 'getRegistration() failed: ' + e.name + }, '*'); + }); +}; +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js b/testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js new file mode 100644 index 0000000000..ae681ba30e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js @@ -0,0 +1 @@ +import * as module from './redirect.py?Redirect=/service-workers/service-worker/resources/scope2/imported-module-script.js'; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js b/testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js new file mode 100644 index 0000000000..e28505249c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js @@ -0,0 +1 @@ +import * as module from '../scope2/imported-module-script.js'; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/scope1/redirect.py b/testing/web-platform/tests/service-workers/service-worker/resources/scope1/redirect.py new file mode 100644 index 0000000000..bb4c874aac --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/scope1/redirect.py @@ -0,0 +1,6 @@ +import os +import imp +# Use the file from the parent directory. +mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)), + os.path.basename(__file__))) +main = mod.main diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py new file mode 100644 index 0000000000..5f785b5cc2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py @@ -0,0 +1,6 @@ +def main(req, res): + return ([ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')], + b'echo_output = "%s (scope2/)";\n' % req.GET[b'msg']) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/scope2/imported-module-script.js b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/imported-module-script.js new file mode 100644 index 0000000000..a18e704a3c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/imported-module-script.js @@ -0,0 +1,4 @@ +export const imported = 'A module script.'; +onmessage = msg => { + msg.source.postMessage('pong'); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/scope2/simple.txt b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/simple.txt new file mode 100644 index 0000000000..cd876676e8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/simple.txt @@ -0,0 +1 @@ +a simple text file (scope2/) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py new file mode 100644 index 0000000000..bb4c874aac --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py @@ -0,0 +1,6 @@ +import os +import imp +# Use the file from the parent directory. +mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)), + os.path.basename(__file__))) +main = mod.main diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/secure-context-service-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/secure-context-service-worker.js new file mode 100644 index 0000000000..5ba99f0753 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/secure-context-service-worker.js @@ -0,0 +1,21 @@ +self.addEventListener('fetch', event => { + let url = new URL(event.request.url); + if (url.pathname.indexOf('sender.html') != -1) { + event.respondWith(new Response( + "<script>window.parent.postMessage('interception', '*');</script>", + { headers: { 'Content-Type': 'text/html'} } + )); + } else if (url.pathname.indexOf('report') != -1) { + self.clients.matchAll().then(clients => { + for (client of clients) { + client.postMessage(url.searchParams.get('result')); + } + }); + event.respondWith( + new Response( + '<script>window.close()</script>', + { headers: { 'Content-Type': 'text/html'} } + ) + ); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/sender.html b/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/sender.html new file mode 100644 index 0000000000..05e58822a8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/sender.html @@ -0,0 +1 @@ +<script>window.parent.postMessage('network', '*');</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/window.html b/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/window.html new file mode 100644 index 0000000000..071a507cb3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/window.html @@ -0,0 +1,15 @@ +<body> +<script src="/common/get-host-info.sub.js"></script> +<script src="../test-helpers.sub.js"></script> +<script> +const HTTPS_PREFIX = get_host_info().HTTPS_ORIGIN + base_path(); + +window.onmessage = event => { + window.location = HTTPS_PREFIX + 'report?result=' + event.data; +}; + +const frame = document.createElement('iframe'); +frame.src = HTTPS_PREFIX + 'sender.html'; +document.body.appendChild(frame); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-csp-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-csp-worker.py new file mode 100644 index 0000000000..35a46964a7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-csp-worker.py @@ -0,0 +1,183 @@ +bodyDefault = b''' +importScripts('worker-testharness.js'); +importScripts('test-helpers.sub.js'); +importScripts('/common/get-host-info.sub.js'); + +var host_info = get_host_info(); + +test(function() { + var import_script_failed = false; + try { + importScripts(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'empty.js'); + } catch(e) { + import_script_failed = true; + } + assert_true(import_script_failed, + 'Importing the other origins script should fail.'); + }, 'importScripts test for default-src'); + +test(function() { + assert_throws_js(EvalError, + function() { eval('1 + 1'); }, + 'eval() should throw EvalError.') + assert_throws_js(EvalError, + function() { new Function('1 + 1'); }, + 'new Function() should throw EvalError.') + }, 'eval test for default-src'); + +async_test(function(t) { + fetch(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?ACAOrigin=*', + {mode: 'cors'}) + .then(function(response){ + assert_unreached('fetch should fail.'); + }, function(){ + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Fetch test for default-src'); + +async_test(function(t) { + var REDIRECT_URL = host_info.HTTPS_ORIGIN + + base_path() + 'redirect.py?Redirect='; + var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?' + fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'), + {mode: 'cors'}) + .then(function(response){ + assert_unreached('Redirected fetch should fail.'); + }, function(){ + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Redirected fetch test for default-src');''' + +bodyScript = b''' +importScripts('worker-testharness.js'); +importScripts('test-helpers.sub.js'); +importScripts('/common/get-host-info.sub.js'); + +var host_info = get_host_info(); + +test(function() { + var import_script_failed = false; + try { + importScripts(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'empty.js'); + } catch(e) { + import_script_failed = true; + } + assert_true(import_script_failed, + 'Importing the other origins script should fail.'); + }, 'importScripts test for script-src'); + +test(function() { + assert_throws_js(EvalError, + function() { eval('1 + 1'); }, + 'eval() should throw EvalError.') + assert_throws_js(EvalError, + function() { new Function('1 + 1'); }, + 'new Function() should throw EvalError.') + }, 'eval test for script-src'); + +async_test(function(t) { + fetch(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?ACAOrigin=*', + {mode: 'cors'}) + .then(function(response){ + t.done(); + }, function(){ + assert_unreached('fetch should not fail.'); + }) + .catch(unreached_rejection(t)); + }, 'Fetch test for script-src'); + +async_test(function(t) { + var REDIRECT_URL = host_info.HTTPS_ORIGIN + + base_path() + 'redirect.py?Redirect='; + var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?' + fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'), + {mode: 'cors'}) + .then(function(response){ + t.done(); + }, function(){ + assert_unreached('Redirected fetch should not fail.'); + }) + .catch(unreached_rejection(t)); + }, 'Redirected fetch test for script-src');''' + +bodyConnect = b''' +importScripts('worker-testharness.js'); +importScripts('test-helpers.sub.js'); +importScripts('/common/get-host-info.sub.js'); + +var host_info = get_host_info(); + +test(function() { + var import_script_failed = false; + try { + importScripts(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'empty.js'); + } catch(e) { + import_script_failed = true; + } + assert_false(import_script_failed, + 'Importing the other origins script should not fail.'); + }, 'importScripts test for connect-src'); + +test(function() { + var eval_failed = false; + try { + eval('1 + 1'); + new Function('1 + 1'); + } catch(e) { + eval_failed = true; + } + assert_false(eval_failed, + 'connect-src without unsafe-eval should not block eval().'); + }, 'eval test for connect-src'); + +async_test(function(t) { + fetch(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?ACAOrigin=*', + {mode: 'cors'}) + .then(function(response){ + assert_unreached('fetch should fail.'); + }, function(){ + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Fetch test for connect-src'); + +async_test(function(t) { + var REDIRECT_URL = host_info.HTTPS_ORIGIN + + base_path() + 'redirect.py?Redirect='; + var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?' + fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'), + {mode: 'cors'}) + .then(function(response){ + assert_unreached('Redirected fetch should fail.'); + }, function(){ + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Redirected fetch test for connect-src');''' + +def main(request, response): + headers = [] + headers.append((b'Content-Type', b'application/javascript')) + directive = request.GET[b'directive'] + body = b'ERROR: Unknown directive' + if directive == b'default': + headers.append((b'Content-Security-Policy', b"default-src 'self'")) + body = bodyDefault + elif directive == b'script': + headers.append((b'Content-Security-Policy', b"script-src 'self'")) + body = bodyScript + elif directive == b'connect': + headers.append((b'Content-Security-Policy', b"connect-src 'self'")) + body = bodyConnect + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-header.py b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-header.py new file mode 100644 index 0000000000..d64a9d2494 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-header.py @@ -0,0 +1,20 @@ +def main(request, response): + service_worker_header = request.headers.get(b'service-worker') + + if b'header' in request.GET and service_worker_header != b'script': + return 400, [(b'Content-Type', b'text/plain')], b'Bad Request' + + if b'no-header' in request.GET and service_worker_header == b'script': + return 400, [(b'Content-Type', b'text/plain')], b'Bad Request' + + # no-cache itself to ensure the user agent finds a new version for each + # update. + headers = [(b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')] + body = b'/* This is a service worker script */\n' + + if b'import' in request.GET: + body += b"importScripts('%s');" % request.GET[b'import'] + + return 200, headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js new file mode 100644 index 0000000000..680e07ff58 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js @@ -0,0 +1 @@ +import('./service-worker-interception-network-worker.js'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js new file mode 100644 index 0000000000..5ff3900101 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js @@ -0,0 +1 @@ +postMessage('LOADED_FROM_NETWORK'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js new file mode 100644 index 0000000000..6b43a37696 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js @@ -0,0 +1,9 @@ +const kURL = '/service-worker-interception-network-worker.js'; +const kScript = 'postMessage("LOADED_FROM_SERVICE_WORKER")'; +const kHeaders = [['content-type', 'text/javascript']]; + +self.addEventListener('fetch', e => { + // Serve a generated response for kURL. + if (e.request.url.indexOf(kURL) != -1) + e.respondWith(new Response(kScript, { headers: kHeaders })); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js new file mode 100644 index 0000000000..e570958701 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js @@ -0,0 +1 @@ +import './service-worker-interception-network-worker.js'; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/silence.oga b/testing/web-platform/tests/service-workers/service-worker/resources/silence.oga Binary files differnew file mode 100644 index 0000000000..af59188043 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/silence.oga diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js new file mode 100644 index 0000000000..f8b5f8c5cb --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js @@ -0,0 +1,5 @@ +self.onfetch = function(event) { + if (event.request.url.indexOf('simple') != -1) + event.respondWith( + new Response(new Blob(['intercepted by service worker']))); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers b/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers new file mode 100644 index 0000000000..a17a9a3a12 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers @@ -0,0 +1 @@ +Content-Type: application/javascript diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/simple.html b/testing/web-platform/tests/service-workers/service-worker/resources/simple.html new file mode 100644 index 0000000000..0c3e3e7870 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/simple.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<title>Simple</title> +Here's a simple html file. diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/simple.txt b/testing/web-platform/tests/service-workers/service-worker/resources/simple.txt new file mode 100644 index 0000000000..9e3cb91fb9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/simple.txt @@ -0,0 +1 @@ +a simple text file diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js new file mode 100644 index 0000000000..6f7008bddc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js @@ -0,0 +1,33 @@ +var saw_activate_event = false + +self.addEventListener('activate', function() { + saw_activate_event = true; + }); + +self.addEventListener('message', function(event) { + var port = event.data.port; + event.waitUntil(self.skipWaiting() + .then(function(result) { + if (result !== undefined) { + port.postMessage('FAIL: Promise should be resolved with undefined'); + return; + } + + if (!saw_activate_event) { + port.postMessage( + 'FAIL: Promise should be resolved after activate event is dispatched'); + return; + } + + if (self.registration.active.state !== 'activating') { + port.postMessage( + 'FAIL: Promise should be resolved before ServiceWorker#state is set to activated'); + return; + } + + port.postMessage('PASS'); + }) + .catch(function(e) { + port.postMessage('FAIL: unexpected exception: ' + e); + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-worker.js new file mode 100644 index 0000000000..3fc1d1e237 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-worker.js @@ -0,0 +1,21 @@ +importScripts('worker-testharness.js'); + +promise_test(function() { + return skipWaiting() + .then(function(result) { + assert_equals(result, undefined, + 'Promise should be resolved with undefined'); + }) + .then(function() { + var promises = []; + for (var i = 0; i < 8; ++i) + promises.push(self.skipWaiting()); + return Promise.all(promises); + }) + .then(function(results) { + results.forEach(function(r) { + assert_equals(r, undefined, + 'Promises should be resolved with undefined'); + }); + }); + }, 'skipWaiting'); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/square.png b/testing/web-platform/tests/service-workers/service-worker/resources/square.png Binary files differnew file mode 100644 index 0000000000..01c9666a8d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/square.png diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/square.png.sub.headers b/testing/web-platform/tests/service-workers/service-worker/resources/square.png.sub.headers new file mode 100644 index 0000000000..7341132745 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/square.png.sub.headers @@ -0,0 +1,2 @@ +Content-Type: image/png +Access-Control-Allow-Origin: * diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/stalling-service-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/stalling-service-worker.js new file mode 100644 index 0000000000..fdf1e6cac0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/stalling-service-worker.js @@ -0,0 +1,54 @@ +async function post_message_to_client(role, message, ports) { + (await clients.matchAll()).forEach(client => { + if (new URL(client.url).searchParams.get('role') === role) { + client.postMessage(message, ports); + } + }); +} + +async function post_message_to_child(message, ports) { + await post_message_to_client('child', message, ports); +} + +function ping_message(data) { + return { type: 'ping', data }; +} + +self.onmessage = event => { + const message = ping_message(event.data); + post_message_to_child(message); + post_message_to_parent(message); +} + +async function post_message_to_parent(message, ports) { + await post_message_to_client('parent', message, ports); +} + +function fetch_message(key) { + return { type: 'fetch', key }; +} + +// Send a message to the parent along with a MessagePort to respond +// with. +function report_fetch_request(key) { + const channel = new MessageChannel(); + const reply = new Promise(resolve => { + channel.port1.onmessage = resolve; + }).then(event => event.data); + return post_message_to_parent(fetch_message(key), [channel.port2]).then(() => reply); +} + +function respond_with_script(script) { + return new Response(new Blob(script, { type: 'text/javascript' })); +} + +// Whenever a controlled document requests a URL with a 'key' search +// parameter we report the request to the parent frame and wait for +// a response. The content of the response is then used to respond to +// the fetch request. +addEventListener('fetch', event => { + let key = new URL(event.request.url).searchParams.get('key'); + if (key) { + event.respondWith(report_fetch_request(key).then(respond_with_script)); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/subdir/blank.html b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/blank.html new file mode 100644 index 0000000000..a3c3a4689a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/blank.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<title>Empty doc</title> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py new file mode 100644 index 0000000000..f745d7ae46 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py @@ -0,0 +1,6 @@ +def main(req, res): + return ([ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')], + b'echo_output = "%s (subdir/)";\n' % req.GET[b'msg']) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/subdir/simple.txt b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/simple.txt new file mode 100644 index 0000000000..86bcdd7dc5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/simple.txt @@ -0,0 +1 @@ +a simple text file (subdir/) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py new file mode 100644 index 0000000000..bb4c874aac --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py @@ -0,0 +1,6 @@ +import os +import imp +# Use the file from the parent directory. +mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)), + os.path.basename(__file__))) +main = mod.main diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/success.py b/testing/web-platform/tests/service-workers/service-worker/resources/success.py new file mode 100644 index 0000000000..a0269918ee --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/success.py @@ -0,0 +1,8 @@ +def main(request, response): + headers = [] + + if b"ACAOrigin" in request.GET: + for item in request.GET[b"ACAOrigin"].split(b","): + headers.append((b"Access-Control-Allow-Origin", item)) + + return headers, b"{ \"result\": \"success\" }" diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html new file mode 100644 index 0000000000..59fb524049 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<img src="/images/green.svg"> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001.html b/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001.html new file mode 100644 index 0000000000..9a93d3b370 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Green svg box reference file</title> +<p>Pass if you see a green box below.</p> +<iframe src="svg-target-reftest-001-frame.html"> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html new file mode 100644 index 0000000000..d6fc820f78 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<img src="/images/colors.svg#green"> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/test-helpers.sub.js b/testing/web-platform/tests/service-workers/service-worker/resources/test-helpers.sub.js new file mode 100644 index 0000000000..74301523e7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/test-helpers.sub.js @@ -0,0 +1,300 @@ +// 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(); +} + diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.js new file mode 100644 index 0000000000..566e2e9984 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.js @@ -0,0 +1,10 @@ +// Add a unique UUID per request to induce service worker script update. +// Time stamp: %UUID% + +// The server injects the request headers here as a JSON string. +const headersAsJson = `%HEADERS%`; +const headers = JSON.parse(headersAsJson); + +self.addEventListener('message', async (e) => { + e.source.postMessage(headers); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.py new file mode 100644 index 0000000000..78a93356b7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.py @@ -0,0 +1,21 @@ +import json +import os +import uuid +import sys + +from wptserve.utils import isomorphic_decode + +def main(request, response): + path = os.path.join(os.path.dirname(isomorphic_decode(__file__)), + u"test-request-headers-worker.js") + body = open(path, u"rb").read() + + data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()} + body = body.replace(b"%HEADERS%", json.dumps(data).encode("utf-8")) + body = body.replace(b"%UUID%", str(uuid.uuid4()).encode("utf-8")) + + headers = [] + headers.append((b"ETag", b"etag")) + headers.append((b"Content-Type", b'text/javascript')) + + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.js new file mode 100644 index 0000000000..566e2e9984 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.js @@ -0,0 +1,10 @@ +// Add a unique UUID per request to induce service worker script update. +// Time stamp: %UUID% + +// The server injects the request headers here as a JSON string. +const headersAsJson = `%HEADERS%`; +const headers = JSON.parse(headersAsJson); + +self.addEventListener('message', async (e) => { + e.source.postMessage(headers); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.py new file mode 100644 index 0000000000..8449841a99 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.py @@ -0,0 +1,22 @@ +import json +import os +import uuid +import sys + +from wptserve.utils import isomorphic_decode + +def main(request, response): + path = os.path.join(os.path.dirname(isomorphic_decode(__file__)), + u"test-request-mode-worker.js") + body = open(path, u"rb").read() + + data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()} + + body = body.replace(b"%HEADERS%", json.dumps(data).encode("utf-8")) + body = body.replace(b"%UUID%", str(uuid.uuid4()).encode("utf-8")) + + headers = [] + headers.append((b"ETag", b"etag")) + headers.append((b"Content-Type", b'text/javascript')) + + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/testharness-helpers.js b/testing/web-platform/tests/service-workers/service-worker/resources/testharness-helpers.js new file mode 100644 index 0000000000..b1a5b960e0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/testharness-helpers.js @@ -0,0 +1,136 @@ +/* + * testharness-helpers contains various useful extensions to testharness.js to + * allow them to be used across multiple tests before they have been + * upstreamed. This file is intended to be usable from both document and worker + * environments, so code should for example not rely on the DOM. + */ + +// Asserts that two objects |actual| and |expected| are weakly equal under the +// following definition: +// +// |a| and |b| are weakly equal if any of the following are true: +// 1. If |a| is not an 'object', and |a| === |b|. +// 2. If |a| is an 'object', and all of the following are true: +// 2.1 |a.p| is weakly equal to |b.p| for all own properties |p| of |a|. +// 2.2 Every own property of |b| is an own property of |a|. +// +// This is a replacement for the the version of assert_object_equals() in +// testharness.js. The latter doesn't handle own properties correctly. I.e. if +// |a.p| is not an own property, it still requires that |b.p| be an own +// property. +// +// Note that |actual| must not contain cyclic references. +self.assert_object_equals = function(actual, expected, description) { + var object_stack = []; + + function _is_equal(actual, expected, prefix) { + if (typeof actual !== 'object') { + assert_equals(actual, expected, prefix); + return; + } + assert_equals(typeof expected, 'object', prefix); + assert_equals(object_stack.indexOf(actual), -1, + prefix + ' must not contain cyclic references.'); + + object_stack.push(actual); + + Object.getOwnPropertyNames(expected).forEach(function(property) { + assert_own_property(actual, property, prefix); + _is_equal(actual[property], expected[property], + prefix + '.' + property); + }); + Object.getOwnPropertyNames(actual).forEach(function(property) { + assert_own_property(expected, property, prefix); + }); + + object_stack.pop(); + } + + function _brand(object) { + return Object.prototype.toString.call(object).match(/^\[object (.*)\]$/)[1]; + } + + _is_equal(actual, expected, + (description ? description + ': ' : '') + _brand(expected)); +}; + +// Equivalent to assert_in_array, but uses a weaker equivalence relation +// (assert_object_equals) than '==='. +function assert_object_in_array(actual, expected_array, description) { + assert_true(expected_array.some(function(element) { + try { + assert_object_equals(actual, element); + return true; + } catch (e) { + return false; + } + }), description); +} + +// Assert that the two arrays |actual| and |expected| contain the same set of +// elements as determined by assert_object_equals. The order is not significant. +// +// |expected| is assumed to not contain any duplicates as determined by +// assert_object_equals(). +function assert_array_equivalent(actual, expected, description) { + assert_true(Array.isArray(actual), description); + assert_equals(actual.length, expected.length, description); + expected.forEach(function(expected_element) { + // assert_in_array treats the first argument as being 'actual', and the + // second as being 'expected array'. We are switching them around because + // we want to be resilient against the |actual| array containing + // duplicates. + assert_object_in_array(expected_element, actual, description); + }); +} + +// Asserts that two arrays |actual| and |expected| contain the same set of +// elements as determined by assert_object_equals(). The corresponding elements +// must occupy corresponding indices in their respective arrays. +function assert_array_objects_equals(actual, expected, description) { + assert_true(Array.isArray(actual), description); + assert_equals(actual.length, expected.length, description); + actual.forEach(function(value, index) { + assert_object_equals(value, expected[index], + description + ' : object[' + index + ']'); + }); +} + +// Asserts that |object| that is an instance of some interface has the attribute +// |attribute_name| following the conditions specified by WebIDL, but it's +// acceptable that the attribute |attribute_name| is an own property of the +// object because we're in the middle of moving the attribute to a prototype +// chain. Once we complete the transition to prototype chains, +// assert_will_be_idl_attribute must be replaced with assert_idl_attribute +// defined in testharness.js. +// +// FIXME: Remove assert_will_be_idl_attribute once we complete the transition +// of moving the DOM attributes to prototype chains. (http://crbug.com/43394) +function assert_will_be_idl_attribute(object, attribute_name, description) { + assert_equals(typeof object, "object", description); + + assert_true("hasOwnProperty" in object, description); + + // Do not test if |attribute_name| is not an own property because + // |attribute_name| is in the middle of the transition to a prototype + // chain. (http://crbug.com/43394) + + assert_true(attribute_name in object, description); +} + +// Stringifies a DOM object. This function stringifies not only own properties +// but also DOM attributes which are on a prototype chain. Note that +// JSON.stringify only stringifies own properties. +function stringifyDOMObject(object) +{ + function deepCopy(src) { + if (typeof src != "object") + return src; + var dst = Array.isArray(src) ? [] : {}; + for (var property in src) { + dst[property] = deepCopy(src[property]); + } + return dst; + } + return JSON.stringify(deepCopy(object)); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/trickle.py b/testing/web-platform/tests/service-workers/service-worker/resources/trickle.py new file mode 100644 index 0000000000..6423f7f36f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/trickle.py @@ -0,0 +1,14 @@ +import time + +def main(request, response): + delay = float(request.GET.first(b"ms", 500)) / 1E3 + count = int(request.GET.first(b"count", 50)) + # Read request body + request.body + time.sleep(delay) + response.headers.set(b"Content-type", b"text/plain") + response.write_status_headers() + time.sleep(delay) + for i in range(count): + response.writer.write_content(b"TEST_TRICKLE\n") + time.sleep(delay) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/type-check-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/type-check-worker.js new file mode 100644 index 0000000000..1779e2323d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/type-check-worker.js @@ -0,0 +1,10 @@ +let type = ''; +try { + importScripts('empty.js'); + type = 'classic'; +} catch (e) { + type = 'module'; +} +onmessage = e => { + e.source.postMessage(type); +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/unregister-controller-page.html b/testing/web-platform/tests/service-workers/service-worker/resources/unregister-controller-page.html new file mode 100644 index 0000000000..18a95ee892 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/unregister-controller-page.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<script> +function fetch_url(url) { + return new Promise(function(resolve, reject) { + var request = new XMLHttpRequest(); + request.addEventListener('load', function(event) { + if (request.status == 200) + resolve(request.response); + else + reject(Error(request.statusText)); + }); + request.open('GET', url); + request.send(); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js b/testing/web-platform/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js new file mode 100644 index 0000000000..91a30de5b7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js @@ -0,0 +1,19 @@ +'use strict'; + +// Returns a promise for a network response that contains the Clear-Site-Data: +// "storage" header. +function clear_site_data() { + return fetch('resources/blank.html?pipe=header(Clear-Site-Data,"storage")'); +} + +async function assert_no_registrations_exist() { + const registrations = await navigator.serviceWorker.getRegistrations(); + assert_equals(registrations.length, 0); +} + +async function add_controlled_iframe(test, url) { + const frame = await with_iframe(url); + test.add_cleanup(() => { frame.remove(); }); + assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null); + return frame; +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html new file mode 100644 index 0000000000..f5d0367877 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<script> +async function onLoad() { + const params = new URLSearchParams(self.location.search); + const scope = self.origin + params.get('scopepath'); + const reg = await navigator.serviceWorker.getRegistration(scope); + if (reg) { + await reg.unregister(); + } + if (window.opener) { + window.opener.postMessage({ type: 'SW-UNREGISTERED' }, '*'); + } else { + window.top.postMessage({ type: 'SW-UNREGISTERED' }, '*'); + } +} +self.addEventListener('load', onLoad); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-claim-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-claim-worker.py new file mode 100644 index 0000000000..64914a9dfe --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-claim-worker.py @@ -0,0 +1,24 @@ +import time + +script = u''' +// Time stamp: %s +// (This ensures the source text is *not* a byte-for-byte match with any +// previously-fetched version of this script.) + +// This no-op fetch handler is necessary to bypass explicitly the no fetch +// handler optimization by which this service worker script can be skipped. +addEventListener('fetch', event => { + return; + }); + +addEventListener('install', event => { + event.waitUntil(self.skipWaiting()); + }); + +addEventListener('activate', event => { + event.waitUntil(self.clients.claim()); + });''' + + +def main(request, response): + return [(b'Content-Type', b'application/javascript')], script % time.time() diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.js new file mode 100644 index 0000000000..f1997bd824 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.js @@ -0,0 +1,61 @@ +'use strict'; + +const installEventFired = new Promise(resolve => { + self.fireInstallEvent = resolve; +}); + +const installFinished = new Promise(resolve => { + self.finishInstall = resolve; +}); + +addEventListener('install', event => { + fireInstallEvent(); + event.waitUntil(installFinished); +}); + +addEventListener('message', event => { + let resolveWaitUntil; + event.waitUntil(new Promise(resolve => { resolveWaitUntil = resolve; })); + + // Use a dedicated MessageChannel for every request so senders can wait for + // individual requests to finish, and concurrent requests (to different + // workers) don't cause race conditions. + const port = event.data; + port.onmessage = (event) => { + switch (event.data) { + case 'awaitInstallEvent': + installEventFired.then(() => { + port.postMessage('installEventFired'); + }).finally(resolveWaitUntil); + break; + + case 'finishInstall': + installFinished.then(() => { + port.postMessage('installFinished'); + }).finally(resolveWaitUntil); + finishInstall(); + break; + + case 'callUpdate': { + const channel = new MessageChannel(); + registration.update().then(() => { + channel.port2.postMessage({ + success: true, + }); + }).catch((exception) => { + channel.port2.postMessage({ + success: false, + exception: exception.name, + }); + }).finally(resolveWaitUntil); + port.postMessage(channel.port1, [channel.port1]); + break; + } + + default: + port.postMessage('Unexpected command ' + event.data); + resolveWaitUntil(); + break; + } + }; +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.py new file mode 100644 index 0000000000..3e15926185 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.py @@ -0,0 +1,11 @@ +import random + +def main(request, response): + headers = [(b'Content-Type', b'application/javascript'), + (b'Cache-Control', b'max-age=0')] + # Plug in random.random() to the worker so update() finds a new worker every time. + body = u''' +// %s +importScripts('update-during-installation-worker.js'); + '''.strip() % (random.random()) + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-fetch-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-fetch-worker.py new file mode 100644 index 0000000000..02cbb42dc6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-fetch-worker.py @@ -0,0 +1,18 @@ +import random +import time + +def main(request, response): + # no-cache itself to ensure the user agent finds a new version for each update. + headers = [(b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache')] + + content_type = b'' + extra_body = u'' + + content_type = b'application/javascript' + headers.append((b'Content-Type', content_type)) + + extra_body = u"self.onfetch = (event) => { event.respondWith(fetch(event.request)); };" + + # Return a different script for each access. + return headers, u'/* %s %s */ %s' % (time.time(), random.random(), extra_body) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py new file mode 100644 index 0000000000..7cc5a6561e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py @@ -0,0 +1,14 @@ +import time + +from wptserve.utils import isomorphic_encode + +def main(request, response): + headers = [(b'Content-Type', b'application/javascript'), + (b'Cache-Control', b'max-age=86400'), + (b'Last-Modified', isomorphic_encode(time.strftime(u"%a, %d %b %Y %H:%M:%S GMT", time.gmtime())))] + + body = u''' + const importTime = {time:8f}; + '''.format(time=time.time()) + + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker.py new file mode 100644 index 0000000000..4f879069ef --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker.py @@ -0,0 +1,30 @@ +import time +import json + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def main(request, response): + headers = [(b'Content-Type', b'application/javascript'), + (b'Cache-Control', b'max-age=86400'), + (b'Last-Modified', isomorphic_encode(time.strftime(u"%a, %d %b %Y %H:%M:%S GMT", time.gmtime())))] + + test = request.GET[b'test'] + + body = u''' + const mainTime = {time:8f}; + const testName = {test}; + importScripts('update-max-aged-worker-imported-script.py'); + + addEventListener('message', event => {{ + event.source.postMessage({{ + mainTime, + importTime, + test: {test} + }}); + }}); + '''.format( + time=time.time(), + test=json.dumps(isomorphic_decode(test)) + ) + + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py new file mode 100644 index 0000000000..1547cb5235 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py @@ -0,0 +1,9 @@ +def main(request, response): + key = request.GET[b'key'] + already_requested = request.server.stash.take(key) + + if already_requested is None: + request.server.stash.put(key, True) + return [(b'Content-Type', b'application/javascript')], b'// initial script' + + response.status = (404, b'Not found: should not have been able to import this script twice!') diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py new file mode 100644 index 0000000000..1c447e118e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py @@ -0,0 +1,15 @@ +from wptserve.utils import isomorphic_decode + +def main(request, response): + key = request.GET[b'key'] + already_requested = request.server.stash.take(key) + + header = [(b'Content-Type', b'application/javascript')] + initial_script = u'importScripts("./update-missing-import-scripts-imported-worker.py?key={0}")'.format(isomorphic_decode(key)) + updated_script = u'// removed importScripts()' + + if already_requested is None: + request.server.stash.put(key, True) + return header, initial_script + + return header, updated_script diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-nocookie-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-nocookie-worker.py new file mode 100644 index 0000000000..34eff0263c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-nocookie-worker.py @@ -0,0 +1,14 @@ +import random +import time + +def main(request, response): + # no-cache itself to ensure the user agent finds a new version for each update. + headers = [(b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache')] + + # Set a normal mimetype. + content_type = b'application/javascript' + + headers.append((b'Content-Type', content_type)) + # Return a different script for each access. + return headers, u'// %s %s' % (time.time(), random.random()) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-recovery-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-recovery-worker.py new file mode 100644 index 0000000000..9ac7ce7c75 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-recovery-worker.py @@ -0,0 +1,25 @@ +def main(request, response): + # Set mode to 'init' for initial fetch. + mode = b'init' + if b'update-recovery-mode' in request.cookies: + mode = request.cookies[b'update-recovery-mode'].value + + # no-cache itself to ensure the user agent finds a new version for each update. + headers = [(b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache')] + + extra_body = b'' + + if mode == b'init': + # Install a bad service worker that will break the controlled + # document navigation. + response.set_cookie(b'update-recovery-mode', b'bad') + extra_body = b"addEventListener('fetch', function(e) { e.respondWith(Promise.reject()); });" + elif mode == b'bad': + # When the update tries to pull the script again, update to + # a worker service worker that does not break document + # navigation. Serve the same script from then on. + response.delete_cookie(b'update-recovery-mode') + + headers.append((b'Content-Type', b'application/javascript')) + return headers, b'%s' % (extra_body) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-registration-with-type.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-registration-with-type.py new file mode 100644 index 0000000000..3cabc0fb46 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-registration-with-type.py @@ -0,0 +1,33 @@ +def classic_script(): + return b""" + importScripts('./imported-classic-script.js'); + self.onmessage = e => { + e.source.postMessage(imported); + }; + """ + +def module_script(): + return b""" + import * as module from './imported-module-script.js'; + self.onmessage = e => { + e.source.postMessage(module.imported); + }; + """ + +# Returns the classic script for a first request and +# returns the module script for second and subsequent requests. +def main(request, response): + headers = [(b'Content-Type', b'application/javascript'), + (b'Pragma', b'no-store'), + (b'Cache-Control', b'no-store')] + + classic_first = request.GET[b'classic_first'] + key = request.GET[b'key'] + requested_once = request.server.stash.take(key) + if requested_once is None: + request.server.stash.put(key, True) + body = classic_script() if classic_first == b'1' else module_script() + else: + body = module_script() if classic_first == b'1' else classic_script() + + return 200, headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js new file mode 100644 index 0000000000..d43f6b2f5c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js @@ -0,0 +1 @@ +// Hello world! diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js new file mode 100644 index 0000000000..30c8783a70 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js @@ -0,0 +1,2 @@ +// Hello world! +// **with extra body** diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-worker-from-file.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-worker-from-file.py new file mode 100644 index 0000000000..ac0850f476 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-worker-from-file.py @@ -0,0 +1,33 @@ +import os + +from wptserve.utils import isomorphic_encode + +def serve_js_from_file(request, response, filename): + body = b'' + path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), filename) + with open(path, 'rb') as f: + body = f.read() + return ( + [ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript') + ], body) + +def main(request, response): + key = request.GET[b"Key"] + + visited_count = request.server.stash.take(key) + if visited_count is None: + visited_count = 0 + + # Keep how many times the test requested this resource. + visited_count += 1 + request.server.stash.put(key, visited_count) + + # Serve a file based on how many times it's requested. + if visited_count == 1: + return serve_js_from_file(request, response, request.GET[b"First"]) + if visited_count == 2: + return serve_js_from_file(request, response, request.GET[b"Second"]) + raise u"Unknown state" diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-worker.py new file mode 100644 index 0000000000..5638a8849c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-worker.py @@ -0,0 +1,62 @@ +from urllib.parse import unquote + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def redirect_response(request, response, visited_count): + # |visited_count| is used as a unique id to differentiate responses + # every time. + location = b'empty.js' + if b'Redirect' in request.GET: + location = isomorphic_encode(unquote(isomorphic_decode(request.GET[b'Redirect']))) + return (301, + [ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript'), + (b'Location', location), + ], + u'/* %s */' % str(visited_count)) + +def not_found_response(): + return 404, [(b'Content-Type', b'text/plain')], u"Page not found" + +def ok_response(request, response, visited_count, + extra_body=u'', mime_type=b'application/javascript'): + # |visited_count| is used as a unique id to differentiate responses + # every time. + return ( + [ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', mime_type) + ], + u'/* %s */ %s' % (str(visited_count), extra_body)) + +def main(request, response): + key = request.GET[b"Key"] + mode = request.GET[b"Mode"] + + visited_count = request.server.stash.take(key) + if visited_count is None: + visited_count = 0 + + # Keep how many times the test requested this resource. + visited_count += 1 + request.server.stash.put(key, visited_count) + + # Return a response based on |mode| only when it's the second time (== update). + if visited_count == 2: + if mode == b'normal': + return ok_response(request, response, visited_count) + if mode == b'bad_mime_type': + return ok_response(request, response, visited_count, mime_type=b'text/html') + if mode == b'not_found': + return not_found_response() + if mode == b'redirect': + return redirect_response(request, response, visited_count) + if mode == b'syntax_error': + return ok_response(request, response, visited_count, extra_body=u'badsyntax(isbad;') + if mode == b'throw_install': + return ok_response(request, response, visited_count, extra_body=u"addEventListener('install', function(e) { throw new Error('boom'); });") + + return ok_response(request, response, visited_count) diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html new file mode 100644 index 0000000000..9d4c982721 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html @@ -0,0 +1,8 @@ +<body> +<script> +function load_image(url) { + var img = document.createElement('img'); + img.src = url; +} +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update_shell.py b/testing/web-platform/tests/service-workers/service-worker/resources/update_shell.py new file mode 100644 index 0000000000..2070509437 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/update_shell.py @@ -0,0 +1,32 @@ +# This serves a different response to each request, to test service worker +# updates. If |filename| is provided, it writes that file into the body. +# +# Usage: +# navigator.serviceWorker.register('update_shell.py?filename=worker.js') +# +# This registers worker.js as a service worker, and every update check +# will return a new response. +import os +import random +import time + +from wptserve.utils import isomorphic_encode + +def main(request, response): + # Set no-cache to ensure the user agent finds a new version for each update. + headers = [(b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')] + + # Return a different script for each access. + timestamp = u'// %s %s' % (time.time(), random.random()) + body = isomorphic_encode(timestamp) + b'\n' + + # Inject the file into the response. + if b'filename' in request.GET: + path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), + request.GET[b'filename']) + with open(path, 'rb') as f: + body += f.read() + + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/vtt-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/vtt-frame.html new file mode 100644 index 0000000000..c3ac8034e1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/vtt-frame.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Page Title</title> +<video> + <track> +</video> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js new file mode 100644 index 0000000000..af85a73ad3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js @@ -0,0 +1,12 @@ +var waitUntilResolve; +self.addEventListener('install', function(event) { + event.waitUntil(new Promise(function(resolve) { + waitUntilResolve = resolve; + })); + }); + +self.addEventListener('message', function(event) { + if (event.data === 'STOP_WAITING') { + waitUntilResolve(); + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/websocket-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/websocket-worker.js new file mode 100644 index 0000000000..bb2dc81e55 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/websocket-worker.js @@ -0,0 +1,35 @@ +let port; +let received = false; + +function reportFailure(details) { + port.postMessage('FAIL: ' + details); +} + +onmessage = event => { + port = event.source; + + const ws = new WebSocket('wss://{{host}}:{{ports[wss][0]}}/echo'); + ws.onopen = () => { + ws.send('Hello'); + }; + ws.onmessage = msg => { + if (msg.data !== 'Hello') { + reportFailure('Unexpected reply: ' + msg.data); + return; + } + + received = true; + ws.close(); + }; + ws.onclose = (event) => { + if (!received) { + reportFailure('Closed before receiving reply: ' + event.code); + return; + } + + port.postMessage('PASS'); + }; + ws.onerror = () => { + reportFailure('Got an error event'); + }; +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/websocket.js b/testing/web-platform/tests/service-workers/service-worker/resources/websocket.js new file mode 100644 index 0000000000..fc6abd283a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/websocket.js @@ -0,0 +1,7 @@ +self.urls = []; +self.addEventListener('fetch', function(event) { + self.urls.push(event.request.url); + }); +self.addEventListener('message', function(event) { + event.data.port.postMessage({urls: self.urls}); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/window-opener.html b/testing/web-platform/tests/service-workers/service-worker/resources/window-opener.html new file mode 100644 index 0000000000..32d0744646 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/window-opener.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<meta name="referrer" content="origin"> +<script> +function onLoad() { + self.onmessage = evt => { + if (self.opener) + self.opener.postMessage(evt.data, '*'); + else + self.top.postMessage(evt.data, '*'); + } + const params = new URLSearchParams(self.location.search); + const w = window.open(params.get('target')); + self.addEventListener('unload', evt => w.close()); +} +self.addEventListener('load', onLoad); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js new file mode 100644 index 0000000000..383f66631d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js @@ -0,0 +1,75 @@ +importScripts('/resources/testharness.js'); + +function matchQuery(queryString) { + return self.location.search.substr(1) === queryString; +} + +async function navigateTest(t, e) { + const port = e.data.port; + const url = e.data.url; + const expected = e.data.expected; + + let p = clients.matchAll({ includeUncontrolled : true }) + .then(function(clients) { + for (const client of clients) { + if (client.url === e.data.clientUrl) { + assert_equals(client.frameType, e.data.frameType); + return client.navigate(url); + } + } + throw 'Could not locate window client.'; + }).then(function(newClient) { + // If we didn't reject, we better get resolved with the right thing. + if (newClient === null) { + assert_equals(newClient, expected); + } else { + assert_equals(newClient.url, expected); + } + }); + + if (typeof self[expected] === "function") { + // It's a JS error type name. We are expecting our promise to be rejected + // with that error. + p = promise_rejects_js(t, self[expected], p); + } + + // Let our caller know we are done. + return p.finally(() => port.postMessage(null)); +} + +function getTestClient() { + return clients.matchAll({ includeUncontrolled: true }) + .then(function(clients) { + for (const client of clients) { + if (client.url.includes('windowclient-navigate.https.html')) { + return client; + } + } + + throw new Error('Service worker was unable to locate test client.'); + }); +} + +function waitForMessage(client) { + const channel = new MessageChannel(); + client.postMessage({ port: channel.port2 }, [channel.port2]); + + return new Promise(function(resolve) { + channel.port1.onmessage = resolve; + }); +} + +// The worker must remain in the "installing" state for the duration of some +// sub-tests. In order to achieve this coordination without relying on global +// state, the worker must create a message channel with the client from within +// the "install" event handler. +if (matchQuery('installing')) { + self.addEventListener('install', function(e) { + e.waitUntil(getTestClient().then(waitForMessage)); + }); +} + +self.addEventListener('message', function(e) { + e.waitUntil(promise_test(t => navigateTest(t, e), + e.data.description + " worker side")); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/worker-client-id-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-client-id-worker.js new file mode 100644 index 0000000000..f592629d07 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/worker-client-id-worker.js @@ -0,0 +1,25 @@ +addEventListener('fetch', evt => { + if (evt.request.url.includes('worker-echo-client-id.js')) { + evt.respondWith(new Response( + 'fetch("fetch-echo-client-id").then(r => r.text()).then(t => self.postMessage(t));', + { headers: { 'Content-Type': 'application/javascript' }})); + return; + } + + if (evt.request.url.includes('fetch-echo-client-id')) { + evt.respondWith(new Response(evt.clientId)); + return; + } + + if (evt.request.url.includes('frame.html')) { + evt.respondWith(new Response('')); + return; + } +}); + +addEventListener('message', evt => { + if (evt.data === 'echo-client-id') { + evt.ports[0].postMessage(evt.source.id); + return; + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js new file mode 100644 index 0000000000..a81bb3dd6e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js @@ -0,0 +1,12 @@ +importScripts('/common/get-host-info.sub.js'); +importScripts('test-helpers.sub.js'); + +self.addEventListener('fetch', event => { + const host_info = get_host_info(); + // The sneaky Service Worker changes the same-origin 'square' request for a cross-origin image. + if (event.request.url.indexOf('square') != -1) { + const searchParams = new URLSearchParams(location.search); + const mode = searchParams.get("mode") || "cors"; + event.respondWith(fetch(`${host_info['HTTPS_REMOTE_ORIGIN']}${base_path()}square.png`, { mode })); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js new file mode 100644 index 0000000000..d36b0b6da6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js @@ -0,0 +1,53 @@ +let name; +if (self.registration.scope.indexOf('scope1') != -1) + name = 'sw1'; +if (self.registration.scope.indexOf('scope2') != -1) + name = 'sw2'; + + +self.addEventListener('fetch', evt => { + // There are three types of requests this service worker handles. + + // (1) The first request for the worker, which will redirect elsewhere. + // "redirect.py" means to test network redirect, so let network handle it. + if (evt.request.url.indexOf('redirect.py') != -1) { + return; + } + // "sw-redirect" means to test service worker redirect, so respond with a + // redirect. + if (evt.request.url.indexOf('sw-redirect') != -1) { + const url = new URL(evt.request.url); + const redirect_to = url.searchParams.get('Redirect'); + evt.respondWith(Response.redirect(redirect_to)); + return; + } + + // (2) After redirect, the request is for a "webworker.py" URL. + // Add a search parameter to indicate this service worker handled the + // final request for the worker. + if (evt.request.url.indexOf('webworker.py') != -1) { + const greeting = encodeURIComponent(`${name} saw the request for the worker script`); + // Serve from `./subdir/`, not `./`, + // to conform that the base URL used in the worker is + // the response URL (`./subdir/`), not the current request URL (`./`). + evt.respondWith(fetch(`subdir/worker_interception_redirect_webworker.py?greeting=${greeting}`)); + return; + } + + const path = (new URL(evt.request.url)).pathname; + + // (3) The worker does an importScripts() to import-scripts-echo.py. Indicate + // that this service worker handled the request. + if (evt.request.url.indexOf('import-scripts-echo.py') != -1) { + const msg = encodeURIComponent(`${name} saw importScripts from the worker: ${path}`); + evt.respondWith(fetch(`import-scripts-echo.py?msg=${msg}`)); + return; + } + + // (4) The worker does a fetch() to simple.txt. Indicate that this service + // worker handled the request. + if (evt.request.url.indexOf('simple.txt') != -1) { + evt.respondWith(new Response(`${name} saw the fetch from the worker: ${path}`)); + return; + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js new file mode 100644 index 0000000000..b7e6d81b09 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js @@ -0,0 +1,56 @@ +// This is the (shared or dedicated) worker file for the +// worker-interception-redirect test. It should be served by the corresponding +// .py file instead of being served directly. +// +// This file is served from both resources/*webworker.py, +// resources/scope2/*webworker.py and resources/subdir/*webworker.py. +// Relative paths are used in `fetch()` and `importScripts()` to confirm that +// the correct base URLs are used. + +// This greeting text is meant to be injected by the Python script that serves +// this file, to indicate how the script was served (from network or from +// service worker). +// +// We can't just use a sub pipe and name this file .sub.js since we want +// to serve the file from multiple URLs (see above). +let greeting = '%GREETING_TEXT%'; +if (!greeting) + greeting = 'the worker script was served from network'; + +// Call importScripts() which fills |echo_output| with a string indicating +// whether a service worker intercepted the importScripts() request. +let echo_output; +const import_scripts_msg = encodeURIComponent( + 'importScripts: served from network'); +let import_scripts_greeting = 'not set'; +try { + importScripts(`import-scripts-echo.py?msg=${import_scripts_msg}`); + import_scripts_greeting = echo_output; +} catch(e) { + import_scripts_greeting = 'importScripts failed'; +} + +async function runTest(port) { + port.postMessage(greeting); + + port.postMessage(import_scripts_greeting); + + const response = await fetch('simple.txt'); + const text = await response.text(); + port.postMessage('fetch(): ' + text); + + port.postMessage(self.location.href); +} + +if ('DedicatedWorkerGlobalScope' in self && + self instanceof DedicatedWorkerGlobalScope) { + runTest(self); +} else if ( + 'SharedWorkerGlobalScope' in self && + self instanceof SharedWorkerGlobalScope) { + self.onconnect = function(e) { + const port = e.ports[0]; + port.start(); + runTest(port); + }; +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/worker-load-interceptor.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-load-interceptor.js new file mode 100644 index 0000000000..ebc0db67aa --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/worker-load-interceptor.js @@ -0,0 +1,16 @@ +importScripts('/common/get-host-info.sub.js'); + +const response_text = 'This load was successfully intercepted.'; +const response_script = + `const message = 'This load was successfully intercepted.';`; + +self.onfetch = event => { + const url = event.request.url; + if (url.indexOf('synthesized-response.txt') != -1) { + event.respondWith(new Response(response_text)); + } else if (url.indexOf('synthesized-response.js') != -1) { + event.respondWith(new Response( + response_script, + {headers: {'Content-Type': 'application/javascript'}})); + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/worker-testharness.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-testharness.js new file mode 100644 index 0000000000..73e97be1ea --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/worker-testharness.js @@ -0,0 +1,49 @@ +/* + * worker-test-harness should be considered a temporary polyfill around + * testharness.js for supporting Service Worker based tests. It should not be + * necessary once the test harness is able to drive worker based tests natively. + * See https://github.com/w3c/testharness.js/pull/82 for status of effort to + * update upstream testharness.js. Once the upstreaming is complete, tests that + * reference worker-test-harness should be updated to directly import + * testharness.js. + */ + +importScripts('/resources/testharness.js'); + +(function() { + var next_cache_index = 1; + + // Returns a promise that resolves to a newly created Cache object. The + // returned Cache will be destroyed when |test| completes. + function create_temporary_cache(test) { + var uniquifier = String(++next_cache_index); + var cache_name = self.location.pathname + '/' + uniquifier; + + test.add_cleanup(function() { + return self.caches.delete(cache_name); + }); + + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }); + } + + self.create_temporary_cache = create_temporary_cache; +})(); + +// Runs |test_function| with a temporary unique Cache passed in as the only +// argument. The function is run as a part of Promise chain owned by +// promise_test(). As such, it is expected to behave in a manner identical (with +// the exception of the argument) to a function passed into promise_test(). +// +// E.g.: +// cache_test(function(cache) { +// // Do something with |cache|, which is a Cache object. +// }, "Some Cache test"); +function cache_test(test_function, description) { + promise_test(function(test) { + return create_temporary_cache(test) + .then(test_function); + }, description); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py b/testing/web-platform/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py new file mode 100644 index 0000000000..4ed5beea74 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py @@ -0,0 +1,20 @@ +# This serves the worker JavaScript file. It takes a |greeting| request +# parameter to inject into the JavaScript to indicate how the request +# reached the server. +import os + +from wptserve.utils import isomorphic_decode + +def main(request, response): + path = os.path.join(os.path.dirname(isomorphic_decode(__file__)), + u"worker-interception-redirect-webworker.js") + body = open(path, u"rb").read() + if b"greeting" in request.GET: + body = body.replace(b"%GREETING_TEXT%", request.GET[b"greeting"]) + else: + body = body.replace(b"%GREETING_TEXT%", b"") + + headers = [] + headers.append((b"Content-Type", b"text/javascript")) + + return headers, body diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/xhr-content-length-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/xhr-content-length-worker.js new file mode 100644 index 0000000000..604deece2d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/xhr-content-length-worker.js @@ -0,0 +1,22 @@ +// Service worker for the xhr-content-length test. + +self.addEventListener("fetch", event => { + const url = new URL(event.request.url); + const type = url.searchParams.get("type"); + + if (type === "no-content-length") { + event.respondWith(new Response("Hello!")); + } + + if (type === "larger-content-length") { + event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "10000"]] })); + } + + if (type === "double-content-length") { + event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "10000"], ["Content-Length", "10000"]] })); + } + + if (type === "bogus-content-length") { + event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "test"]] })); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/xhr-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/xhr-iframe.html new file mode 100644 index 0000000000..4c57bbbc65 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/xhr-iframe.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>iframe for xhr tests</title> +<script> +async function xhr(url, options) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + const opts = options ? options : {}; + xhr.onload = () => { + resolve(xhr); + }; + xhr.onerror = () => { + reject('xhr failed'); + }; + + xhr.open('GET', url); + if (opts.responseType) { + xhr.responseType = opts.responseType; + } + xhr.send(); + }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/xhr-response-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/xhr-response-url-worker.js new file mode 100644 index 0000000000..906ad5005b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/xhr-response-url-worker.js @@ -0,0 +1,32 @@ +// Service worker for the xhr-response-url test. + +self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + const respondWith = url.searchParams.get('respondWith'); + if (!respondWith) + return; + + if (respondWith == 'fetch') { + const target = url.searchParams.get('url'); + event.respondWith(fetch(target)); + return; + } + + if (respondWith == 'string') { + const headers = {'content-type': 'text/plain'}; + event.respondWith(new Response('hello', {headers})); + return; + } + + if (respondWith == 'document') { + const doc = ` + <!DOCTYPE html> + <html> + <title>hi</title> + <body>hello</body> + </html>`; + const headers = {'content-type': 'text/html'}; + event.respondWith(new Response(doc, {headers})); + return; + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml b/testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml new file mode 100644 index 0000000000..065a07acb2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml @@ -0,0 +1,5 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/xsl" href="resources/request-url-path/import-relative.xsl"?> +<stylesheet-test> +This tests a stylesheet which has a xsl:import with a relative URL. +</stylesheet-test> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-worker.js new file mode 100644 index 0000000000..50e2b1842f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + + // For the import-relative.xsl file, respond in a way that changes the + // response URL. This is expected to change the base URL and allow the import + // from the file to succeed. + const path = 'request-url-path/import-relative.xsl'; + if (url.pathname.indexOf(path) != -1) { + // Respond with a different URL, deleting "request-url-path/". + event.respondWith(fetch('import-relative.xsl')); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/xslt-pass.xsl b/testing/web-platform/tests/service-workers/service-worker/resources/xslt-pass.xsl new file mode 100644 index 0000000000..2cd7f2f8f8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/xslt-pass.xsl @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> + +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:template match="/"> + <html> + <body> + <p>PASS</p> + </body> + </html> + </xsl:template> +</xsl:stylesheet> |