diff options
Diffstat (limited to 'testing/web-platform/tests/service-workers')
748 files changed, 40691 insertions, 0 deletions
diff --git a/testing/web-platform/tests/service-workers/META.yml b/testing/web-platform/tests/service-workers/META.yml new file mode 100644 index 0000000000..03a0dd0fe1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/META.yml @@ -0,0 +1,6 @@ +spec: https://w3c.github.io/ServiceWorker/ +suggested_reviewers: + - asutherland + - mkruisselbrink + - mattto + - wanderview diff --git a/testing/web-platform/tests/service-workers/cache-storage/META.yml b/testing/web-platform/tests/service-workers/cache-storage/META.yml new file mode 100644 index 0000000000..bf34474f74 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/META.yml @@ -0,0 +1,3 @@ +suggested_reviewers: + - inexorabletash + - wanderview diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js new file mode 100644 index 0000000000..960d1bb1bf --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js @@ -0,0 +1,81 @@ +// META: title=Cache Storage: Abort +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: script=/common/utils.js +// META: timeout=long + +// We perform the same tests on put, add, addAll. Parameterise the tests to +// reduce repetition. +const methodsToTest = { + put: async (cache, request) => { + const response = await fetch(request); + return cache.put(request, response); + }, + add: async (cache, request) => cache.add(request), + addAll: async (cache, request) => cache.addAll([request]), +}; + +for (const method in methodsToTest) { + const perform = methodsToTest[method]; + + cache_test(async (cache, test) => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + const request = new Request('../resources/simple.txt', { signal }); + return promise_rejects_dom(test, 'AbortError', perform(cache, request), + `${method} should reject`); + }, `${method}() on an already-aborted request should reject with AbortError`); + + cache_test(async (cache, test) => { + const controller = new AbortController(); + const signal = controller.signal; + const request = new Request('../resources/simple.txt', { signal }); + const promise = perform(cache, request); + controller.abort(); + return promise_rejects_dom(test, 'AbortError', promise, + `${method} should reject`); + }, `${method}() synchronously followed by abort should reject with ` + + `AbortError`); + + cache_test(async (cache, test) => { + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + const request = new Request( + `../../../fetch/api/resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, + { signal }); + + const promise = perform(cache, request); + + // Wait for the server to start sending the response body. + let opened = false; + do { + // Normally only one fetch to 'stash-take' is needed, but the fetches + // will be served in reverse order sometimes + // (i.e., 'stash-take' gets served before 'infinite-slow-response'). + + const response = + await fetch(`../../../fetch/api/resources/stash-take.py?key=${stateKey}`); + const body = await response.json(); + if (body === 'open') opened = true; + } while (!opened); + + // Sadly the above loop cannot guarantee that the browser has started + // processing the response body. This delay is needed to make the test + // failures non-flaky in Chrome version 66. My deepest apologies. + await new Promise(resolve => setTimeout(resolve, 250)); + + controller.abort(); + + await promise_rejects_dom(test, 'AbortError', promise, + `${method} should reject`); + + // infinite-slow-response.py doesn't know when to stop. + return fetch(`../../../fetch/api/resources/stash-put.py?key=${abortKey}`); + }, `${method}() followed by abort after headers received should reject ` + + `with AbortError`); +} + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js new file mode 100644 index 0000000000..eca516abd5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js @@ -0,0 +1,368 @@ +// META: title=Cache.add and Cache.addAll +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=./resources/test-helpers.js +// META: timeout=long + +const { REMOTE_HOST } = get_host_info(); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.add(), + 'Cache.add should throw a TypeError when no arguments are given.'); + }, 'Cache.add called with no arguments'); + +cache_test(function(cache) { + return cache.add('./resources/simple.txt') + .then(function(result) { + assert_equals(result, undefined, + 'Cache.add should resolve with undefined on success.'); + return cache.match('./resources/simple.txt'); + }) + .then(function(response) { + assert_class_string(response, 'Response', + 'Cache.add should put a resource in the cache.'); + return response.text(); + }) + .then(function(body) { + assert_equals(body, 'a simple text file\n', + 'Cache.add should retrieve the correct body.'); + }); + }, 'Cache.add called with relative URL specified as a string'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.add('javascript://this-is-not-http-mmkay'), + 'Cache.add should throw a TypeError for non-HTTP/HTTPS URLs.'); + }, 'Cache.add called with non-HTTP/HTTPS URL'); + +cache_test(function(cache) { + var request = new Request('./resources/simple.txt'); + return cache.add(request) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.add should resolve with undefined on success.'); + }); + }, 'Cache.add called with Request object'); + +cache_test(function(cache, test) { + var request = new Request('./resources/simple.txt', + {method: 'POST', body: 'This is a body.'}); + return promise_rejects_js( + test, + TypeError, + cache.add(request), + 'Cache.add should throw a TypeError for non-GET requests.'); + }, 'Cache.add called with POST request'); + +cache_test(function(cache) { + var request = new Request('./resources/simple.txt'); + return cache.add(request) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.add should resolve with undefined on success.'); + }) + .then(function() { + return cache.add(request); + }) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.add should resolve with undefined on success.'); + }); + }, 'Cache.add called twice with the same Request object'); + +cache_test(function(cache) { + var request = new Request('./resources/simple.txt'); + return request.text() + .then(function() { + assert_false(request.bodyUsed); + }) + .then(function() { + return cache.add(request); + }); + }, 'Cache.add with request with null body (not consumed)'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.add('./resources/fetch-status.py?status=206'), + 'Cache.add should reject on partial response'); + }, 'Cache.add with 206 response'); + +cache_test(function(cache, test) { + var urls = ['./resources/fetch-status.py?status=206', + './resources/fetch-status.py?status=200']; + var requests = urls.map(function(url) { + return new Request(url); + }); + return promise_rejects_js( + test, + TypeError, + cache.addAll(requests), + 'Cache.addAll should reject with TypeError if any request fails'); + }, 'Cache.addAll with 206 response'); + +cache_test(function(cache, test) { + var urls = ['./resources/fetch-status.py?status=206', + './resources/fetch-status.py?status=200']; + var requests = urls.map(function(url) { + var cross_origin_url = new URL(url, location.href); + cross_origin_url.hostname = REMOTE_HOST; + return new Request(cross_origin_url.href, { mode: 'no-cors' }); + }); + return promise_rejects_js( + test, + TypeError, + cache.addAll(requests), + 'Cache.addAll should reject with TypeError if any request fails'); + }, 'Cache.addAll with opaque-filtered 206 response'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.add('this-does-not-exist-please-dont-create-it'), + 'Cache.add should reject if response is !ok'); + }, 'Cache.add with request that results in a status of 404'); + + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.add('./resources/fetch-status.py?status=500'), + 'Cache.add should reject if response is !ok'); + }, 'Cache.add with request that results in a status of 500'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.addAll(), + 'Cache.addAll with no arguments should throw TypeError.'); + }, 'Cache.addAll with no arguments'); + +cache_test(function(cache, test) { + // Assumes the existence of ../resources/simple.txt and ../resources/blank.html + var urls = ['./resources/simple.txt', undefined, './resources/blank.html']; + return promise_rejects_js( + test, + TypeError, + cache.addAll(urls), + 'Cache.addAll should throw TypeError for an undefined argument.'); + }, 'Cache.addAll with a mix of valid and undefined arguments'); + +cache_test(function(cache) { + return cache.addAll([]) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.addAll should resolve with undefined on ' + + 'success.'); + return cache.keys(); + }) + .then(function(result) { + assert_equals(result.length, 0, + 'There should be no entry in the cache.'); + }); + }, 'Cache.addAll with an empty array'); + +cache_test(function(cache) { + // Assumes the existence of ../resources/simple.txt and + // ../resources/blank.html + var urls = ['./resources/simple.txt', + self.location.href, + './resources/blank.html']; + return cache.addAll(urls) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.addAll should resolve with undefined on ' + + 'success.'); + return Promise.all( + urls.map(function(url) { return cache.match(url); })); + }) + .then(function(responses) { + assert_class_string( + responses[0], 'Response', + 'Cache.addAll should put a resource in the cache.'); + assert_class_string( + responses[1], 'Response', + 'Cache.addAll should put a resource in the cache.'); + assert_class_string( + responses[2], 'Response', + 'Cache.addAll should put a resource in the cache.'); + return Promise.all( + responses.map(function(response) { return response.text(); })); + }) + .then(function(bodies) { + assert_equals( + bodies[0], 'a simple text file\n', + 'Cache.add should retrieve the correct body.'); + assert_equals( + bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n', + 'Cache.add should retrieve the correct body.'); + }); + }, 'Cache.addAll with string URL arguments'); + +cache_test(function(cache) { + // Assumes the existence of ../resources/simple.txt and + // ../resources/blank.html + var urls = ['./resources/simple.txt', + self.location.href, + './resources/blank.html']; + var requests = urls.map(function(url) { + return new Request(url); + }); + return cache.addAll(requests) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.addAll should resolve with undefined on ' + + 'success.'); + return Promise.all( + urls.map(function(url) { return cache.match(url); })); + }) + .then(function(responses) { + assert_class_string( + responses[0], 'Response', + 'Cache.addAll should put a resource in the cache.'); + assert_class_string( + responses[1], 'Response', + 'Cache.addAll should put a resource in the cache.'); + assert_class_string( + responses[2], 'Response', + 'Cache.addAll should put a resource in the cache.'); + return Promise.all( + responses.map(function(response) { return response.text(); })); + }) + .then(function(bodies) { + assert_equals( + bodies[0], 'a simple text file\n', + 'Cache.add should retrieve the correct body.'); + assert_equals( + bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n', + 'Cache.add should retrieve the correct body.'); + }); + }, 'Cache.addAll with Request arguments'); + +cache_test(function(cache, test) { + // Assumes that ../resources/simple.txt and ../resources/blank.html exist. + // The second resource does not. + var urls = ['./resources/simple.txt', + 'this-resource-should-not-exist', + './resources/blank.html']; + var requests = urls.map(function(url) { + return new Request(url); + }); + return promise_rejects_js( + test, + TypeError, + cache.addAll(requests), + 'Cache.addAll should reject with TypeError if any request fails') + .then(function() { + return Promise.all(urls.map(function(url) { + return cache.match(url); + })); + }) + .then(function(matches) { + assert_array_equals( + matches, + [undefined, undefined, undefined], + 'If any response fails, no response should be added to cache'); + }); + }, 'Cache.addAll with a mix of succeeding and failing requests'); + +cache_test(function(cache, test) { + var request = new Request('../resources/simple.txt'); + return promise_rejects_dom( + test, + 'InvalidStateError', + cache.addAll([request, request]), + 'Cache.addAll should throw InvalidStateError if the same request is added ' + + 'twice.'); + }, 'Cache.addAll called with the same Request object specified twice'); + +cache_test(async function(cache, test) { + const url = './resources/vary.py?vary=x-shape'; + let requests = [ + new Request(url, { headers: { 'x-shape': 'circle' }}), + new Request(url, { headers: { 'x-shape': 'square' }}), + ]; + let result = await cache.addAll(requests); + assert_equals(result, undefined, 'Cache.addAll() should succeed'); + }, 'Cache.addAll should succeed when entries differ by vary header'); + +cache_test(async function(cache, test) { + const url = './resources/vary.py?vary=x-shape'; + let requests = [ + new Request(url, { headers: { 'x-shape': 'circle' }}), + new Request(url, { headers: { 'x-shape': 'circle' }}), + ]; + await promise_rejects_dom( + test, + 'InvalidStateError', + cache.addAll(requests), + 'Cache.addAll() should reject when entries are duplicate by vary header'); + }, 'Cache.addAll should reject when entries are duplicate by vary header'); + +// VARY header matching is asymmetric. Determining if two entries are duplicate +// depends on which entry's response is used in the comparison. The target +// response's VARY header determines what request headers are examined. This +// test verifies that Cache.addAll() duplicate checking handles this asymmetric +// behavior correctly. +cache_test(async function(cache, test) { + const base_url = './resources/vary.py'; + + // Define a request URL that sets a VARY header in the + // query string to be echoed back by the server. + const url = base_url + '?vary=x-size'; + + // Set a cookie to override the VARY header of the response + // when the request is made with credentials. This will + // take precedence over the query string vary param. This + // is a bit confusing, but it's necessary to construct a test + // where the URL is the same, but the VARY headers differ. + // + // Note, the test could also pass this information in additional + // request headers. If the cookie approach becomes too unwieldy + // this test could be rewritten to use that technique. + await fetch(base_url + '?set-vary-value-override-cookie=x-shape'); + test.add_cleanup(_ => fetch(base_url + '?clear-vary-value-override-cookie')); + + let requests = [ + // This request will result in a Response with a "Vary: x-shape" + // header. This *will not* result in a duplicate match with the + // other entry. + new Request(url, { headers: { 'x-shape': 'circle', + 'x-size': 'big' }, + credentials: 'same-origin' }), + + // This request will result in a Response with a "Vary: x-size" + // header. This *will* result in a duplicate match with the other + // entry. + new Request(url, { headers: { 'x-shape': 'square', + 'x-size': 'big' }, + credentials: 'omit' }), + ]; + await promise_rejects_dom( + test, + 'InvalidStateError', + cache.addAll(requests), + 'Cache.addAll() should reject when one entry has a vary header ' + + 'matching an earlier entry.'); + + // Test the reverse order now. + await promise_rejects_dom( + test, + 'InvalidStateError', + cache.addAll(requests.reverse()), + 'Cache.addAll() should reject when one entry has a vary header ' + + 'matching a later entry.'); + + }, 'Cache.addAll should reject when one entry has a vary header ' + + 'matching another entry'); + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js new file mode 100644 index 0000000000..3eae2b6a08 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js @@ -0,0 +1,164 @@ +// META: title=Cache.delete +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +var test_url = 'https://example.com/foo'; + +// Construct a generic Request object. The URL is |test_url|. All other fields +// are defaults. +function new_test_request() { + return new Request(test_url); +} + +// Construct a generic Response object. +function new_test_response() { + return new Response('Hello world!', { status: 200 }); +} + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.delete(), + 'Cache.delete should reject with a TypeError when called with no ' + + 'arguments.'); + }, 'Cache.delete with no arguments'); + +cache_test(function(cache) { + return cache.put(new_test_request(), new_test_response()) + .then(function() { + return cache.delete(test_url); + }) + .then(function(result) { + assert_true(result, + 'Cache.delete should resolve with "true" if an entry ' + + 'was successfully deleted.'); + return cache.match(test_url); + }) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.delete should remove matching entries from cache.'); + }); + }, 'Cache.delete called with a string URL'); + +cache_test(function(cache) { + var request = new Request(test_url); + return cache.put(request, new_test_response()) + .then(function() { + return cache.delete(request); + }) + .then(function(result) { + assert_true(result, + 'Cache.delete should resolve with "true" if an entry ' + + 'was successfully deleted.'); + }); + }, 'Cache.delete called with a Request object'); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new_test_response(); + return cache.put(request, response) + .then(function() { + return cache.delete(new Request(test_url, {method: 'HEAD'})); + }) + .then(function(result) { + assert_false(result, + 'Cache.delete should not match a non-GET request ' + + 'unless ignoreMethod option is set.'); + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, response, + 'Cache.delete should leave non-matching response in the cache.'); + return cache.delete(new Request(test_url, {method: 'HEAD'}), + {ignoreMethod: true}); + }) + .then(function(result) { + assert_true(result, + 'Cache.delete should match a non-GET request ' + + ' if ignoreMethod is true.'); + }); + }, 'Cache.delete called with a HEAD request'); + +cache_test(function(cache) { + var vary_request = new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}); + var vary_response = new Response('', {headers: {'Vary': 'Cookies'}}); + var mismatched_vary_request = new Request('http://example.com/c'); + + return cache.put(vary_request.clone(), vary_response.clone()) + .then(function() { + return cache.delete(mismatched_vary_request.clone()); + }) + .then(function(result) { + assert_false(result, + 'Cache.delete should not delete if vary does not ' + + 'match unless ignoreVary is true'); + return cache.delete(mismatched_vary_request.clone(), + {ignoreVary: true}); + }) + .then(function(result) { + assert_true(result, + 'Cache.delete should ignore vary if ignoreVary is true'); + }); + }, 'Cache.delete supports ignoreVary'); + +cache_test(function(cache) { + return cache.delete(test_url) + .then(function(result) { + assert_false(result, + 'Cache.delete should resolve with "false" if there ' + + 'are no matching entries.'); + }); + }, 'Cache.delete with a non-existent entry'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a_with_query.request, + { ignoreSearch: true }) + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.a.response, + entries.a_with_query.response + ]); + return cache.delete(entries.a_with_query.request, + { ignoreSearch: true }); + }) + .then(function(result) { + return cache.matchAll(entries.a_with_query.request, + { ignoreSearch: true }); + }) + .then(function(result) { + assert_response_array_equals(result, []); + }); + }, + 'Cache.delete with ignoreSearch option (request with search parameters)'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a_with_query.request, + { ignoreSearch: true }) + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.a.response, + entries.a_with_query.response + ]); + // cache.delete()'s behavior should be the same if ignoreSearch is + // not provided or if ignoreSearch is false. + return cache.delete(entries.a_with_query.request, + { ignoreSearch: false }); + }) + .then(function(result) { + return cache.matchAll(entries.a_with_query.request, + { ignoreSearch: true }); + }) + .then(function(result) { + assert_response_array_equals(result, [ entries.a.response ]); + }); + }, + 'Cache.delete with ignoreSearch option (when it is specified as false)'); + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html b/testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html new file mode 100644 index 0000000000..3c96348e0e --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<title>Cache.keys (checking request attributes that can be set only on service workers)</title> +<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-keys"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="./../service-worker/resources/test-helpers.sub.js"></script> +<script> +const worker = './resources/cache-keys-attributes-for-service-worker.js'; + +function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); +} + +promise_test(async (t) => { + const scope = './resources/blank.html?name=isReloadNavigation'; + let frame; + let reg; + + try { + reg = await service_worker_unregister_and_register(t, worker, scope); + await wait_for_state(t, reg.installing, 'activated'); + frame = await with_iframe(scope); + assert_equals(frame.contentDocument.body.textContent, + 'original: false, stored: false'); + await new Promise((resolve) => { + frame.onload = resolve; + frame.contentWindow.location.reload(); + }); + assert_equals(frame.contentDocument.body.textContent, + 'original: true, stored: true'); + } finally { + if (frame) { + frame.remove(); + } + if (reg) { + await reg.unregister(); + } + } +}, 'Request.IsReloadNavigation should persist.'); + +promise_test(async (t) => { + const scope = './resources/blank.html?name=isHistoryNavigation'; + let frame; + let reg; + + try { + reg = await service_worker_unregister_and_register(t, worker, scope); + await wait_for_state(t, reg.installing, 'activated'); + frame = await with_iframe(scope); + assert_equals(frame.contentDocument.body.textContent, + 'original: false, stored: false'); + // Use step_timeout(0) to ensure the history entry is created for Blink + // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861. + await wait(0); + await new Promise((resolve) => { + frame.onload = resolve; + frame.src = '../resources/blank.html?ignore'; + }); + await wait(0); + await new Promise((resolve) => { + frame.onload = resolve; + frame.contentWindow.history.go(-1); + }); + assert_equals(frame.contentDocument.body.textContent, + 'original: true, stored: true'); + } finally { + if (frame) { + frame.remove(); + } + if (reg) { + await reg.unregister(); + } + } +}, 'Request.IsHistoryNavigation should persist.'); +</script> diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js new file mode 100644 index 0000000000..232fb760d4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js @@ -0,0 +1,212 @@ +// META: title=Cache.keys +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +cache_test(cache => { + return cache.keys() + .then(requests => { + assert_equals( + requests.length, 0, + 'Cache.keys should resolve to an empty array for an empty cache'); + }); + }, 'Cache.keys() called on an empty cache'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys('not-present-in-the-cache') + .then(function(result) { + assert_request_array_equals( + result, [], + 'Cache.keys should resolve with an empty array on failure.'); + }); + }, 'Cache.keys with no matching entries'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(entries.a.request.url) + .then(function(result) { + assert_request_array_equals(result, [entries.a.request], + 'Cache.keys should match by URL.'); + }); + }, 'Cache.keys with URL'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(entries.a.request) + .then(function(result) { + assert_request_array_equals( + result, [entries.a.request], + 'Cache.keys should match by Request.'); + }); + }, 'Cache.keys with Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(new Request(entries.a.request.url)) + .then(function(result) { + assert_request_array_equals( + result, [entries.a.request], + 'Cache.keys should match by Request.'); + }); + }, 'Cache.keys with new Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(entries.a.request, {ignoreSearch: true}) + .then(function(result) { + assert_request_array_equals( + result, + [ + entries.a.request, + entries.a_with_query.request + ], + 'Cache.keys with ignoreSearch should ignore the ' + + 'search parameters of cached request.'); + }); + }, + 'Cache.keys with ignoreSearch option (request with no search ' + + 'parameters)'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(entries.a_with_query.request, {ignoreSearch: true}) + .then(function(result) { + assert_request_array_equals( + result, + [ + entries.a.request, + entries.a_with_query.request + ], + 'Cache.keys with ignoreSearch should ignore the ' + + 'search parameters of request.'); + }); + }, + 'Cache.keys with ignoreSearch option (request with search parameters)'); + +cache_test(function(cache) { + var request = new Request('http://example.com/'); + var head_request = new Request('http://example.com/', {method: 'HEAD'}); + var response = new Response('foo'); + return cache.put(request.clone(), response.clone()) + .then(function() { + return cache.keys(head_request.clone()); + }) + .then(function(result) { + assert_request_array_equals( + result, [], + 'Cache.keys should resolve with an empty array with a ' + + 'mismatched method.'); + return cache.keys(head_request.clone(), + {ignoreMethod: true}); + }) + .then(function(result) { + assert_request_array_equals( + result, + [ + request, + ], + 'Cache.keys with ignoreMethod should ignore the ' + + 'method of request.'); + }); + }, 'Cache.keys supports ignoreMethod'); + +cache_test(function(cache) { + var vary_request = new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}); + var vary_response = new Response('', {headers: {'Vary': 'Cookies'}}); + var mismatched_vary_request = new Request('http://example.com/c'); + + return cache.put(vary_request.clone(), vary_response.clone()) + .then(function() { + return cache.keys(mismatched_vary_request.clone()); + }) + .then(function(result) { + assert_request_array_equals( + result, [], + 'Cache.keys should resolve with an empty array with a ' + + 'mismatched vary.'); + return cache.keys(mismatched_vary_request.clone(), + {ignoreVary: true}); + }) + .then(function(result) { + assert_request_array_equals( + result, + [ + vary_request, + ], + 'Cache.keys with ignoreVary should ignore the ' + + 'vary of request.'); + }); + }, 'Cache.keys supports ignoreVary'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(entries.cat.request.url + '#mouse') + .then(function(result) { + assert_request_array_equals( + result, + [ + entries.cat.request, + ], + 'Cache.keys should ignore URL fragment.'); + }); + }, 'Cache.keys with URL containing fragment'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys('http') + .then(function(result) { + assert_request_array_equals( + result, [], + 'Cache.keys should treat query as a URL and not ' + + 'just a string fragment.'); + }); + }, 'Cache.keys with string fragment "http" as query'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys() + .then(function(result) { + assert_request_array_equals( + result, + simple_entries.map(entry => entry.request), + 'Cache.keys without parameters should match all entries.'); + }); + }, 'Cache.keys without parameters'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(undefined) + .then(function(result) { + assert_request_array_equals( + result, + simple_entries.map(entry => entry.request), + 'Cache.keys with undefined request should match all entries.'); + }); + }, 'Cache.keys with explicitly undefined request'); + +cache_test(cache => { + return cache.keys(undefined, {}) + .then(requests => { + assert_equals( + requests.length, 0, + 'Cache.keys should resolve to an empty array for an empty cache'); + }); + }, 'Cache.keys with explicitly undefined request and empty options'); + +prepopulated_cache_test(vary_entries, function(cache, entries) { + return cache.keys() + .then(function(result) { + assert_request_array_equals( + result, + [ + entries.vary_cookie_is_cookie.request, + entries.vary_cookie_is_good.request, + entries.vary_cookie_absent.request, + ], + 'Cache.keys without parameters should match all entries.'); + }); + }, 'Cache.keys without parameters and VARY entries'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(new Request(entries.cat.request.url, {method: 'HEAD'})) + .then(function(result) { + assert_request_array_equals( + result, [], + 'Cache.keys should not match HEAD request unless ignoreMethod ' + + 'option is set.'); + }); + }, 'Cache.keys with a HEAD Request'); + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js new file mode 100644 index 0000000000..9ca45903cb --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js @@ -0,0 +1,437 @@ +// META: title=Cache.match +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: script=/common/get-host-info.sub.js +// META: timeout=long + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match('not-present-in-the-cache') + .then(function(result) { + assert_equals(result, undefined, + 'Cache.match failures should resolve with undefined.'); + }); + }, 'Cache.match with no matching entries'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(entries.a.request.url) + .then(function(result) { + assert_response_equals(result, entries.a.response, + 'Cache.match should match by URL.'); + }); + }, 'Cache.match with URL'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(entries.a.request) + .then(function(result) { + assert_response_equals(result, entries.a.response, + 'Cache.match should match by Request.'); + }); + }, 'Cache.match with Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + var alt_response = new Response('', {status: 201}); + + return self.caches.open('second_matching_cache') + .then(function(cache) { + return cache.put(entries.a.request, alt_response.clone()); + }) + .then(function() { + return cache.match(entries.a.request); + }) + .then(function(result) { + assert_response_equals( + result, entries.a.response, + 'Cache.match should match the first cache.'); + }); + }, 'Cache.match with multiple cache hits'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(new Request(entries.a.request.url)) + .then(function(result) { + assert_response_equals(result, entries.a.response, + 'Cache.match should match by Request.'); + }); + }, 'Cache.match with new Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(new Request(entries.a.request.url, {method: 'HEAD'})) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.match should not match HEAD Request.'); + }); + }, 'Cache.match with HEAD'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(entries.a.request, + {ignoreSearch: true}) + .then(function(result) { + assert_response_in_array( + result, + [ + entries.a.response, + entries.a_with_query.response + ], + 'Cache.match with ignoreSearch should ignore the ' + + 'search parameters of cached request.'); + }); + }, + 'Cache.match with ignoreSearch option (request with no search ' + + 'parameters)'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(entries.a_with_query.request, + {ignoreSearch: true}) + .then(function(result) { + assert_response_in_array( + result, + [ + entries.a.response, + entries.a_with_query.response + ], + 'Cache.match with ignoreSearch should ignore the ' + + 'search parameters of request.'); + }); + }, + 'Cache.match with ignoreSearch option (request with search parameter)'); + +cache_test(function(cache) { + var request = new Request('http://example.com/'); + var head_request = new Request('http://example.com/', {method: 'HEAD'}); + var response = new Response('foo'); + return cache.put(request.clone(), response.clone()) + .then(function() { + return cache.match(head_request.clone()); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'Cache.match should resolve as undefined with a ' + + 'mismatched method.'); + return cache.match(head_request.clone(), + {ignoreMethod: true}); + }) + .then(function(result) { + assert_response_equals( + result, response, + 'Cache.match with ignoreMethod should ignore the ' + + 'method of request.'); + }); + }, 'Cache.match supports ignoreMethod'); + +cache_test(function(cache) { + var vary_request = new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}); + var vary_response = new Response('', {headers: {'Vary': 'Cookies'}}); + var mismatched_vary_request = new Request('http://example.com/c'); + + return cache.put(vary_request.clone(), vary_response.clone()) + .then(function() { + return cache.match(mismatched_vary_request.clone()); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'Cache.match should resolve as undefined with a ' + + 'mismatched vary.'); + return cache.match(mismatched_vary_request.clone(), + {ignoreVary: true}); + }) + .then(function(result) { + assert_response_equals( + result, vary_response, + 'Cache.match with ignoreVary should ignore the ' + + 'vary of request.'); + }); + }, 'Cache.match supports ignoreVary'); + +cache_test(function(cache) { + let has_cache_name = false; + const opts = { + get cacheName() { + has_cache_name = true; + return undefined; + } + }; + return self.caches.open('foo') + .then(function() { + return cache.match('bar', opts); + }) + .then(function() { + assert_false(has_cache_name, + 'Cache.match does not support cacheName option ' + + 'which was removed in CacheQueryOptions.'); + }); + }, 'Cache.match does not support cacheName option'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(entries.cat.request.url + '#mouse') + .then(function(result) { + assert_response_equals(result, entries.cat.response, + 'Cache.match should ignore URL fragment.'); + }); + }, 'Cache.match with URL containing fragment'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match('http') + .then(function(result) { + assert_equals( + result, undefined, + 'Cache.match should treat query as a URL and not ' + + 'just a string fragment.'); + }); + }, 'Cache.match with string fragment "http" as query'); + +prepopulated_cache_test(vary_entries, function(cache, entries) { + return cache.match('http://example.com/c') + .then(function(result) { + assert_response_in_array( + result, + [ + entries.vary_cookie_absent.response + ], + 'Cache.match should honor "Vary" header.'); + }); + }, 'Cache.match with responses containing "Vary" header'); + +cache_test(function(cache) { + var request = new Request('http://example.com'); + var response; + var request_url = new URL('./resources/simple.txt', location.href).href; + return fetch(request_url) + .then(function(fetch_result) { + response = fetch_result; + assert_equals( + response.url, request_url, + '[https://fetch.spec.whatwg.org/#dom-response-url] ' + + 'Reponse.url should return the URL of the response.'); + return cache.put(request, response.clone()); + }) + .then(function() { + return cache.match(request.url); + }) + .then(function(result) { + assert_response_equals( + result, response, + 'Cache.match should return a Response object that has the same ' + + 'properties as the stored response.'); + return cache.match(response.url); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'Cache.match should not match cache entry based on response URL.'); + }); + }, 'Cache.match with Request and Response objects with different URLs'); + +cache_test(function(cache) { + var request_url = new URL('./resources/simple.txt', location.href).href; + return fetch(request_url) + .then(function(fetch_result) { + return cache.put(new Request(request_url), fetch_result); + }) + .then(function() { + return cache.match(request_url); + }) + .then(function(result) { + return result.text(); + }) + .then(function(body_text) { + assert_equals(body_text, 'a simple text file\n', + 'Cache.match should return a Response object with a ' + + 'valid body.'); + }) + .then(function() { + return cache.match(request_url); + }) + .then(function(result) { + return result.text(); + }) + .then(function(body_text) { + assert_equals(body_text, 'a simple text file\n', + 'Cache.match should return a Response object with a ' + + 'valid body each time it is called.'); + }); + }, 'Cache.match invoked multiple times for the same Request/Response'); + +cache_test(function(cache) { + var request_url = new URL('./resources/simple.txt', location.href).href; + return fetch(request_url) + .then(function(fetch_result) { + return cache.put(new Request(request_url), fetch_result); + }) + .then(function() { + return cache.match(request_url); + }) + .then(function(result) { + return result.blob(); + }) + .then(function(blob) { + var sliced = blob.slice(2,8); + + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onloadend = function(event) { + resolve(event.target.result); + }; + reader.readAsText(sliced); + }); + }) + .then(function(text) { + assert_equals(text, 'simple', + 'A Response blob returned by Cache.match should be ' + + 'sliceable.' ); + }); + }, 'Cache.match blob should be sliceable'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + var request = new Request(entries.a.request.clone(), {method: 'POST'}); + return cache.match(request) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.match should not find a match'); + }); + }, 'Cache.match with POST Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + var response = entries.non_2xx_response.response; + return cache.match(entries.non_2xx_response.request.url) + .then(function(result) { + assert_response_equals( + result, entries.non_2xx_response.response, + 'Cache.match should return a Response object that has the ' + + 'same properties as a stored non-2xx response.'); + }); + }, 'Cache.match with a non-2xx Response'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + var response = entries.error_response.response; + return cache.match(entries.error_response.request.url) + .then(function(result) { + assert_response_equals( + result, entries.error_response.response, + 'Cache.match should return a Response object that has the ' + + 'same properties as a stored network error response.'); + }); + }, 'Cache.match with a network error Response'); + +cache_test(function(cache) { + // This test validates that we can get a Response from the Cache API, + // clone it, and read just one side of the clone. This was previously + // bugged in FF for Responses with large bodies. + var data = []; + data.length = 80 * 1024; + data.fill('F'); + var response; + return cache.put('/', new Response(data.toString())) + .then(function(result) { + return cache.match('/'); + }) + .then(function(r) { + // Make sure the original response is not GC'd. + response = r; + // Return only the clone. We purposefully test that the other + // half of the clone does not need to be read here. + return response.clone().text(); + }) + .then(function(text) { + assert_equals(text, data.toString(), 'cloned body text can be read correctly'); + }); + }, 'Cache produces large Responses that can be cloned and read correctly.'); + +cache_test(async (cache) => { + const url = get_host_info().HTTPS_REMOTE_ORIGIN + + '/service-workers/cache-storage/resources/simple.txt?pipe=' + + 'header(access-control-allow-origin,*)|' + + 'header(access-control-expose-headers,*)|' + + 'header(foo,bar)|' + + 'header(set-cookie,X)'; + + const response = await fetch(url); + await cache.put(new Request(url), response); + const cached_response = await cache.match(url); + + const headers = cached_response.headers; + assert_equals(headers.get('access-control-expose-headers'), '*'); + assert_equals(headers.get('foo'), 'bar'); + assert_equals(headers.get('set-cookie'), null); + }, 'cors-exposed header should be stored correctly.'); + +cache_test(async (cache) => { + // A URL that should load a resource with a known mime type. + const url = '/service-workers/cache-storage/resources/blank.html'; + const expected_mime_type = 'text/html'; + + // Verify we get the expected mime type from the network. Note, + // we cannot use an exact match here since some browsers append + // character encoding information to the blob.type value. + const net_response = await fetch(url); + const net_mime_type = (await net_response.blob()).type; + assert_true(net_mime_type.includes(expected_mime_type), + 'network response should include the expected mime type'); + + // Verify we get the exact same mime type when reading the same + // URL resource back out of the cache. + await cache.add(url); + const cache_response = await cache.match(url); + const cache_mime_type = (await cache_response.blob()).type; + assert_equals(cache_mime_type, net_mime_type, + 'network and cache response mime types should match'); + }, 'MIME type should be set from content-header correctly.'); + +cache_test(async (cache) => { + const url = '/dummy'; + const original_type = 'text/html'; + const override_type = 'text/plain'; + const init_with_headers = { + headers: { + 'content-type': original_type + } + } + + // Verify constructing a synthetic response with a content-type header + // gets the correct mime type. + const response = new Response('hello world', init_with_headers); + const original_response_type = (await response.blob()).type; + assert_true(original_response_type.includes(original_type), + 'original response should include the expected mime type'); + + // Verify overwriting the content-type header changes the mime type. + const overwritten_response = new Response('hello world', init_with_headers); + overwritten_response.headers.set('content-type', override_type); + const overwritten_response_type = (await overwritten_response.blob()).type; + assert_equals(overwritten_response_type, override_type, + 'mime type can be overridden'); + + // Verify the Response read from Cache uses the original mime type + // computed when it was first constructed. + const tmp = new Response('hello world', init_with_headers); + tmp.headers.set('content-type', override_type); + await cache.put(url, tmp); + const cache_response = await cache.match(url); + const cache_mime_type = (await cache_response.blob()).type; + assert_equals(cache_mime_type, override_type, + 'overwritten and cached response mime types should match'); + }, 'MIME type should reflect Content-Type headers of response.'); + +cache_test(async (cache) => { + const url = new URL('./resources/vary.py?vary=foo', + get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname); + const original_request = new Request(url, { mode: 'no-cors', + headers: { 'foo': 'bar' } }); + const fetch_response = await fetch(original_request); + assert_equals(fetch_response.type, 'opaque'); + + await cache.put(original_request, fetch_response); + + const match_response_1 = await cache.match(original_request); + assert_not_equals(match_response_1, undefined); + + // Verify that cache.match() finds the entry even if queried with a varied + // header that does not match the cache key. Vary headers should be ignored + // for opaque responses. + const different_request = new Request(url, { headers: { 'foo': 'CHANGED' } }); + const match_response_2 = await cache.match(different_request); + assert_not_equals(match_response_2, undefined); +}, 'Cache.match ignores vary headers on opaque response.'); + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js new file mode 100644 index 0000000000..93c5517891 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js @@ -0,0 +1,244 @@ +// META: title=Cache.matchAll +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll('not-present-in-the-cache') + .then(function(result) { + assert_response_array_equals( + result, [], + 'Cache.matchAll should resolve with an empty array on failure.'); + }); + }, 'Cache.matchAll with no matching entries'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a.request.url) + .then(function(result) { + assert_response_array_equals(result, [entries.a.response], + 'Cache.matchAll should match by URL.'); + }); + }, 'Cache.matchAll with URL'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a.request) + .then(function(result) { + assert_response_array_equals( + result, [entries.a.response], + 'Cache.matchAll should match by Request.'); + }); + }, 'Cache.matchAll with Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(new Request(entries.a.request.url)) + .then(function(result) { + assert_response_array_equals( + result, [entries.a.response], + 'Cache.matchAll should match by Request.'); + }); + }, 'Cache.matchAll with new Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(new Request(entries.a.request.url, {method: 'HEAD'}), + {ignoreSearch: true}) + .then(function(result) { + assert_response_array_equals( + result, [], + 'Cache.matchAll should not match HEAD Request.'); + }); + }, 'Cache.matchAll with HEAD'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a.request, + {ignoreSearch: true}) + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.a.response, + entries.a_with_query.response + ], + 'Cache.matchAll with ignoreSearch should ignore the ' + + 'search parameters of cached request.'); + }); + }, + 'Cache.matchAll with ignoreSearch option (request with no search ' + + 'parameters)'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a_with_query.request, + {ignoreSearch: true}) + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.a.response, + entries.a_with_query.response + ], + 'Cache.matchAll with ignoreSearch should ignore the ' + + 'search parameters of request.'); + }); + }, + 'Cache.matchAll with ignoreSearch option (request with search parameters)'); + +cache_test(function(cache) { + var request = new Request('http://example.com/'); + var head_request = new Request('http://example.com/', {method: 'HEAD'}); + var response = new Response('foo'); + return cache.put(request.clone(), response.clone()) + .then(function() { + return cache.matchAll(head_request.clone()); + }) + .then(function(result) { + assert_response_array_equals( + result, [], + 'Cache.matchAll should resolve with empty array for a ' + + 'mismatched method.'); + return cache.matchAll(head_request.clone(), + {ignoreMethod: true}); + }) + .then(function(result) { + assert_response_array_equals( + result, [response], + 'Cache.matchAll with ignoreMethod should ignore the ' + + 'method of request.'); + }); + }, 'Cache.matchAll supports ignoreMethod'); + +cache_test(function(cache) { + var vary_request = new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}); + var vary_response = new Response('', {headers: {'Vary': 'Cookies'}}); + var mismatched_vary_request = new Request('http://example.com/c'); + + return cache.put(vary_request.clone(), vary_response.clone()) + .then(function() { + return cache.matchAll(mismatched_vary_request.clone()); + }) + .then(function(result) { + assert_response_array_equals( + result, [], + 'Cache.matchAll should resolve as undefined with a ' + + 'mismatched vary.'); + return cache.matchAll(mismatched_vary_request.clone(), + {ignoreVary: true}); + }) + .then(function(result) { + assert_response_array_equals( + result, [vary_response], + 'Cache.matchAll with ignoreVary should ignore the ' + + 'vary of request.'); + }); + }, 'Cache.matchAll supports ignoreVary'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.cat.request.url + '#mouse') + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.cat.response, + ], + 'Cache.matchAll should ignore URL fragment.'); + }); + }, 'Cache.matchAll with URL containing fragment'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll('http') + .then(function(result) { + assert_response_array_equals( + result, [], + 'Cache.matchAll should treat query as a URL and not ' + + 'just a string fragment.'); + }); + }, 'Cache.matchAll with string fragment "http" as query'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll() + .then(function(result) { + assert_response_array_equals( + result, + simple_entries.map(entry => entry.response), + 'Cache.matchAll without parameters should match all entries.'); + }); + }, 'Cache.matchAll without parameters'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(undefined) + .then(result => { + assert_response_array_equals( + result, + simple_entries.map(entry => entry.response), + 'Cache.matchAll with undefined request should match all entries.'); + }); + }, 'Cache.matchAll with explicitly undefined request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(undefined, {}) + .then(result => { + assert_response_array_equals( + result, + simple_entries.map(entry => entry.response), + 'Cache.matchAll with undefined request should match all entries.'); + }); + }, 'Cache.matchAll with explicitly undefined request and empty options'); + +prepopulated_cache_test(vary_entries, function(cache, entries) { + return cache.matchAll('http://example.com/c') + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.vary_cookie_absent.response + ], + 'Cache.matchAll should exclude matches if a vary header is ' + + 'missing in the query request, but is present in the cached ' + + 'request.'); + }) + + .then(function() { + return cache.matchAll( + new Request('http://example.com/c', + {headers: {'Cookies': 'none-of-the-above'}})); + }) + .then(function(result) { + assert_response_array_equals( + result, + [ + ], + 'Cache.matchAll should exclude matches if a vary header is ' + + 'missing in the cached request, but is present in the query ' + + 'request.'); + }) + + .then(function() { + return cache.matchAll( + new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}})); + }) + .then(function(result) { + assert_response_array_equals( + result, + [entries.vary_cookie_is_cookie.response], + 'Cache.matchAll should match the entire header if a vary header ' + + 'is present in both the query and cached requests.'); + }); + }, 'Cache.matchAll with responses containing "Vary" header'); + +prepopulated_cache_test(vary_entries, function(cache, entries) { + return cache.matchAll('http://example.com/c', + {ignoreVary: true}) + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.vary_cookie_is_cookie.response, + entries.vary_cookie_is_good.response, + entries.vary_cookie_absent.response + ], + 'Cache.matchAll should support multiple vary request/response ' + + 'pairs.'); + }); + }, 'Cache.matchAll with multiple vary pairs'); + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js new file mode 100644 index 0000000000..dbf2650a75 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js @@ -0,0 +1,411 @@ +// META: title=Cache.put +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=./resources/test-helpers.js +// META: timeout=long + +var test_url = 'https://example.com/foo'; +var test_body = 'Hello world!'; +const { REMOTE_HOST } = get_host_info(); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new Response(test_body); + return cache.put(request, response) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.put should resolve with undefined on success.'); + }); + }, 'Cache.put called with simple Request and Response'); + +cache_test(function(cache) { + var test_url = new URL('./resources/simple.txt', location.href).href; + var request = new Request(test_url); + var response; + return fetch(test_url) + .then(function(fetch_result) { + response = fetch_result.clone(); + return cache.put(request, fetch_result); + }) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, response, + 'Cache.put should update the cache with ' + + 'new request and response.'); + return result.text(); + }) + .then(function(body) { + assert_equals(body, 'a simple text file\n', + 'Cache.put should store response body.'); + }); + }, 'Cache.put called with Request and Response from fetch()'); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new Response(test_body); + assert_false(request.bodyUsed, + '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' + + 'Request.bodyUsed should be initially false.'); + return cache.put(request, response) + .then(function() { + assert_false(request.bodyUsed, + 'Cache.put should not mark empty request\'s body used'); + }); + }, 'Cache.put with Request without a body'); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new Response(); + assert_false(response.bodyUsed, + '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' + + 'Response.bodyUsed should be initially false.'); + return cache.put(request, response) + .then(function() { + assert_false(response.bodyUsed, + 'Cache.put should not mark empty response\'s body used'); + }); + }, 'Cache.put with Response without a body'); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new Response(test_body); + return cache.put(request, response.clone()) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, response, + 'Cache.put should update the cache with ' + + 'new Request and Response.'); + }); + }, 'Cache.put with a Response containing an empty URL'); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new Response('', { + status: 200, + headers: [['Content-Type', 'text/plain']] + }); + return cache.put(request, response) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_equals(result.status, 200, 'Cache.put should store status.'); + assert_equals(result.headers.get('Content-Type'), 'text/plain', + 'Cache.put should store headers.'); + return result.text(); + }) + .then(function(body) { + assert_equals(body, '', + 'Cache.put should store response body.'); + }); + }, 'Cache.put with an empty response body'); + +cache_test(function(cache, test) { + var request = new Request(test_url); + var response = new Response('', { + status: 206, + headers: [['Content-Type', 'text/plain']] + }); + + return promise_rejects_js( + test, + TypeError, + cache.put(request, response), + 'Cache.put should reject 206 Responses with a TypeError.'); + }, 'Cache.put with synthetic 206 response'); + +cache_test(function(cache, test) { + var test_url = new URL('./resources/fetch-status.py?status=206', location.href).href; + var request = new Request(test_url); + var response; + return fetch(test_url) + .then(function(fetch_result) { + assert_equals(fetch_result.status, 206, + 'Test framework error: The status code should be 206.'); + response = fetch_result.clone(); + return promise_rejects_js(test, TypeError, cache.put(request, fetch_result)); + }); + }, 'Cache.put with HTTP 206 response'); + +cache_test(function(cache, test) { + // We need to jump through some hoops to allow the test to perform opaque + // response filtering, but bypass the ORB safelist check. This is + // done, by forcing the MIME type retrieval to fail and the + // validation of partial first response to succeed. + var pipe = "status(206)|header(Content-Type,)|header(Content-Range, bytes 0-1/41)|slice(null, 1)"; + var test_url = new URL(`./resources/blank.html?pipe=${pipe}`, location.href); + test_url.hostname = REMOTE_HOST; + var request = new Request(test_url.href, { mode: 'no-cors' }); + var response; + return fetch(request) + .then(function(fetch_result) { + assert_equals(fetch_result.type, 'opaque', + 'Test framework error: The response type should be opaque.'); + assert_equals(fetch_result.status, 0, + 'Test framework error: The status code should be 0 for an ' + + ' opaque-filtered response. This is actually HTTP 206.'); + response = fetch_result.clone(); + return cache.put(request, fetch_result); + }) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_not_equals(result, undefined, + 'Cache.put should store an entry for the opaque response'); + }); + }, 'Cache.put with opaque-filtered HTTP 206 response'); + +cache_test(function(cache) { + var test_url = new URL('./resources/fetch-status.py?status=500', location.href).href; + var request = new Request(test_url); + var response; + return fetch(test_url) + .then(function(fetch_result) { + assert_equals(fetch_result.status, 500, + 'Test framework error: The status code should be 500.'); + response = fetch_result.clone(); + return cache.put(request, fetch_result); + }) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, response, + 'Cache.put should update the cache with ' + + 'new request and response.'); + return result.text(); + }) + .then(function(body) { + assert_equals(body, '', + 'Cache.put should store response body.'); + }); + }, 'Cache.put with HTTP 500 response'); + +cache_test(function(cache) { + var alternate_response_body = 'New body'; + var alternate_response = new Response(alternate_response_body, + { statusText: 'New status' }); + return cache.put(new Request(test_url), + new Response('Old body', { statusText: 'Old status' })) + .then(function() { + return cache.put(new Request(test_url), alternate_response.clone()); + }) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, alternate_response, + 'Cache.put should replace existing ' + + 'response with new response.'); + return result.text(); + }) + .then(function(body) { + assert_equals(body, alternate_response_body, + 'Cache put should store new response body.'); + }); + }, 'Cache.put called twice with matching Requests and different Responses'); + +cache_test(function(cache) { + var first_url = test_url; + var second_url = first_url + '#(O_o)'; + var third_url = first_url + '#fragment'; + var alternate_response_body = 'New body'; + var alternate_response = new Response(alternate_response_body, + { statusText: 'New status' }); + return cache.put(new Request(first_url), + new Response('Old body', { statusText: 'Old status' })) + .then(function() { + return cache.put(new Request(second_url), alternate_response.clone()); + }) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, alternate_response, + 'Cache.put should replace existing ' + + 'response with new response.'); + return result.text(); + }) + .then(function(body) { + assert_equals(body, alternate_response_body, + 'Cache put should store new response body.'); + }) + .then(function() { + return cache.put(new Request(third_url), alternate_response.clone()); + }) + .then(function() { + return cache.keys(); + }) + .then(function(results) { + // Should match urls (without fragments or with different ones) to the + // same cache key. However, result.url should be the latest url used. + assert_equals(results[0].url, third_url); + return; + }); +}, 'Cache.put called multiple times with request URLs that differ only by a fragment'); + +cache_test(function(cache) { + var url = 'http://example.com/foo'; + return cache.put(url, new Response('some body')) + .then(function() { return cache.match(url); }) + .then(function(response) { return response.text(); }) + .then(function(body) { + assert_equals(body, 'some body', + 'Cache.put should accept a string as request.'); + }); + }, 'Cache.put with a string request'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.put(new Request(test_url), 'Hello world!'), + 'Cache.put should only accept a Response object as the response.'); + }, 'Cache.put with an invalid response'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.put(new Request('file:///etc/passwd'), + new Response(test_body)), + 'Cache.put should reject non-HTTP/HTTPS requests with a TypeError.'); + }, 'Cache.put with a non-HTTP/HTTPS request'); + +cache_test(function(cache) { + var response = new Response(test_body); + return cache.put(new Request('relative-url'), response.clone()) + .then(function() { + return cache.match(new URL('relative-url', location.href).href); + }) + .then(function(result) { + assert_response_equals(result, response, + 'Cache.put should accept a relative URL ' + + 'as the request.'); + }); + }, 'Cache.put with a relative URL'); + +cache_test(function(cache, test) { + var request = new Request('http://example.com/foo', { method: 'HEAD' }); + return promise_rejects_js( + test, + TypeError, + cache.put(request, new Response(test_body)), + 'Cache.put should throw a TypeError for non-GET requests.'); + }, 'Cache.put with a non-GET request'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.put(new Request(test_url), null), + 'Cache.put should throw a TypeError for a null response.'); + }, 'Cache.put with a null response'); + +cache_test(function(cache, test) { + var request = new Request(test_url, {method: 'POST', body: test_body}); + return promise_rejects_js( + test, + TypeError, + cache.put(request, new Response(test_body)), + 'Cache.put should throw a TypeError for a POST request.'); + }, 'Cache.put with a POST request'); + +cache_test(function(cache) { + var response = new Response(test_body); + assert_false(response.bodyUsed, + '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' + + 'Response.bodyUsed should be initially false.'); + return response.text().then(function() { + assert_true( + response.bodyUsed, + '[https://fetch.spec.whatwg.org/#concept-body-consume-body] ' + + 'The text() method should make the body disturbed.'); + var request = new Request(test_url); + return cache.put(request, response).then(() => { + assert_unreached('cache.put should be rejected'); + }, () => {}); + }); + }, 'Cache.put with a used response body'); + +cache_test(function(cache) { + var response = new Response(test_body); + return cache.put(new Request(test_url), response) + .then(function() { + assert_throws_js(TypeError, () => response.body.getReader()); + }); + }, 'getReader() after Cache.put'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.put(new Request(test_url), + new Response(test_body, { headers: { VARY: '*' }})), + 'Cache.put should reject VARY:* Responses with a TypeError.'); + }, 'Cache.put with a VARY:* Response'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.put(new Request(test_url), + new Response(test_body, + { headers: { VARY: 'Accept-Language,*' }})), + 'Cache.put should reject Responses with an embedded VARY:* with a ' + + 'TypeError.'); + }, 'Cache.put with an embedded VARY:* Response'); + +cache_test(async function(cache, test) { + const url = new URL('./resources/vary.py?vary=*', + get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname); + const request = new Request(url, { mode: 'no-cors' }); + const response = await fetch(request); + assert_equals(response.type, 'opaque'); + await cache.put(request, response); + }, 'Cache.put with a VARY:* opaque response should not reject'); + +cache_test(function(cache) { + var url = 'foo.html'; + var redirectURL = 'http://example.com/foo-bar.html'; + var redirectResponse = Response.redirect(redirectURL); + assert_equals(redirectResponse.headers.get('Location'), redirectURL, + 'Response.redirect() should set Location header.'); + return cache.put(url, redirectResponse.clone()) + .then(function() { + return cache.match(url); + }) + .then(function(response) { + assert_response_equals(response, redirectResponse, + 'Redirect response is reproduced by the Cache API'); + assert_equals(response.headers.get('Location'), redirectURL, + 'Location header is preserved by Cache API.'); + }); + }, 'Cache.put should store Response.redirect() correctly'); + +cache_test(async (cache) => { + var request = new Request(test_url); + var response = new Response(new Blob([test_body])); + await cache.put(request, response); + var cachedResponse = await cache.match(request); + assert_equals(await cachedResponse.text(), test_body); + }, 'Cache.put called with simple Request and blob Response'); + +cache_test(async (cache) => { + var formData = new FormData(); + formData.append("name", "value"); + + var request = new Request(test_url); + var response = new Response(formData); + await cache.put(request, response); + var cachedResponse = await cache.match(request); + var cachedResponseText = await cachedResponse.text(); + assert_true(cachedResponseText.indexOf("name=\"name\"\r\n\r\nvalue") !== -1); +}, 'Cache.put called with simple Request and form data Response'); + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js new file mode 100644 index 0000000000..fd59ba464d --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js @@ -0,0 +1,64 @@ +// META: title=Cache.put +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/test-helpers.js +// META: script=/storage/buckets/resources/util.js +// META: timeout=long + +var test_url = 'https://example.com/foo'; +var test_body = 'Hello world!'; +const { REMOTE_HOST } = get_host_info(); + +promise_test(async function(test) { + await prepareForBucketTest(test); + var inboxBucket = await navigator.storageBuckets.open('inbox'); + var draftsBucket = await navigator.storageBuckets.open('drafts'); + + const cacheName = 'attachments'; + const cacheKey = 'receipt1.txt'; + + var inboxCache = await inboxBucket.caches.open(cacheName); + var draftsCache = await draftsBucket.caches.open(cacheName); + + await inboxCache.put(cacheKey, new Response('bread x 2')) + await draftsCache.put(cacheKey, new Response('eggs x 1')); + + return inboxCache.match(cacheKey) + .then(function(result) { + return result.text(); + }) + .then(function(body) { + assert_equals(body, 'bread x 2', 'Wrong cache contents'); + return draftsCache.match(cacheKey); + }) + .then(function(result) { + return result.text(); + }) + .then(function(body) { + assert_equals(body, 'eggs x 1', 'Wrong cache contents'); + }); +}, 'caches from different buckets have different contents'); + +promise_test(async function(test) { + await prepareForBucketTest(test); + var inboxBucket = await navigator.storageBuckets.open('inbox'); + var draftBucket = await navigator.storageBuckets.open('drafts'); + + var caches = inboxBucket.caches; + var attachments = await caches.open('attachments'); + await attachments.put('receipt1.txt', new Response('bread x 2')); + var result = await attachments.match('receipt1.txt'); + assert_equals(await result.text(), 'bread x 2'); + + await navigator.storageBuckets.delete('inbox'); + + await promise_rejects_dom( + test, 'UnknownError', caches.open('attachments')); + + // Also test when `caches` is first accessed after the deletion. + await navigator.storageBuckets.delete('drafts'); + return promise_rejects_dom( + test, 'UnknownError', draftBucket.caches.open('attachments')); +}, 'cache.open promise is rejected when bucket is gone'); + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js new file mode 100644 index 0000000000..f19522be1b --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js @@ -0,0 +1,35 @@ +// META: title=CacheStorage.keys +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +var test_cache_list = + ['', 'example', 'Another cache name', 'A', 'a', 'ex ample']; + +promise_test(function(test) { + return self.caches.keys() + .then(function(keys) { + assert_true(Array.isArray(keys), + 'CacheStorage.keys should return an Array.'); + return Promise.all(keys.map(function(key) { + return self.caches.delete(key); + })); + }) + .then(function() { + return Promise.all(test_cache_list.map(function(key) { + return self.caches.open(key); + })); + }) + + .then(function() { return self.caches.keys(); }) + .then(function(keys) { + assert_true(Array.isArray(keys), + 'CacheStorage.keys should return an Array.'); + assert_array_equals(keys, + test_cache_list, + 'CacheStorage.keys should only return ' + + 'existing caches.'); + }); + }, 'CacheStorage keys'); + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js new file mode 100644 index 0000000000..0c31b72629 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js @@ -0,0 +1,245 @@ +// META: title=CacheStorage.match +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +(function() { + var next_index = 1; + + // Returns a transaction (request, response, and url) for a unique URL. + function create_unique_transaction(test) { + var uniquifier = String(next_index++); + var url = 'http://example.com/' + uniquifier; + + return { + request: new Request(url), + response: new Response('hello'), + url: url + }; + } + + self.create_unique_transaction = create_unique_transaction; +})(); + +cache_test(function(cache) { + var transaction = create_unique_transaction(); + + return cache.put(transaction.request.clone(), transaction.response.clone()) + .then(function() { + return self.caches.match(transaction.request); + }) + .then(function(response) { + assert_response_equals(response, transaction.response, + 'The response should not have changed.'); + }); +}, 'CacheStorageMatch with no cache name provided'); + +cache_test(function(cache) { + var transaction = create_unique_transaction(); + + var test_cache_list = ['a', 'b', 'c']; + return cache.put(transaction.request.clone(), transaction.response.clone()) + .then(function() { + return Promise.all(test_cache_list.map(function(key) { + return self.caches.open(key); + })); + }) + .then(function() { + return self.caches.match(transaction.request); + }) + .then(function(response) { + assert_response_equals(response, transaction.response, + 'The response should not have changed.'); + }); +}, 'CacheStorageMatch from one of many caches'); + +promise_test(function(test) { + var transaction = create_unique_transaction(); + + var test_cache_list = ['x', 'y', 'z']; + return Promise.all(test_cache_list.map(function(key) { + return self.caches.open(key); + })) + .then(function() { return self.caches.open('x'); }) + .then(function(cache) { + return cache.put(transaction.request.clone(), + transaction.response.clone()); + }) + .then(function() { + return self.caches.match(transaction.request, {cacheName: 'x'}); + }) + .then(function(response) { + assert_response_equals(response, transaction.response, + 'The response should not have changed.'); + }) + .then(function() { + return self.caches.match(transaction.request, {cacheName: 'y'}); + }) + .then(function(response) { + assert_equals(response, undefined, + 'Cache y should not have a response for the request.'); + }); +}, 'CacheStorageMatch from one of many caches by name'); + +cache_test(function(cache) { + var transaction = create_unique_transaction(); + return cache.put(transaction.url, transaction.response.clone()) + .then(function() { + return self.caches.match(transaction.request); + }) + .then(function(response) { + assert_response_equals(response, transaction.response, + 'The response should not have changed.'); + }); +}, 'CacheStorageMatch a string request'); + +cache_test(function(cache) { + var transaction = create_unique_transaction(); + return cache.put(transaction.request.clone(), transaction.response.clone()) + .then(function() { + return self.caches.match(new Request(transaction.request.url, + {method: 'HEAD'})); + }) + .then(function(response) { + assert_equals(response, undefined, + 'A HEAD request should not be matched'); + }); +}, 'CacheStorageMatch a HEAD request'); + +promise_test(function(test) { + var transaction = create_unique_transaction(); + return self.caches.match(transaction.request) + .then(function(response) { + assert_equals(response, undefined, + 'The response should not be found.'); + }); +}, 'CacheStorageMatch with no cached entry'); + +promise_test(function(test) { + var transaction = create_unique_transaction(); + return self.caches.delete('foo') + .then(function() { + return self.caches.has('foo'); + }) + .then(function(has_foo) { + assert_false(has_foo, "The cache should not exist."); + return self.caches.match(transaction.request, {cacheName: 'foo'}); + }) + .then(function(response) { + assert_equals(response, undefined, + 'The match with bad cache name should resolve to ' + + 'undefined.'); + return self.caches.has('foo'); + }) + .then(function(has_foo) { + assert_false(has_foo, "The cache should still not exist."); + }); +}, 'CacheStorageMatch with no caches available but name provided'); + +cache_test(function(cache) { + var transaction = create_unique_transaction(); + + return self.caches.delete('') + .then(function() { + return self.caches.has(''); + }) + .then(function(has_cache) { + assert_false(has_cache, "The cache should not exist."); + return cache.put(transaction.request, transaction.response.clone()); + }) + .then(function() { + return self.caches.match(transaction.request, {cacheName: ''}); + }) + .then(function(response) { + assert_equals(response, undefined, + 'The response should not be found.'); + return self.caches.open(''); + }) + .then(function(cache) { + return cache.put(transaction.request, transaction.response); + }) + .then(function() { + return self.caches.match(transaction.request, {cacheName: ''}); + }) + .then(function(response) { + assert_response_equals(response, transaction.response, + 'The response should be matched.'); + return self.caches.delete(''); + }); +}, 'CacheStorageMatch with empty cache name provided'); + +cache_test(function(cache) { + var request = new Request('http://example.com/?foo'); + var no_query_request = new Request('http://example.com/'); + var response = new Response('foo'); + return cache.put(request.clone(), response.clone()) + .then(function() { + return self.caches.match(no_query_request.clone()); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'CacheStorageMatch should resolve as undefined with a ' + + 'mismatched query.'); + return self.caches.match(no_query_request.clone(), + {ignoreSearch: true}); + }) + .then(function(result) { + assert_response_equals( + result, response, + 'CacheStorageMatch with ignoreSearch should ignore the ' + + 'query of the request.'); + }); + }, 'CacheStorageMatch supports ignoreSearch'); + +cache_test(function(cache) { + var request = new Request('http://example.com/'); + var head_request = new Request('http://example.com/', {method: 'HEAD'}); + var response = new Response('foo'); + return cache.put(request.clone(), response.clone()) + .then(function() { + return self.caches.match(head_request.clone()); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'CacheStorageMatch should resolve as undefined with a ' + + 'mismatched method.'); + return self.caches.match(head_request.clone(), + {ignoreMethod: true}); + }) + .then(function(result) { + assert_response_equals( + result, response, + 'CacheStorageMatch with ignoreMethod should ignore the ' + + 'method of request.'); + }); + }, 'Cache.match supports ignoreMethod'); + +cache_test(function(cache) { + var vary_request = new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}); + var vary_response = new Response('', {headers: {'Vary': 'Cookies'}}); + var mismatched_vary_request = new Request('http://example.com/c'); + + return cache.put(vary_request.clone(), vary_response.clone()) + .then(function() { + return self.caches.match(mismatched_vary_request.clone()); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'CacheStorageMatch should resolve as undefined with a ' + + ' mismatched vary.'); + return self.caches.match(mismatched_vary_request.clone(), + {ignoreVary: true}); + }) + .then(function(result) { + assert_response_equals( + result, vary_response, + 'CacheStorageMatch with ignoreVary should ignore the ' + + 'vary of request.'); + }); + }, 'CacheStorageMatch supports ignoreVary'); + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js new file mode 100644 index 0000000000..b7d5af7b53 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js @@ -0,0 +1,239 @@ +// META: title=CacheStorage +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +promise_test(function(t) { + var cache_name = 'cache-storage/foo'; + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function(cache) { + assert_true(cache instanceof Cache, + 'CacheStorage.open should return a Cache.'); + }); + }, 'CacheStorage.open'); + +promise_test(function(t) { + var cache_name = 'cache-storage/bar'; + var first_cache = null; + var second_cache = null; + return self.caches.open(cache_name) + .then(function(cache) { + first_cache = cache; + return self.caches.delete(cache_name); + }) + .then(function() { + return first_cache.add('./resources/simple.txt'); + }) + .then(function() { + return self.caches.keys(); + }) + .then(function(cache_names) { + assert_equals(cache_names.indexOf(cache_name), -1); + return self.caches.open(cache_name); + }) + .then(function(cache) { + second_cache = cache; + return second_cache.keys(); + }) + .then(function(keys) { + assert_equals(keys.length, 0); + return first_cache.keys(); + }) + .then(function(keys) { + assert_equals(keys.length, 1); + // Clean up + return self.caches.delete(cache_name); + }); + }, 'CacheStorage.delete dooms, but does not delete immediately'); + +promise_test(function(t) { + // Note that this test may collide with other tests running in the same + // origin that also uses an empty cache name. + var cache_name = ''; + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function(cache) { + assert_true(cache instanceof Cache, + 'CacheStorage.open should accept an empty name.'); + }); + }, 'CacheStorage.open with an empty name'); + +promise_test(function(t) { + return promise_rejects_js( + t, + TypeError, + self.caches.open(), + 'CacheStorage.open should throw TypeError if called with no arguments.'); + }, 'CacheStorage.open with no arguments'); + +promise_test(function(t) { + var test_cases = [ + { + name: 'cache-storage/lowercase', + should_not_match: + [ + 'cache-storage/Lowercase', + ' cache-storage/lowercase', + 'cache-storage/lowercase ' + ] + }, + { + name: 'cache-storage/has a space', + should_not_match: + [ + 'cache-storage/has' + ] + }, + { + name: 'cache-storage/has\000_in_the_name', + should_not_match: + [ + 'cache-storage/has', + 'cache-storage/has_in_the_name' + ] + } + ]; + return Promise.all(test_cases.map(function(testcase) { + var cache_name = testcase.name; + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function() { + return self.caches.has(cache_name); + }) + .then(function(result) { + assert_true(result, + 'CacheStorage.has should return true for existing ' + + 'cache.'); + }) + .then(function() { + return Promise.all( + testcase.should_not_match.map(function(cache_name) { + return self.caches.has(cache_name) + .then(function(result) { + assert_false(result, + 'CacheStorage.has should only perform ' + + 'exact matches on cache names.'); + }); + })); + }) + .then(function() { + return self.caches.delete(cache_name); + }); + })); + }, 'CacheStorage.has with existing cache'); + +promise_test(function(t) { + return self.caches.has('cheezburger') + .then(function(result) { + assert_false(result, + 'CacheStorage.has should return false for ' + + 'nonexistent cache.'); + }); + }, 'CacheStorage.has with nonexistent cache'); + +promise_test(function(t) { + var cache_name = 'cache-storage/open'; + var cache; + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function(result) { + cache = result; + }) + .then(function() { + return cache.add('./resources/simple.txt'); + }) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function(result) { + assert_true(result instanceof Cache, + 'CacheStorage.open should return a Cache object'); + assert_not_equals(result, cache, + 'CacheStorage.open should return a new Cache ' + + 'object each time its called.'); + return Promise.all([cache.keys(), result.keys()]); + }) + .then(function(results) { + var expected_urls = results[0].map(function(r) { return r.url }); + var actual_urls = results[1].map(function(r) { return r.url }); + assert_array_equals(actual_urls, expected_urls, + 'CacheStorage.open should return a new Cache ' + + 'object for the same backing store.'); + }); + }, 'CacheStorage.open with existing cache'); + +promise_test(function(t) { + var cache_name = 'cache-storage/delete'; + + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function() { return self.caches.delete(cache_name); }) + .then(function(result) { + assert_true(result, + 'CacheStorage.delete should return true after ' + + 'deleting an existing cache.'); + }) + + .then(function() { return self.caches.has(cache_name); }) + .then(function(cache_exists) { + assert_false(cache_exists, + 'CacheStorage.has should return false after ' + + 'fulfillment of CacheStorage.delete promise.'); + }); + }, 'CacheStorage.delete with existing cache'); + +promise_test(function(t) { + return self.caches.delete('cheezburger') + .then(function(result) { + assert_false(result, + 'CacheStorage.delete should return false for a ' + + 'nonexistent cache.'); + }); + }, 'CacheStorage.delete with nonexistent cache'); + +promise_test(function(t) { + var unpaired_name = 'unpaired\uD800'; + var converted_name = 'unpaired\uFFFD'; + + // The test assumes that a cache with converted_name does not + // exist, but if the implementation fails the test then such + // a cache will be created. Start off in a fresh state by + // deleting all caches. + return delete_all_caches() + .then(function() { + return self.caches.has(converted_name); + }) + .then(function(cache_exists) { + assert_false(cache_exists, + 'Test setup failure: cache should not exist'); + }) + .then(function() { return self.caches.open(unpaired_name); }) + .then(function() { return self.caches.keys(); }) + .then(function(keys) { + assert_true(keys.indexOf(unpaired_name) !== -1, + 'keys should include cache with bad name'); + }) + .then(function() { return self.caches.has(unpaired_name); }) + .then(function(cache_exists) { + assert_true(cache_exists, + 'CacheStorage names should be not be converted.'); + }) + .then(function() { return self.caches.has(converted_name); }) + .then(function(cache_exists) { + assert_false(cache_exists, + 'CacheStorage names should be not be converted.'); + }); + }, 'CacheStorage names are DOMStrings not USVStrings'); + +done(); diff --git a/testing/web-platform/tests/service-workers/cache-storage/common.https.window.js b/testing/web-platform/tests/service-workers/cache-storage/common.https.window.js new file mode 100644 index 0000000000..eba312c273 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/common.https.window.js @@ -0,0 +1,44 @@ +// META: title=Cache Storage: Verify that Window and Workers see same storage +// META: timeout=long + +function wait_for_message(worker) { + return new Promise(function(resolve) { + worker.addEventListener('message', function listener(e) { + resolve(e.data); + worker.removeEventListener('message', listener); + }); + }); +} + +promise_test(function(t) { + var cache_name = 'common-test'; + return self.caches.delete(cache_name) + .then(function() { + var worker = new Worker('resources/common-worker.js'); + worker.postMessage({name: cache_name}); + return wait_for_message(worker); + }) + .then(function(message) { + return self.caches.open(cache_name); + }) + .then(function(cache) { + return Promise.all([ + cache.match('https://example.com/a'), + cache.match('https://example.com/b'), + cache.match('https://example.com/c') + ]); + }) + .then(function(responses) { + return Promise.all(responses.map( + function(response) { return response.text(); } + )); + }) + .then(function(bodies) { + assert_equals(bodies[0], 'a', + 'Body should match response put by worker'); + assert_equals(bodies[1], 'b', + 'Body should match response put by worker'); + assert_equals(bodies[2], 'c', + 'Body should match response put by worker'); + }); +}, 'Window sees cache puts by Worker'); diff --git a/testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html b/testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html new file mode 100644 index 0000000000..ec930a87d9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="test-wait"> +<meta charset="utf-8"> +<script type="module"> + const cache = await window.caches.open('cache_name_0') + await cache.add("") + const resp1 = await cache.match("") + const readStream = resp1.body + // Cloning will open the stream via NS_AsyncCopy in Gecko + resp1.clone() + // Give a little bit of time + await new Promise(setTimeout) + // At this point the previous open operation is about to finish but not yet. + // It will finish after the second open operation is made, potentially causing incorrect state. + await readStream.getReader().read(); + document.documentElement.classList.remove('test-wait') +</script> diff --git a/testing/web-platform/tests/service-workers/cache-storage/credentials.https.html b/testing/web-platform/tests/service-workers/cache-storage/credentials.https.html new file mode 100644 index 0000000000..0fe4a0a0ac --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/credentials.https.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Cache Storage: Verify credentials are respected by Cache operations</title> +<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="./../service-worker/resources/test-helpers.sub.js"></script> +<style>iframe { display: none; }</style> +<script> + +var worker = "./resources/credentials-worker.js"; +var scope = "./resources/credentials-iframe.html"; +promise_test(function(t) { + return self.caches.delete('credentials') + .then(function() { + return service_worker_unregister_and_register(t, worker, scope) + }) + .then(function(reg) { + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(frame) { + frame.contentWindow.postMessage([ + {name: 'file.txt', username: 'aa', password: 'bb'}, + {name: 'file.txt', username: 'cc', password: 'dd'}, + {name: 'file.txt'} + ], '*'); + return new Promise(function(resolve, reject) { + window.onmessage = t.step_func(function(e) { + resolve(e.data); + }); + }); + }) + .then(function(data) { + assert_equals(data.length, 3, 'three entries should be present'); + assert_equals(data.filter(function(url) { return /@/.test(url); }).length, 2, + 'two entries should contain credentials'); + assert_true(data.some(function(url) { return /aa:bb@/.test(url); }), + 'entry with credentials aa:bb should be present'); + assert_true(data.some(function(url) { return /cc:dd@/.test(url); }), + 'entry with credentials cc:dd should be present'); + }); +}, "Cache API matching includes credentials"); +</script> diff --git a/testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html b/testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html new file mode 100644 index 0000000000..1cfc256908 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html @@ -0,0 +1,269 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<!-- Pull in executor_path needed by newPopup / newIframe --> +<script src="/html/cross-origin-embedder-policy/credentialless/resources/common.js"></script> +<!-- Pull in importScript / newPopup / newIframe --> +<script src="/html/anonymous-iframe/resources/common.js"></script> +<body> +<script> + +const cache_exists_js = (cache_name, response_queue_name) => ` + try { + const exists = await self.caches.has("${cache_name}"); + if (exists) { + await send("${response_queue_name}", "true"); + } else { + await send("${response_queue_name}", "false"); + } + } catch { + await send("${response_queue_name}", "exception"); + } +`; + +const add_iframe_js = (iframe_origin, response_queue_uuid) => ` + const importScript = ${importScript}; + await importScript("/html/cross-origin-embedder-policy/credentialless" + + "/resources/common.js"); + await importScript("/html/anonymous-iframe/resources/common.js"); + await importScript("/common/utils.js"); + await send("${response_queue_uuid}", newIframe("${iframe_origin}")); +`; + +const same_site_origin = get_host_info().HTTPS_ORIGIN; +const cross_site_origin = get_host_info().HTTPS_NOTSAMESITE_ORIGIN; + +async function create_test_iframes(t, response_queue_uuid) { + + // Create a same-origin iframe in a cross-site popup. + const not_same_site_popup_uuid = newPopup(t, cross_site_origin); + await send(not_same_site_popup_uuid, + add_iframe_js(same_site_origin, response_queue_uuid)); + const iframe_1_uuid = await receive(response_queue_uuid); + + // Create a same-origin iframe in a same-site popup. + const same_origin_popup_uuid = newPopup(t, same_site_origin); + await send(same_origin_popup_uuid, + add_iframe_js(same_site_origin, response_queue_uuid)); + const iframe_2_uuid = await receive(response_queue_uuid); + + return [iframe_1_uuid, iframe_2_uuid]; +} + +promise_test(t => { + return new Promise(async (resolve, reject) => { + try { + const response_queue_uuid = token(); + + const [iframe_1_uuid, iframe_2_uuid] = + await create_test_iframes(t, response_queue_uuid); + + const cache_name = token(); + await self.caches.open(cache_name); + t.add_cleanup(() => self.caches.delete(cache_name)); + + await send(iframe_2_uuid, + cache_exists_js(cache_name, response_queue_uuid)); + if (await receive(response_queue_uuid) !== "true") { + reject("Cache not visible in same-top-level-site iframe"); + } + + await send(iframe_1_uuid, + cache_exists_js(cache_name, response_queue_uuid)); + if (await receive(response_queue_uuid) !== "false") { + reject("Cache visible in not-same-top-level-site iframe"); + } + + resolve(); + } catch (e) { + reject(e); + } + }); +}, "CacheStorage caches shouldn't be shared with a cross-partition iframe"); + +const newWorker = (origin) => { + const worker_token = token(); + const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`; + const worker = new Worker(worker_url); + return worker_token; +} + +promise_test(t => { + return new Promise(async (resolve, reject) => { + try { + const response_queue_uuid = token(); + + const create_worker_js = (origin) => ` + const importScript = ${importScript}; + await importScript("/html/cross-origin-embedder-policy/credentialless" + + "/resources/common.js"); + await importScript("/html/anonymous-iframe/resources/common.js"); + await importScript("/common/utils.js"); + const newWorker = ${newWorker}; + await send("${response_queue_uuid}", newWorker("${origin}")); + `; + + const [iframe_1_uuid, iframe_2_uuid] = + await create_test_iframes(t, response_queue_uuid); + + // Create a dedicated worker in the cross-top-level-site iframe. + await send(iframe_1_uuid, create_worker_js(same_site_origin)); + const worker_1_uuid = await receive(response_queue_uuid); + + // Create a dedicated worker in the same-top-level-site iframe. + await send(iframe_2_uuid, create_worker_js(same_site_origin)); + const worker_2_uuid = await receive(response_queue_uuid); + + const cache_name = token(); + await self.caches.open(cache_name); + t.add_cleanup(() => self.caches.delete(cache_name)); + + await send(worker_2_uuid, + cache_exists_js(cache_name, response_queue_uuid)); + if (await receive(response_queue_uuid) !== "true") { + reject("Cache not visible in same-top-level-site worker"); + } + + await send(worker_1_uuid, + cache_exists_js(cache_name, response_queue_uuid)); + if (await receive(response_queue_uuid) !== "false") { + reject("Cache visible in not-same-top-level-site worker"); + } + resolve(); + } catch (e) { + reject(e); + } + }); +}, "CacheStorage caches shouldn't be shared with a cross-partition dedicated worker"); + +const newSharedWorker = (origin) => { + const worker_token = token(); + const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`; + const worker = new SharedWorker(worker_url, worker_token); + return worker_token; +} + +promise_test(t => { + return new Promise(async (resolve, reject) => { + try { + const response_queue_uuid = token(); + + const create_worker_js = (origin) => ` + const importScript = ${importScript}; + await importScript("/html/cross-origin-embedder-policy/credentialless" + + "/resources/common.js"); + await importScript("/html/anonymous-iframe/resources/common.js"); + await importScript("/common/utils.js"); + const newSharedWorker = ${newSharedWorker}; + await send("${response_queue_uuid}", newSharedWorker("${origin}")); + `; + + const [iframe_1_uuid, iframe_2_uuid] = + await create_test_iframes(t, response_queue_uuid); + + // Create a shared worker in the cross-top-level-site iframe. + await send(iframe_1_uuid, create_worker_js(same_site_origin)); + const worker_1_uuid = await receive(response_queue_uuid); + + // Create a shared worker in the same-top-level-site iframe. + await send(iframe_2_uuid, create_worker_js(same_site_origin)); + const worker_2_uuid = await receive(response_queue_uuid); + + const cache_name = token(); + await self.caches.open(cache_name); + t.add_cleanup(() => self.caches.delete(cache_name)); + + await send(worker_2_uuid, + cache_exists_js(cache_name, response_queue_uuid)); + if (await receive(response_queue_uuid) !== "true") { + reject("Cache not visible in same-top-level-site worker"); + } + + await send(worker_1_uuid, + cache_exists_js(cache_name, response_queue_uuid)); + if (await receive(response_queue_uuid) !== "false") { + reject("Cache visible in not-same-top-level-site worker"); + } + resolve(); + } catch (e) { + reject(e); + } + }); +}, "CacheStorage caches shouldn't be shared with a cross-partition shared worker"); + +const newServiceWorker = async (origin) => { + const worker_token = token(); + const worker_url = origin + executor_service_worker_path + + `&uuid=${worker_token}`; + const worker_url_path = executor_service_worker_path.substring(0, + executor_service_worker_path.lastIndexOf('/')); + const scope = worker_url_path + "/not-used/"; + const reg = await navigator.serviceWorker.register(worker_url, + {'scope': scope}); + return worker_token; +} + +promise_test(t => { + return new Promise(async (resolve, reject) => { + try { + const response_queue_uuid = token(); + + const create_worker_js = (origin) => ` + const importScript = ${importScript}; + await importScript("/html/cross-origin-embedder-policy/credentialless" + + "/resources/common.js"); + await importScript("/html/anonymous-iframe/resources/common.js"); + await importScript("/common/utils.js"); + const newServiceWorker = ${newServiceWorker}; + await send("${response_queue_uuid}", await newServiceWorker("${origin}")); + `; + + const [iframe_1_uuid, iframe_2_uuid] = + await create_test_iframes(t, response_queue_uuid); + + // Create a service worker in the same-top-level-site iframe. + await send(iframe_2_uuid, create_worker_js(same_site_origin)); + const worker_2_uuid = await receive(response_queue_uuid); + + t.add_cleanup(() => + send(worker_2_uuid, "self.registration.unregister();")); + + const cache_name = token(); + await self.caches.open(cache_name); + t.add_cleanup(() => self.caches.delete(cache_name)); + + await send(worker_2_uuid, + cache_exists_js(cache_name, response_queue_uuid)); + if (await receive(response_queue_uuid) !== "true") { + reject("Cache not visible in same-top-level-site worker"); + } + + // Create a service worker in the cross-top-level-site iframe. Note that + // if service workers are unpartitioned then this new service worker would + // replace the one created above. This is why we wait to create the second + // service worker until after we are done with the first one. + await send(iframe_1_uuid, create_worker_js(same_site_origin)); + const worker_1_uuid = await receive(response_queue_uuid); + + t.add_cleanup(() => + send(worker_1_uuid, "self.registration.unregister();")); + + await send(worker_1_uuid, + cache_exists_js(cache_name, response_queue_uuid)); + if (await receive(response_queue_uuid) !== "false") { + reject("Cache visible in not-same-top-level-site worker"); + } + + resolve(); + } catch (e) { + reject(e); + } + }); +}, "CacheStorage caches shouldn't be shared with a cross-partition service worker"); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/blank.html b/testing/web-platform/tests/service-workers/cache-storage/resources/blank.html new file mode 100644 index 0000000000..a3c3a4689a --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/resources/blank.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<title>Empty doc</title> diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js b/testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js new file mode 100644 index 0000000000..ee574d2cb7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js @@ -0,0 +1,22 @@ +self.addEventListener('fetch', (event) => { + const params = new URL(event.request.url).searchParams; + if (params.has('ignore')) { + return; + } + if (!params.has('name')) { + event.respondWith(Promise.reject(TypeError('No name is provided.'))); + return; + } + + event.respondWith(Promise.resolve().then(async () => { + const name = params.get('name'); + await caches.delete('foo'); + const cache = await caches.open('foo'); + await cache.put(event.request, new Response('hello')); + const keys = await cache.keys(); + + const original = event.request[name]; + const stored = keys[0][name]; + return new Response(`original: ${original}, stored: ${stored}`); + })); + }); diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js b/testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js new file mode 100644 index 0000000000..d0e8544b56 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js @@ -0,0 +1,15 @@ +self.onmessage = function(e) { + var cache_name = e.data.name; + + self.caches.open(cache_name) + .then(function(cache) { + return Promise.all([ + cache.put('https://example.com/a', new Response('a')), + cache.put('https://example.com/b', new Response('b')), + cache.put('https://example.com/c', new Response('c')) + ]); + }) + .then(function() { + self.postMessage('ok'); + }); +}; diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html new file mode 100644 index 0000000000..00702df9e5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Controlled frame for Cache API test with credentials</title> +<script> + +function xhr(url, username, password) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(), async = true; + xhr.open('GET', url, async, username, password); + xhr.send(); + xhr.onreadystatechange = function() { + if (xhr.readyState !== XMLHttpRequest.DONE) + return; + if (xhr.status === 200) { + resolve(xhr.responseText); + } else { + reject(new Error(xhr.statusText)); + } + }; + }); +} + +window.onmessage = function(e) { + Promise.all(e.data.map(function(item) { + return xhr(item.name, item.username, item.password); + })) + .then(function() { + navigator.serviceWorker.controller.postMessage('keys'); + navigator.serviceWorker.onmessage = function(e) { + window.parent.postMessage(e.data, '*'); + }; + }); +}; + +</script> +<body> +Hello? Yes, this is iframe. +</body> diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js new file mode 100644 index 0000000000..43965b5fe4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js @@ -0,0 +1,59 @@ +var cache_name = 'credentials'; + +function assert_equals(actual, expected, message) { + if (!Object.is(actual, expected)) + throw Error(message + ': expected: ' + expected + ', actual: ' + actual); +} + +self.onfetch = function(e) { + if (!/\.txt$/.test(e.request.url)) return; + var content = e.request.url; + var cache; + e.respondWith( + self.caches.open(cache_name) + .then(function(result) { + cache = result; + return cache.put(e.request, new Response(content)); + }) + + .then(function() { return cache.match(e.request); }) + .then(function(result) { return result.text(); }) + .then(function(text) { + assert_equals(text, content, 'Cache.match() body should match'); + }) + + .then(function() { return cache.matchAll(e.request); }) + .then(function(results) { + assert_equals(results.length, 1, 'Should have one response'); + return results[0].text(); + }) + .then(function(text) { + assert_equals(text, content, 'Cache.matchAll() body should match'); + }) + + .then(function() { return self.caches.match(e.request); }) + .then(function(result) { return result.text(); }) + .then(function(text) { + assert_equals(text, content, 'CacheStorage.match() body should match'); + }) + + .then(function() { + return new Response('dummy'); + }) + ); +}; + +self.onmessage = function(e) { + if (e.data === 'keys') { + self.caches.open(cache_name) + .then(function(cache) { return cache.keys(); }) + .then(function(requests) { + var urls = requests.map(function(request) { return request.url; }); + self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + client.postMessage(urls); + }); + }); + }); + } +}; diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py b/testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py new file mode 100644 index 0000000000..b7109f4787 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py @@ -0,0 +1,2 @@ +def main(request, response): + return int(request.GET[b"status"]), [], b"" diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html b/testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html new file mode 100644 index 0000000000..a2f1e502bb --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<title>ok</title> +<script> +window.onmessage = function(e) { + var id = e.data.id; + try { + var name = 'checkallowed'; + self.caches.open(name).then(function (cache) { + self.caches.delete(name); + window.parent.postMessage({id: id, result: 'allowed'}, '*'); + }).catch(function(e) { + window.parent.postMessage({id: id, result: 'denied', name: e.name, message: e.message}, '*'); + }); + } catch (e) { + window.parent.postMessage({id: id, result: 'unexpecteddenied', name: e.name, message: e.message}, '*'); + } +}; +</script> diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt b/testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt new file mode 100644 index 0000000000..9e3cb91fb9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt @@ -0,0 +1 @@ +a simple text file diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js b/testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js new file mode 100644 index 0000000000..050ac0b542 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js @@ -0,0 +1,272 @@ +(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() { + 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(function(cache) { return test_function(cache, test); }); + }, description); +} + +// A set of Request/Response pairs to be used with prepopulated_cache_test(). +var simple_entries = [ + { + name: 'a', + request: new Request('http://example.com/a'), + response: new Response('') + }, + + { + name: 'b', + request: new Request('http://example.com/b'), + response: new Response('') + }, + + { + name: 'a_with_query', + request: new Request('http://example.com/a?q=r'), + response: new Response('') + }, + + { + name: 'A', + request: new Request('http://example.com/A'), + response: new Response('') + }, + + { + name: 'a_https', + request: new Request('https://example.com/a'), + response: new Response('') + }, + + { + name: 'a_org', + request: new Request('http://example.org/a'), + response: new Response('') + }, + + { + name: 'cat', + request: new Request('http://example.com/cat'), + response: new Response('') + }, + + { + name: 'catmandu', + request: new Request('http://example.com/catmandu'), + response: new Response('') + }, + + { + name: 'cat_num_lives', + request: new Request('http://example.com/cat?lives=9'), + response: new Response('') + }, + + { + name: 'cat_in_the_hat', + request: new Request('http://example.com/cat/in/the/hat'), + response: new Response('') + }, + + { + name: 'non_2xx_response', + request: new Request('http://example.com/non2xx'), + response: new Response('', {status: 404, statusText: 'nope'}) + }, + + { + name: 'error_response', + request: new Request('http://example.com/error'), + response: Response.error() + }, +]; + +// A set of Request/Response pairs to be used with prepopulated_cache_test(). +// These contain a mix of test cases that use Vary headers. +var vary_entries = [ + { + name: 'vary_cookie_is_cookie', + request: new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}), + response: new Response('', + {headers: {'Vary': 'Cookies'}}) + }, + + { + name: 'vary_cookie_is_good', + request: new Request('http://example.com/c', + {headers: {'Cookies': 'is-good-enough-for-me'}}), + response: new Response('', + {headers: {'Vary': 'Cookies'}}) + }, + + { + name: 'vary_cookie_absent', + request: new Request('http://example.com/c'), + response: new Response('', + {headers: {'Vary': 'Cookies'}}) + } +]; + +// Run |test_function| with a Cache object and a map of entries. Prior to the +// call, the Cache is populated by cache entries from |entries|. The latter is +// expected to be an Object mapping arbitrary keys to objects of the form +// {request: <Request object>, response: <Response object>}. Entries are +// serially added to the cache in the order specified. +// +// |test_function| should return a Promise that can be used with promise_test. +function prepopulated_cache_test(entries, test_function, description) { + cache_test(function(cache) { + var p = Promise.resolve(); + var hash = {}; + entries.forEach(function(entry) { + hash[entry.name] = entry; + p = p.then(function() { + return cache.put(entry.request.clone(), entry.response.clone()) + .catch(function(e) { + assert_unreached( + 'Test setup failed for entry ' + entry.name + ': ' + e + ); + }); + }); + }); + return p + .then(function() { + assert_equals(Object.keys(hash).length, entries.length); + }) + .then(function() { + return test_function(cache, hash); + }); + }, description); +} + +// Helper for testing with Headers objects. Compares Headers instances +// by serializing |expected| and |actual| to arrays and comparing. +function assert_header_equals(actual, expected, description) { + assert_class_string(actual, "Headers", description); + var header; + var actual_headers = []; + var expected_headers = []; + for (header of actual) + actual_headers.push(header[0] + ": " + header[1]); + for (header of expected) + expected_headers.push(header[0] + ": " + header[1]); + assert_array_equals(actual_headers, expected_headers, + description + " Headers differ."); +} + +// Helper for testing with Response objects. Compares simple +// attributes defined on the interfaces, as well as the headers. It +// does not compare the response bodies. +function assert_response_equals(actual, expected, description) { + assert_class_string(actual, "Response", description); + ["type", "url", "status", "ok", "statusText"].forEach(function(attribute) { + assert_equals(actual[attribute], expected[attribute], + description + " Attributes differ: " + attribute + "."); + }); + assert_header_equals(actual.headers, expected.headers, description); +} + +// Assert that the two arrays |actual| and |expected| contain the same +// set of Responses as determined by assert_response_equals. The order +// is not significant. +// +// |expected| is assumed to not contain any duplicates. +function assert_response_array_equivalent(actual, expected, description) { + assert_true(Array.isArray(actual), description); + assert_equals(actual.length, expected.length, description); + expected.forEach(function(expected_element) { + // assert_response_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_response_in_array(expected_element, actual, description); + }); +} + +// Asserts that two arrays |actual| and |expected| contain the same +// set of Responses as determined by assert_response_equals(). The +// corresponding elements must occupy corresponding indices in their +// respective arrays. +function assert_response_array_equals(actual, expected, description) { + assert_true(Array.isArray(actual), description); + assert_equals(actual.length, expected.length, description); + actual.forEach(function(value, index) { + assert_response_equals(value, expected[index], + description + " : object[" + index + "]"); + }); +} + +// Equivalent to assert_in_array, but uses assert_response_equals. +function assert_response_in_array(actual, expected_array, description) { + assert_true(expected_array.some(function(element) { + try { + assert_response_equals(actual, element); + return true; + } catch (e) { + return false; + } + }), description); +} + +// Helper for testing with Request objects. Compares simple +// attributes defined on the interfaces, as well as the headers. +function assert_request_equals(actual, expected, description) { + assert_class_string(actual, "Request", description); + ["url"].forEach(function(attribute) { + assert_equals(actual[attribute], expected[attribute], + description + " Attributes differ: " + attribute + "."); + }); + assert_header_equals(actual.headers, expected.headers, description); +} + +// Asserts that two arrays |actual| and |expected| contain the same +// set of Requests as determined by assert_request_equals(). The +// corresponding elements must occupy corresponding indices in their +// respective arrays. +function assert_request_array_equals(actual, expected, description) { + assert_true(Array.isArray(actual), description); + assert_equals(actual.length, expected.length, description); + actual.forEach(function(value, index) { + assert_request_equals(value, expected[index], + description + " : object[" + index + "]"); + }); +} + +// Deletes all caches, returning a promise indicating success. +function delete_all_caches() { + return self.caches.keys() + .then(function(keys) { + return Promise.all(keys.map(self.caches.delete.bind(self.caches))); + }); +} diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/vary.py b/testing/web-platform/tests/service-workers/cache-storage/resources/vary.py new file mode 100644 index 0000000000..7fde1b1094 --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/resources/vary.py @@ -0,0 +1,25 @@ +def main(request, response): + if b"clear-vary-value-override-cookie" in request.GET: + response.unset_cookie(b"vary-value-override") + return b"vary cookie cleared" + + set_cookie_vary = request.GET.first(b"set-vary-value-override-cookie", + default=b"") + if set_cookie_vary: + response.set_cookie(b"vary-value-override", set_cookie_vary) + return b"vary cookie set" + + # If there is a vary-value-override cookie set, then use its value + # for the VARY header no matter what the query string is set to. This + # override is necessary to test the case when two URLs are identical + # (including query), but differ by VARY header. + cookie_vary = request.cookies.get(b"vary-value-override") + if cookie_vary: + response.headers.set(b"vary", str(cookie_vary)) + else: + # If there is no cookie, then use the query string value, if present. + query_vary = request.GET.first(b"vary", default=b"") + if query_vary: + response.headers.set(b"vary", query_vary) + + return b"vary response" diff --git a/testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html b/testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html new file mode 100644 index 0000000000..098fa89daf --- /dev/null +++ b/testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<title>Cache Storage: Verify access in sandboxed iframes</title> +<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage"> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +function load_iframe(src, sandbox) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement('iframe'); + iframe.onload = function() { resolve(iframe); }; + + iframe.sandbox = sandbox; + iframe.src = src; + + document.documentElement.appendChild(iframe); + }); +} + +function wait_for_message(id) { + return new Promise(function(resolve) { + self.addEventListener('message', function listener(e) { + if (e.data.id === id) { + resolve(e.data); + self.removeEventListener('message', listener); + } + }); + }); +} + +var counter = 0; + +promise_test(function(t) { + return load_iframe('./resources/iframe.html', + 'allow-scripts allow-same-origin') + .then(function(iframe) { + var id = ++counter; + iframe.contentWindow.postMessage({id: id}, '*'); + return wait_for_message(id); + }) + .then(function(message) { + assert_equals( + message.result, 'allowed', + 'Access should be allowed if sandbox has allow-same-origin'); + }); +}, 'Sandboxed iframe with allow-same-origin is allowed access'); + +promise_test(function(t) { + return load_iframe('./resources/iframe.html', + 'allow-scripts') + .then(function(iframe) { + var id = ++counter; + iframe.contentWindow.postMessage({id: id}, '*'); + return wait_for_message(id); + }) + .then(function(message) { + assert_equals( + message.result, 'denied', + 'Access should be denied if sandbox lacks allow-same-origin'); + assert_equals(message.name, 'SecurityError', + 'Failure should be a SecurityError'); + }); +}, 'Sandboxed iframe without allow-same-origin is denied access'); + +</script> diff --git a/testing/web-platform/tests/service-workers/idlharness.https.any.js b/testing/web-platform/tests/service-workers/idlharness.https.any.js new file mode 100644 index 0000000000..8db5d4d10f --- /dev/null +++ b/testing/web-platform/tests/service-workers/idlharness.https.any.js @@ -0,0 +1,53 @@ +// META: global=window,worker +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: script=cache-storage/resources/test-helpers.js +// META: script=service-worker/resources/test-helpers.sub.js +// META: timeout=long + +// https://w3c.github.io/ServiceWorker + +idl_test( + ['service-workers'], + ['dom', 'html'], + async (idl_array, t) => { + self.cacheInstance = await create_temporary_cache(t); + + idl_array.add_objects({ + CacheStorage: ['caches'], + Cache: ['self.cacheInstance'], + ServiceWorkerContainer: ['navigator.serviceWorker'] + }); + + // TODO: Add ServiceWorker and ServiceWorkerRegistration instances for the + // other worker scopes. + if (self.GLOBAL.isWindow()) { + idl_array.add_objects({ + ServiceWorkerRegistration: ['registrationInstance'], + ServiceWorker: ['registrationInstance.installing'] + }); + + const scope = 'service-worker/resources/scope/idlharness'; + const registration = await service_worker_unregister_and_register( + t, 'service-worker/resources/empty-worker.js', scope); + t.add_cleanup(() => registration.unregister()); + + self.registrationInstance = registration; + } else if (self.ServiceWorkerGlobalScope) { + // self.ServiceWorkerGlobalScope should only be defined for the + // ServiceWorker scope, which allows us to detect and test the interfaces + // exposed only for ServiceWorker. + idl_array.add_objects({ + Clients: ['clients'], + ExtendableEvent: ['new ExtendableEvent("type")'], + FetchEvent: ['new FetchEvent("type", { request: new Request("") })'], + ServiceWorkerGlobalScope: ['self'], + ServiceWorkerRegistration: ['registration'], + ServiceWorker: ['serviceWorker'], + // TODO: Test instances of Client and WindowClient, e.g. + // Client: ['self.clientInstance'], + // WindowClient: ['self.windowClientInstance'] + }); + } + } +); diff --git a/testing/web-platform/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html b/testing/web-platform/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html new file mode 100644 index 0000000000..6f44bb17e7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<title>Service Worker: Service-Worker-Allowed header</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +const host_info = get_host_info(); + +// Returns a URL for a service worker script whose Service-Worker-Allowed +// header value is set to |allowed_path|. If |origin| is specified, that origin +// is used. +function build_script_url(allowed_path, origin) { + const script = 'resources/empty-worker.js'; + const url = origin ? `${origin}${base_path()}${script}` : script; + return `${url}?pipe=header(Service-Worker-Allowed,${allowed_path})`; +} + +// register_test is a promise_test that registers a service worker. +function register_test(script, scope, description) { + promise_test(async t => { + t.add_cleanup(() => { + return service_worker_unregister(t, scope); + }); + + const registration = await service_worker_unregister_and_register( + t, script, scope); + assert_true(registration instanceof ServiceWorkerRegistration, 'registered'); + assert_equals(registration.scope, normalizeURL(scope)); + }, description); +} + +// register_fail_test is like register_test but expects a SecurityError. +function register_fail_test(script, scope, description) { + promise_test(async t => { + t.add_cleanup(() => { + return service_worker_unregister(t, scope); + }); + + await service_worker_unregister(t, scope); + await promise_rejects_dom(t, + 'SecurityError', + navigator.serviceWorker.register(script, {scope})); + }, description); +} + +register_test( + build_script_url('/allowed-path'), + '/allowed-path', + 'Registering within Service-Worker-Allowed path'); + +register_test( + build_script_url(new URL('/allowed-path', document.location)), + '/allowed-path', + 'Registering within Service-Worker-Allowed path (absolute URL)'); + +register_test( + build_script_url('../allowed-path-with-parent'), + 'allowed-path-with-parent', + 'Registering within Service-Worker-Allowed path with parent reference'); + +register_fail_test( + build_script_url('../allowed-path'), + '/disallowed-path', + 'Registering outside Service-Worker-Allowed path'), + +register_fail_test( + build_script_url('../allowed-path-with-parent'), + '/allowed-path-with-parent', + 'Registering outside Service-Worker-Allowed path with parent reference'); + +register_fail_test( + build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/'), + 'resources/this-scope-is-normally-allowed', + 'Service-Worker-Allowed is cross-origin to script, registering on a normally allowed scope'); + +register_fail_test( + build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/'), + '/this-scope-is-normally-disallowed', + 'Service-Worker-Allowed is cross-origin to script, registering on a normally disallowed scope'); + +register_fail_test( + build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/cross-origin/', + host_info.HTTPS_REMOTE_ORIGIN), + '/cross-origin/', + 'Service-Worker-Allowed is cross-origin to page, same-origin to script'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html new file mode 100644 index 0000000000..3e3cc8b2b0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<title>ServiceWorkerGlobalScope: close operation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<script> + +service_worker_test( + 'resources/close-worker.js', 'ServiceWorkerGlobalScope: close operation'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html new file mode 100644 index 0000000000..525245fe9e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<title>ServiceWorkerGlobalScope: ExtendableMessageEvent Constructor</title> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../resources/test-helpers.sub.js'></script> +<script> +service_worker_test( + 'resources/extendable-message-event-constructor-worker.js', document.title + ); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html new file mode 100644 index 0000000000..89efd7a4a6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html @@ -0,0 +1,226 @@ +<!DOCTYPE html> +<title>ServiceWorkerGlobalScope: ExtendableMessageEvent</title> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../resources/test-helpers.sub.js'></script> +<script src='./resources/extendable-message-event-utils.js'></script> +<script> +promise_test(function(t) { + var script = 'resources/extendable-message-event-worker.js'; + var scope = 'resources/scope/extendable-message-event-from-toplevel'; + var registration; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(r) { + registration = r; + add_completion_callback(function() { registration.unregister(); }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + var saw_message = new Promise(function(resolve) { + navigator.serviceWorker.onmessage = + function(event) { resolve(event.data); } + }); + var channel = new MessageChannel; + registration.active.postMessage('', [channel.port1]); + return saw_message; + }) + .then(function(results) { + var expected = { + constructor: { name: 'ExtendableMessageEvent' }, + origin: location.origin, + lastEventId: '', + source: { + constructor: { name: 'WindowClient' }, + frameType: 'top-level', + url: location.href, + visibilityState: 'visible', + focused: true + }, + ports: [ { constructor: { name: 'MessagePort' } } ] + }; + ExtendableMessageEventUtils.assert_equals(results, expected); + }); + }, 'Post an extendable message from a top-level client'); + +promise_test(function(t) { + var script = 'resources/extendable-message-event-worker.js'; + var scope = 'resources/scope/extendable-message-event-from-nested'; + var frame; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + add_completion_callback(function() { registration.unregister(); }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + frame = f; + add_completion_callback(function() { frame.remove(); }); + var saw_message = new Promise(function(resolve) { + frame.contentWindow.navigator.serviceWorker.onmessage = + function(event) { resolve(event.data); } + }); + f.contentWindow.navigator.serviceWorker.controller.postMessage(''); + return saw_message; + }) + .then(function(results) { + var expected = { + constructor: { name: 'ExtendableMessageEvent' }, + origin: location.origin, + lastEventId: '', + source: { + constructor: { name: 'WindowClient' }, + url: frame.contentWindow.location.href, + frameType: 'nested', + visibilityState: 'visible', + focused: false + }, + ports: [] + }; + ExtendableMessageEventUtils.assert_equals(results, expected); + }); + }, 'Post an extendable message from a nested client'); + +promise_test(function(t) { + var script = 'resources/extendable-message-event-loopback-worker.js'; + var scope = 'resources/scope/extendable-message-event-loopback'; + var registration; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(r) { + registration = r; + add_completion_callback(function() { registration.unregister(); }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + var results = []; + var saw_message = new Promise(function(resolve) { + navigator.serviceWorker.onmessage = function(event) { + switch (event.data.type) { + case 'record': + results.push(event.data.results); + break; + case 'finish': + resolve(results); + break; + } + }; + }); + registration.active.postMessage({type: 'start'}); + return saw_message; + }) + .then(function(results) { + assert_equals(results.length, 2); + + var expected_trial_1 = { + constructor: { name: 'ExtendableMessageEvent' }, + origin: location.origin, + lastEventId: '', + source: { + constructor: { name: 'ServiceWorker' }, + scriptURL: normalizeURL(script), + state: 'activated' + }, + ports: [] + }; + assert_equals(results[0].trial, 1); + ExtendableMessageEventUtils.assert_equals( + results[0].event, expected_trial_1 + ); + + var expected_trial_2 = { + constructor: { name: 'ExtendableMessageEvent' }, + origin: location.origin, + lastEventId: '', + source: { + constructor: { name: 'ServiceWorker' }, + scriptURL: normalizeURL(script), + state: 'activated' + }, + ports: [], + }; + assert_equals(results[1].trial, 2); + ExtendableMessageEventUtils.assert_equals( + results[1].event, expected_trial_2 + ); + }); + }, 'Post loopback extendable messages'); + +promise_test(function(t) { + var script1 = 'resources/extendable-message-event-ping-worker.js'; + var script2 = 'resources/extendable-message-event-pong-worker.js'; + var scope = 'resources/scope/extendable-message-event-pingpong'; + var registration; + + return service_worker_unregister_and_register(t, script1, scope) + .then(function(r) { + registration = r; + add_completion_callback(function() { registration.unregister(); }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + // A controlled frame is necessary for keeping a waiting worker. + return with_iframe(scope); + }) + .then(function(frame) { + add_completion_callback(function() { frame.remove(); }); + return navigator.serviceWorker.register(script2, {scope: scope}); + }) + .then(function(r) { + return wait_for_state(t, r.installing, 'installed'); + }) + .then(function() { + var results = []; + var saw_message = new Promise(function(resolve) { + navigator.serviceWorker.onmessage = function(event) { + switch (event.data.type) { + case 'record': + results.push(event.data.results); + break; + case 'finish': + resolve(results); + break; + } + }; + }); + registration.active.postMessage({type: 'start'}); + return saw_message; + }) + .then(function(results) { + assert_equals(results.length, 2); + + var expected_ping = { + constructor: { name: 'ExtendableMessageEvent' }, + origin: location.origin, + lastEventId: '', + source: { + constructor: { name: 'ServiceWorker' }, + scriptURL: normalizeURL(script1), + state: 'activated' + }, + ports: [] + }; + assert_equals(results[0].pingOrPong, 'ping'); + ExtendableMessageEventUtils.assert_equals( + results[0].event, expected_ping + ); + + var expected_pong = { + constructor: { name: 'ExtendableMessageEvent' }, + origin: location.origin, + lastEventId: '', + source: { + constructor: { name: 'ServiceWorker' }, + scriptURL: normalizeURL(script2), + state: 'installed' + }, + ports: [] + }; + assert_equals(results[1].pingOrPong, 'pong'); + ExtendableMessageEventUtils.assert_equals( + results[1].event, expected_pong + ); + }); + }, 'Post extendable messages among service workers'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js new file mode 100644 index 0000000000..5ca5f65680 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js @@ -0,0 +1,14 @@ +// META: title=fetch method on the right interface +// META: global=serviceworker + +test(function() { + assert_false(self.hasOwnProperty('fetch'), 'ServiceWorkerGlobalScope ' + + 'instance should not have "fetch" method as its property.'); + assert_inherits(self, 'fetch', 'ServiceWorkerGlobalScope should ' + + 'inherit "fetch" method.'); + assert_own_property(Object.getPrototypeOf(Object.getPrototypeOf(self)), 'fetch', + 'WorkerGlobalScope should have "fetch" propery in its prototype.'); + assert_equals(self.fetch, Object.getPrototypeOf(Object.getPrototypeOf(self)).fetch, + 'ServiceWorkerGlobalScope.fetch should be the same as ' + + 'WorkerGlobalScope.fetch.'); +}, 'Fetch method on the right interface'); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html new file mode 100644 index 0000000000..399820dd2c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<head> +<title>Service Worker: isSecureContext</title> +</head> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +promise_test(async (t) => { + var url = 'isSecureContext.serviceworker.js'; + var scope = 'empty.html'; + var frame_sw, sw_registration; + + await service_worker_unregister(t, scope); + var f = await with_iframe(scope); + t.add_cleanup(function() { + f.remove(); + }); + frame_sw = f.contentWindow.navigator.serviceWorker; + var registration = await navigator.serviceWorker.register(url, {scope: scope}); + sw_registration = registration; + await wait_for_state(t, registration.installing, 'activated'); + fetch_tests_from_worker(sw_registration.active); +}, 'Setting up tests'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js new file mode 100644 index 0000000000..5033594e34 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js @@ -0,0 +1,5 @@ +importScripts("/resources/testharness.js"); + +test(() => { + assert_true(self.isSecureContext, true); +}, "isSecureContext"); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html new file mode 100644 index 0000000000..99dedebf2e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<title>ServiceWorkerGlobalScope: postMessage</title> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../resources/test-helpers.sub.js'></script> +<script> + +promise_test(function(t) { + var script = 'resources/postmessage-loopback-worker.js'; + var scope = 'resources/scope/postmessage-loopback'; + var registration; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + var channel = new MessageChannel(); + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = function(event) { + resolve(event.data); + }; + }); + registration.active.postMessage({port: channel.port2}, + [channel.port2]); + return saw_message; + }) + .then(function(result) { + assert_equals(result, 'OK'); + }); + }, 'Post loopback messages'); + +promise_test(function(t) { + var script1 = 'resources/postmessage-ping-worker.js'; + var script2 = 'resources/postmessage-pong-worker.js'; + var scope = 'resources/scope/postmessage-pingpong'; + var registration; + var frame; + + return service_worker_unregister_and_register(t, script1, scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + // A controlled frame is necessary for keeping a waiting worker. + return with_iframe(scope); + }) + .then(function(f) { + frame = f; + return navigator.serviceWorker.register(script2, {scope: scope}); + }) + .then(function(r) { + return wait_for_state(t, r.installing, 'installed'); + }) + .then(function() { + var channel = new MessageChannel(); + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = function(event) { + resolve(event.data); + }; + }); + registration.active.postMessage({port: channel.port2}, + [channel.port2]); + return saw_message; + }) + .then(function(result) { + assert_equals(result, 'OK'); + frame.remove(); + }); + }, 'Post messages among service workers'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html new file mode 100644 index 0000000000..aa3c74a13b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<title>ServiceWorkerGlobalScope: registration</title> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../resources/test-helpers.sub.js'></script> +<script> + +promise_test(function(t) { + var script = 'resources/registration-attribute-worker.js'; + var scope = 'resources/scope/registration-attribute'; + var registration; + return service_worker_unregister_and_register(t, script, scope) + .then(function(reg) { + registration = reg; + add_result_callback(function() { registration.unregister(); }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(frame) { + var expected_events_seen = [ + 'updatefound', + 'install', + 'statechange(installed)', + 'statechange(activating)', + 'activate', + 'statechange(activated)', + 'fetch', + ]; + + assert_equals( + frame.contentDocument.body.textContent, + expected_events_seen.toString(), + 'Service Worker should respond to fetch'); + frame.remove(); + return registration.unregister(); + }); + }, 'Verify registration attributes on ServiceWorkerGlobalScope'); + +promise_test(function(t) { + var script = 'resources/registration-attribute-worker.js'; + var newer_script = 'resources/registration-attribute-newer-worker.js'; + var scope = 'resources/scope/registration-attribute'; + var newer_worker; + var registration; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(reg) { + registration = reg; + add_result_callback(function() { registration.unregister(); }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return navigator.serviceWorker.register(newer_script, {scope: scope}); + }) + .then(function(reg) { + assert_equals(reg, registration); + newer_worker = registration.installing; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + var channel = new MessageChannel; + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = function(e) { resolve(e.data); }; + }); + newer_worker.postMessage({port: channel.port2}, [channel.port2]); + return saw_message; + }) + .then(function(results) { + var script_url = normalizeURL(script); + var newer_script_url = normalizeURL(newer_script); + var expectations = [ + 'evaluate', + ' installing: empty', + ' waiting: empty', + ' active: ' + script_url, + 'updatefound', + ' installing: ' + newer_script_url, + ' waiting: empty', + ' active: ' + script_url, + 'install', + ' installing: ' + newer_script_url, + ' waiting: empty', + ' active: ' + script_url, + 'statechange(installed)', + ' installing: empty', + ' waiting: ' + newer_script_url, + ' active: ' + script_url, + 'statechange(activating)', + ' installing: empty', + ' waiting: empty', + ' active: ' + newer_script_url, + 'activate', + ' installing: empty', + ' waiting: empty', + ' active: ' + newer_script_url, + 'statechange(activated)', + ' installing: empty', + ' waiting: empty', + ' active: ' + newer_script_url, + ]; + assert_array_equals(results, expectations); + return registration.unregister(); + }); + }, 'Verify registration attributes on ServiceWorkerGlobalScope of the ' + + 'newer worker'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js new file mode 100644 index 0000000000..41a8bc069a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js @@ -0,0 +1,5 @@ +importScripts('../../resources/worker-testharness.js'); + +test(function() { + assert_false('close' in self); +}, 'ServiceWorkerGlobalScope close operation'); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js new file mode 100644 index 0000000000..f6838ffb39 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js @@ -0,0 +1,12 @@ +var source; + +self.addEventListener('message', function(e) { + source = e.source; + throw 'testError'; +}); + +self.addEventListener('error', function(e) { + source.postMessage({ + error: e.error, filename: e.filename, message: e.message, lineno: e.lineno, + colno: e.colno}); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js new file mode 100644 index 0000000000..42da5825c5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js @@ -0,0 +1,197 @@ +importScripts('/resources/testharness.js'); + +const TEST_OBJECT = { wanwan: 123 }; +const CHANNEL1 = new MessageChannel(); +const CHANNEL2 = new MessageChannel(); +const PORTS = [CHANNEL1.port1, CHANNEL1.port2, CHANNEL2.port1]; +function createEvent(initializer) { + if (initializer === undefined) + return new ExtendableMessageEvent('type'); + return new ExtendableMessageEvent('type', initializer); +} + +// These test cases are mostly copied from the following file in the Chromium +// project (as of commit 848ad70823991e0f12b437d789943a4ab24d65bb): +// third_party/WebKit/LayoutTests/fast/events/constructors/message-event-constructor.html + +test(function() { + assert_false(createEvent().bubbles); + assert_false(createEvent().cancelable); + assert_equals(createEvent().data, null); + assert_equals(createEvent().origin, ''); + assert_equals(createEvent().lastEventId, ''); + assert_equals(createEvent().source, null); + assert_array_equals(createEvent().ports, []); +}, 'no initializer specified'); + +test(function() { + assert_false(createEvent({ bubbles: false }).bubbles); + assert_true(createEvent({ bubbles: true }).bubbles); +}, '`bubbles` is specified'); + +test(function() { + assert_false(createEvent({ cancelable: false }).cancelable); + assert_true(createEvent({ cancelable: true }).cancelable); +}, '`cancelable` is specified'); + +test(function() { + assert_equals(createEvent({ data: TEST_OBJECT }).data, TEST_OBJECT); + assert_equals(createEvent({ data: undefined }).data, null); + assert_equals(createEvent({ data: null }).data, null); + assert_equals(createEvent({ data: false }).data, false); + assert_equals(createEvent({ data: true }).data, true); + assert_equals(createEvent({ data: '' }).data, ''); + assert_equals(createEvent({ data: 'chocolate' }).data, 'chocolate'); + assert_equals(createEvent({ data: 12345 }).data, 12345); + assert_equals(createEvent({ data: 18446744073709551615 }).data, + 18446744073709552000); + assert_equals(createEvent({ data: NaN }).data, NaN); + // Note that valueOf() is not called, when the left hand side is + // evaluated. + assert_false( + createEvent({ data: { + valueOf: function() { return TEST_OBJECT; } } }).data == + TEST_OBJECT); + assert_equals(createEvent({ get data(){ return 123; } }).data, 123); + let thrown = { name: 'Error' }; + assert_throws_exactly(thrown, function() { + createEvent({ get data() { throw thrown; } }); }); +}, '`data` is specified'); + +test(function() { + assert_equals(createEvent({ origin: 'melancholy' }).origin, 'melancholy'); + assert_equals(createEvent({ origin: '' }).origin, ''); + assert_equals(createEvent({ origin: null }).origin, 'null'); + assert_equals(createEvent({ origin: false }).origin, 'false'); + assert_equals(createEvent({ origin: true }).origin, 'true'); + assert_equals(createEvent({ origin: 12345 }).origin, '12345'); + assert_equals( + createEvent({ origin: 18446744073709551615 }).origin, + '18446744073709552000'); + assert_equals(createEvent({ origin: NaN }).origin, 'NaN'); + assert_equals(createEvent({ origin: [] }).origin, ''); + assert_equals(createEvent({ origin: [1, 2, 3] }).origin, '1,2,3'); + assert_equals( + createEvent({ origin: { melancholy: 12345 } }).origin, + '[object Object]'); + // Note that valueOf() is not called, when the left hand side is + // evaluated. + assert_equals( + createEvent({ origin: { + valueOf: function() { return 'melancholy'; } } }).origin, + '[object Object]'); + assert_equals( + createEvent({ get origin() { return 123; } }).origin, '123'); + let thrown = { name: 'Error' }; + assert_throws_exactly(thrown, function() { + createEvent({ get origin() { throw thrown; } }); }); +}, '`origin` is specified'); + +test(function() { + assert_equals( + createEvent({ lastEventId: 'melancholy' }).lastEventId, 'melancholy'); + assert_equals(createEvent({ lastEventId: '' }).lastEventId, ''); + assert_equals(createEvent({ lastEventId: null }).lastEventId, 'null'); + assert_equals(createEvent({ lastEventId: false }).lastEventId, 'false'); + assert_equals(createEvent({ lastEventId: true }).lastEventId, 'true'); + assert_equals(createEvent({ lastEventId: 12345 }).lastEventId, '12345'); + assert_equals( + createEvent({ lastEventId: 18446744073709551615 }).lastEventId, + '18446744073709552000'); + assert_equals(createEvent({ lastEventId: NaN }).lastEventId, 'NaN'); + assert_equals(createEvent({ lastEventId: [] }).lastEventId, ''); + assert_equals( + createEvent({ lastEventId: [1, 2, 3] }).lastEventId, '1,2,3'); + assert_equals( + createEvent({ lastEventId: { melancholy: 12345 } }).lastEventId, + '[object Object]'); + // Note that valueOf() is not called, when the left hand side is + // evaluated. + assert_equals( + createEvent({ lastEventId: { + valueOf: function() { return 'melancholy'; } } }).lastEventId, + '[object Object]'); + assert_equals( + createEvent({ get lastEventId() { return 123; } }).lastEventId, + '123'); + let thrown = { name: 'Error' }; + assert_throws_exactly(thrown, function() { + createEvent({ get lastEventId() { throw thrown; } }); }); +}, '`lastEventId` is specified'); + +test(function() { + assert_equals(createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1); + assert_equals( + createEvent({ source: self.registration.active }).source, + self.registration.active); + assert_equals( + createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1); + assert_throws_js( + TypeError, function() { createEvent({ source: this }); }, + 'source should be Client or ServiceWorker or MessagePort'); +}, '`source` is specified'); + +test(function() { + // Valid message ports. + var passed_ports = createEvent({ ports: PORTS}).ports; + assert_equals(passed_ports[0], CHANNEL1.port1); + assert_equals(passed_ports[1], CHANNEL1.port2); + assert_equals(passed_ports[2], CHANNEL2.port1); + assert_array_equals(createEvent({ ports: [] }).ports, []); + assert_array_equals(createEvent({ ports: undefined }).ports, []); + + // Invalid message ports. + assert_throws_js(TypeError, + function() { createEvent({ ports: [1, 2, 3] }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: TEST_OBJECT }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: null }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: this }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: false }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: true }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: '' }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: 'chocolate' }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: 12345 }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: 18446744073709551615 }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: NaN }); }); + assert_throws_js(TypeError, + function() { createEvent({ get ports() { return 123; } }); }); + let thrown = { name: 'Error' }; + assert_throws_exactly(thrown, function() { + createEvent({ get ports() { throw thrown; } }); }); + // Note that valueOf() is not called, when the left hand side is + // evaluated. + var valueOf = function() { return PORTS; }; + assert_throws_js(TypeError, function() { + createEvent({ ports: { valueOf: valueOf } }); }); +}, '`ports` is specified'); + +test(function() { + var initializers = { + bubbles: true, + cancelable: true, + data: TEST_OBJECT, + origin: 'wonderful', + lastEventId: 'excellent', + source: CHANNEL1.port1, + ports: PORTS + }; + assert_equals(createEvent(initializers).bubbles, true); + assert_equals(createEvent(initializers).cancelable, true); + assert_equals(createEvent(initializers).data, TEST_OBJECT); + assert_equals(createEvent(initializers).origin, 'wonderful'); + assert_equals(createEvent(initializers).lastEventId, 'excellent'); + assert_equals(createEvent(initializers).source, CHANNEL1.port1); + assert_equals(createEvent(initializers).ports[0], PORTS[0]); + assert_equals(createEvent(initializers).ports[1], PORTS[1]); + assert_equals(createEvent(initializers).ports[2], PORTS[2]); +}, 'all initial values are specified'); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js new file mode 100644 index 0000000000..13cae88d80 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js @@ -0,0 +1,36 @@ +importScripts('./extendable-message-event-utils.js'); + +self.addEventListener('message', function(event) { + switch (event.data.type) { + case 'start': + self.registration.active.postMessage( + {type: '1st', client_id: event.source.id}); + break; + case '1st': + // 1st loopback message via ServiceWorkerRegistration.active. + var results = { + trial: 1, + event: ExtendableMessageEventUtils.serialize(event) + }; + var client_id = event.data.client_id; + event.source.postMessage({type: '2nd', client_id: client_id}); + event.waitUntil(clients.get(client_id) + .then(function(client) { + client.postMessage({type: 'record', results: results}); + })); + break; + case '2nd': + // 2nd loopback message via ExtendableMessageEvent.source. + var results = { + trial: 2, + event: ExtendableMessageEventUtils.serialize(event) + }; + var client_id = event.data.client_id; + event.waitUntil(clients.get(client_id) + .then(function(client) { + client.postMessage({type: 'record', results: results}); + client.postMessage({type: 'finish'}); + })); + break; + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js new file mode 100644 index 0000000000..d07b22959c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js @@ -0,0 +1,23 @@ +importScripts('./extendable-message-event-utils.js'); + +self.addEventListener('message', function(event) { + switch (event.data.type) { + case 'start': + // Send a ping message to another service worker. + self.registration.waiting.postMessage( + {type: 'ping', client_id: event.source.id}); + break; + case 'pong': + var results = { + pingOrPong: 'pong', + event: ExtendableMessageEventUtils.serialize(event) + }; + var client_id = event.data.client_id; + event.waitUntil(clients.get(client_id) + .then(function(client) { + client.postMessage({type: 'record', results: results}); + client.postMessage({type: 'finish'}); + })); + break; + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js new file mode 100644 index 0000000000..5e9669e83c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js @@ -0,0 +1,18 @@ +importScripts('./extendable-message-event-utils.js'); + +self.addEventListener('message', function(event) { + switch (event.data.type) { + case 'ping': + var results = { + pingOrPong: 'ping', + event: ExtendableMessageEventUtils.serialize(event) + }; + var client_id = event.data.client_id; + event.waitUntil(clients.get(client_id) + .then(function(client) { + client.postMessage({type: 'record', results: results}); + event.source.postMessage({type: 'pong', client_id: client_id}); + })); + break; + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js new file mode 100644 index 0000000000..d6a3b483f5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js @@ -0,0 +1,78 @@ +var ExtendableMessageEventUtils = {}; + +// Create a representation of a given ExtendableMessageEvent that is suitable +// for transmission via the `postMessage` API. +ExtendableMessageEventUtils.serialize = function(event) { + var ports = event.ports.map(function(port) { + return { constructor: { name: port.constructor.name } }; + }); + return { + constructor: { + name: event.constructor.name + }, + origin: event.origin, + lastEventId: event.lastEventId, + source: { + constructor: { + name: event.source.constructor.name + }, + url: event.source.url, + frameType: event.source.frameType, + visibilityState: event.source.visibilityState, + focused: event.source.focused + }, + ports: ports + }; +}; + +// Compare the actual and expected values of an ExtendableMessageEvent that has +// been transformed using the `serialize` function defined in this file. +ExtendableMessageEventUtils.assert_equals = function(actual, expected) { + assert_equals( + actual.constructor.name, expected.constructor.name, 'event constructor' + ); + assert_equals(actual.origin, expected.origin, 'event `origin` property'); + assert_equals( + actual.lastEventId, + expected.lastEventId, + 'event `lastEventId` property' + ); + + assert_equals( + actual.source.constructor.name, + expected.source.constructor.name, + 'event `source` property constructor' + ); + assert_equals( + actual.source.url, expected.source.url, 'event `source` property `url`' + ); + assert_equals( + actual.source.frameType, + expected.source.frameType, + 'event `source` property `frameType`' + ); + assert_equals( + actual.source.visibilityState, + expected.source.visibilityState, + 'event `source` property `visibilityState`' + ); + assert_equals( + actual.source.focused, + expected.source.focused, + 'event `source` property `focused`' + ); + + assert_equals( + actual.ports.length, + expected.ports.length, + 'event `ports` property length' + ); + + for (var idx = 0; idx < expected.ports.length; ++idx) { + assert_equals( + actual.ports[idx].constructor.name, + expected.ports[idx].constructor.name, + 'MessagePort #' + idx + ' constructor' + ); + } +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js new file mode 100644 index 0000000000..f5e7647e3e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js @@ -0,0 +1,5 @@ +importScripts('./extendable-message-event-utils.js'); + +self.addEventListener('message', function(event) { + event.source.postMessage(ExtendableMessageEventUtils.serialize(event)); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js new file mode 100644 index 0000000000..083e9aa2a8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js @@ -0,0 +1,15 @@ +self.addEventListener('message', function(event) { + if ('port' in event.data) { + var port = event.data.port; + + var channel = new MessageChannel(); + channel.port1.onmessage = function(event) { + if ('pong' in event.data) + port.postMessage(event.data.pong); + }; + self.registration.active.postMessage({ping: channel.port2}, + [channel.port2]); + } else if ('ping' in event.data) { + event.data.ping.postMessage({pong: 'OK'}); + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js new file mode 100644 index 0000000000..ebb1eccce2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js @@ -0,0 +1,15 @@ +self.addEventListener('message', function(event) { + if ('port' in event.data) { + var port = event.data.port; + + var channel = new MessageChannel(); + channel.port1.onmessage = function(event) { + if ('pong' in event.data) + port.postMessage(event.data.pong); + }; + + // Send a ping message to another service worker. + self.registration.waiting.postMessage({ping: channel.port2}, + [channel.port2]); + } + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js new file mode 100644 index 0000000000..4a0d90b618 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js @@ -0,0 +1,4 @@ +self.addEventListener('message', function(event) { + if ('ping' in event.data) + event.data.ping.postMessage({pong: 'OK'}); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js new file mode 100644 index 0000000000..44f3e2e8e9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js @@ -0,0 +1,33 @@ +// TODO(nhiroki): stop using global states because service workers can be killed +// at any point. Instead, we could post a message to the page on each event via +// Client object (http://crbug.com/558244). +var results = []; + +function stringify(worker) { + return worker ? worker.scriptURL : 'empty'; +} + +function record(event_name) { + results.push(event_name); + results.push(' installing: ' + stringify(self.registration.installing)); + results.push(' waiting: ' + stringify(self.registration.waiting)); + results.push(' active: ' + stringify(self.registration.active)); +} + +record('evaluate'); + +self.registration.addEventListener('updatefound', function() { + record('updatefound'); + var worker = self.registration.installing; + self.registration.installing.addEventListener('statechange', function() { + record('statechange(' + worker.state + ')'); + }); + }); + +self.addEventListener('install', function(e) { record('install'); }); + +self.addEventListener('activate', function(e) { record('activate'); }); + +self.addEventListener('message', function(e) { + e.data.port.postMessage(results); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js new file mode 100644 index 0000000000..315f437593 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js @@ -0,0 +1,139 @@ +importScripts('../../resources/test-helpers.sub.js'); +importScripts('../../resources/worker-testharness.js'); + +// TODO(nhiroki): stop using global states because service workers can be killed +// at any point. Instead, we could post a message to the page on each event via +// Client object (http://crbug.com/558244). +var events_seen = []; + +// TODO(nhiroki): Move these assertions to registration-attribute.html because +// an assertion failure on the worker is not shown on the result page and +// handled as timeout. See registration-attribute-newer-worker.js for example. + +assert_equals( + self.registration.scope, + normalizeURL('scope/registration-attribute'), + 'On worker script evaluation, registration attribute should be set'); +assert_equals( + self.registration.installing, + null, + 'On worker script evaluation, installing worker should be null'); +assert_equals( + self.registration.waiting, + null, + 'On worker script evaluation, waiting worker should be null'); +assert_equals( + self.registration.active, + null, + 'On worker script evaluation, active worker should be null'); + +self.registration.addEventListener('updatefound', function() { + events_seen.push('updatefound'); + + assert_equals( + self.registration.scope, + normalizeURL('scope/registration-attribute'), + 'On updatefound event, registration attribute should be set'); + assert_equals( + self.registration.installing.scriptURL, + normalizeURL('registration-attribute-worker.js'), + 'On updatefound event, installing worker should be set'); + assert_equals( + self.registration.waiting, + null, + 'On updatefound event, waiting worker should be null'); + assert_equals( + self.registration.active, + null, + 'On updatefound event, active worker should be null'); + + assert_equals( + self.registration.installing.state, + 'installing', + 'On updatefound event, worker should be in the installing state'); + + var worker = self.registration.installing; + self.registration.installing.addEventListener('statechange', function() { + events_seen.push('statechange(' + worker.state + ')'); + }); + }); + +self.addEventListener('install', function(e) { + events_seen.push('install'); + + assert_equals( + self.registration.scope, + normalizeURL('scope/registration-attribute'), + 'On install event, registration attribute should be set'); + assert_equals( + self.registration.installing.scriptURL, + normalizeURL('registration-attribute-worker.js'), + 'On install event, installing worker should be set'); + assert_equals( + self.registration.waiting, + null, + 'On install event, waiting worker should be null'); + assert_equals( + self.registration.active, + null, + 'On install event, active worker should be null'); + + assert_equals( + self.registration.installing.state, + 'installing', + 'On install event, worker should be in the installing state'); + }); + +self.addEventListener('activate', function(e) { + events_seen.push('activate'); + + assert_equals( + self.registration.scope, + normalizeURL('scope/registration-attribute'), + 'On activate event, registration attribute should be set'); + assert_equals( + self.registration.installing, + null, + 'On activate event, installing worker should be null'); + assert_equals( + self.registration.waiting, + null, + 'On activate event, waiting worker should be null'); + assert_equals( + self.registration.active.scriptURL, + normalizeURL('registration-attribute-worker.js'), + 'On activate event, active worker should be set'); + + assert_equals( + self.registration.active.state, + 'activating', + 'On activate event, worker should be in the activating state'); + }); + +self.addEventListener('fetch', function(e) { + events_seen.push('fetch'); + + assert_equals( + self.registration.scope, + normalizeURL('scope/registration-attribute'), + 'On fetch event, registration attribute should be set'); + assert_equals( + self.registration.installing, + null, + 'On fetch event, installing worker should be null'); + assert_equals( + self.registration.waiting, + null, + 'On fetch event, waiting worker should be null'); + assert_equals( + self.registration.active.scriptURL, + normalizeURL('registration-attribute-worker.js'), + 'On fetch event, active worker should be set'); + + assert_equals( + self.registration.active.state, + 'activated', + 'On fetch event, worker should be in the activated state'); + + e.respondWith(new Response(events_seen)); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js new file mode 100644 index 0000000000..6da397dd15 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js @@ -0,0 +1,25 @@ +function matchQuery(query) { + return self.location.href.indexOf(query) != -1; +} + +if (matchQuery('?evaluation')) + self.registration.unregister(); + +self.addEventListener('install', function(e) { + if (matchQuery('?install')) { + // Don't do waitUntil(unregister()) as that would deadlock as specified. + self.registration.unregister(); + } + }); + +self.addEventListener('activate', function(e) { + if (matchQuery('?activate')) + e.waitUntil(self.registration.unregister()); + }); + +self.addEventListener('message', function(e) { + e.waitUntil(self.registration.unregister() + .then(function(result) { + e.data.port.postMessage({result: result}); + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js new file mode 100644 index 0000000000..8be8a1ffeb --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js @@ -0,0 +1,22 @@ +var events_seen = []; + +self.registration.addEventListener('updatefound', function() { + events_seen.push('updatefound'); + }); + +self.addEventListener('activate', function(e) { + events_seen.push('activate'); + }); + +self.addEventListener('fetch', function(e) { + events_seen.push('fetch'); + e.respondWith(new Response(events_seen)); + }); + +self.addEventListener('message', function(e) { + events_seen.push('message'); + self.registration.update(); + }); + +// update() during the script evaluation should be ignored. +self.registration.update(); diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py new file mode 100644 index 0000000000..8a87e1baa4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py @@ -0,0 +1,16 @@ +import os +import time + +from wptserve.utils import isomorphic_decode + +def main(request, response): + # update() does not bypass cache so set the max-age to 0 such that update() + # can find a new version in the network. + headers = [(b'Cache-Control', b'max-age: 0'), + (b'Content-Type', b'application/javascript')] + with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), + u'update-worker.js'), u'r') as file: + script = file.read() + # Return a different script for each access. + return headers, u'// %s\n%s' % (time.time(), script) + diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html new file mode 100644 index 0000000000..988f5466b9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<title>ServiceWorkerGlobalScope: Error event error message</title> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../resources/test-helpers.sub.js'></script> +<script> +promise_test(t => { + var script = 'resources/error-worker.js'; + var scope = 'resources/scope/service-worker-error-event'; + var error_name = 'testError' + return service_worker_unregister_and_register(t, script, scope) + .then(registration => { + var worker = registration.installing; + add_completion_callback(() => { registration.unregister(); }); + return new Promise(function(resolve) { + navigator.serviceWorker.onmessage = resolve; + worker.postMessage(''); + }); + }) + .then(e => { + assert_equals(e.data.error, error_name, 'error type'); + assert_greater_than( + e.data.message.indexOf(error_name), -1, 'error message'); + assert_greater_than( + e.data.filename.indexOf(script), -1, 'filename'); + assert_equals(e.data.lineno, 5, 'error line number'); + assert_equals(e.data.colno, 3, 'error column number'); + }); + }, 'Error handlers inside serviceworker should see the attributes of ' + + 'ErrorEvent'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html new file mode 100644 index 0000000000..1a124d7276 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html @@ -0,0 +1,139 @@ +<!DOCTYPE html> +<title>ServiceWorkerGlobalScope: unregister</title> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../resources/test-helpers.sub.js'></script> +<script> + +promise_test(function(t) { + var script = 'resources/unregister-worker.js?evaluation'; + var scope = 'resources/scope/unregister-on-script-evaluation'; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'redundant'); + }) + .then(function() { + return navigator.serviceWorker.getRegistration(scope); + }) + .then(function(result) { + assert_equals( + result, + undefined, + 'After unregister(), the registration should not found'); + }); + }, 'Unregister on script evaluation'); + +promise_test(function(t) { + var script = 'resources/unregister-worker.js?install'; + var scope = 'resources/scope/unregister-on-install-event'; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'redundant'); + }) + .then(function() { + return navigator.serviceWorker.getRegistration(scope); + }) + .then(function(result) { + assert_equals( + result, + undefined, + 'After unregister(), the registration should not found'); + }); + }, 'Unregister on installing event'); + +promise_test(function(t) { + var script = 'resources/unregister-worker.js?activate'; + var scope = 'resources/scope/unregister-on-activate-event'; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'redundant'); + }) + .then(function() { + return navigator.serviceWorker.getRegistration(scope); + }) + .then(function(result) { + assert_equals( + result, + undefined, + 'After unregister(), the registration should not found'); + }); + }, 'Unregister on activate event'); + +promise_test(function(t) { + var script = 'resources/unregister-worker.js'; + var scope = 'resources/unregister-controlling-worker.html'; + + var controller; + var frame; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + frame = f; + controller = frame.contentWindow.navigator.serviceWorker.controller; + + assert_equals( + controller.scriptURL, + normalizeURL(script), + 'Service worker should control a new document') + + // Wait for the completion of unregister() on the worker. + var channel = new MessageChannel(); + var promise = new Promise(function(resolve) { + channel.port1.onmessage = t.step_func(function(e) { + assert_true(e.data.result, + 'unregister() should successfully finish'); + resolve(); + }); + }); + controller.postMessage({port: channel.port2}, [channel.port2]); + return promise; + }) + .then(function() { + return navigator.serviceWorker.getRegistration(scope); + }) + .then(function(result) { + assert_equals( + result, + undefined, + 'After unregister(), the registration should not found'); + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller, + controller, + 'After unregister(), the worker should still control the document'); + return with_iframe(scope); + }) + .then(function(new_frame) { + assert_equals( + new_frame.contentWindow.navigator.serviceWorker.controller, + null, + 'After unregister(), the worker should not control a new document'); + + frame.remove(); + new_frame.remove(); + }) + }, 'Unregister controlling service worker'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html new file mode 100644 index 0000000000..a7dde22339 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<title>ServiceWorkerGlobalScope: update</title> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../resources/test-helpers.sub.js'></script> +<script> + +promise_test(function(t) { + var script = 'resources/update-worker.py'; + var scope = 'resources/scope/update'; + var registration; + var frame1; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + frame1 = f; + registration.active.postMessage('update'); + return wait_for_update(t, registration); + }) + .then(function() { return with_iframe(scope); }) + .then(function(frame2) { + var expected_events_seen = [ + 'updatefound', // by register(). + 'activate', + 'fetch', + 'message', + 'updatefound', // by update() in the message handler. + 'fetch', + ]; + assert_equals( + frame2.contentDocument.body.textContent, + expected_events_seen.toString(), + 'events seen by the worker'); + frame1.remove(); + frame2.remove(); + }); + }, 'Update a registration on ServiceWorkerGlobalScope'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/about-blank-replacement.https.html b/testing/web-platform/tests/service-workers/service-worker/about-blank-replacement.https.html new file mode 100644 index 0000000000..b6efe3ec56 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/about-blank-replacement.https.html @@ -0,0 +1,181 @@ +<!DOCTYPE html> +<title>Service Worker: about:blank replacement handling</title> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +// This test attempts to verify various initial about:blank document +// creation is accurately reflected via the Clients API. The goal is +// for Clients API to reflect what the browser actually does and not +// to make special cases for the API. +// +// If your browser does not create an about:blank document in certain +// cases then please just mark the test expected fail for now. The +// reuse of globals from about:blank documents to the final load document +// has particularly bad interop at the moment. Hopefully we can evolve +// tests like this to eventually align browsers. + +const worker = 'resources/about-blank-replacement-worker.js'; + +// Helper routine that creates an iframe that internally has some kind +// of nested window. The nested window could be another iframe or +// it could be a popup window. +function createFrameWithNestedWindow(url) { + return new Promise((resolve, reject) => { + let frame = document.createElement('iframe'); + frame.src = url; + document.body.appendChild(frame); + + window.addEventListener('message', function onMsg(evt) { + if (evt.data.type !== 'NESTED_LOADED') { + return; + } + window.removeEventListener('message', onMsg); + if (evt.data.result && evt.data.result.startsWith('failure:')) { + reject(evt.data.result); + return; + } + resolve(frame); + }); + }); +} + +// Helper routine to request the given worker find the client with +// the specified URL using the clients.matchAll() API. +function getClientIdByURL(worker, url) { + return new Promise(resolve => { + navigator.serviceWorker.addEventListener('message', function onMsg(evt) { + if (evt.data.type !== 'GET_CLIENT_ID') { + return; + } + navigator.serviceWorker.removeEventListener('message', onMsg); + resolve(evt.data.result); + }); + worker.postMessage({ type: 'GET_CLIENT_ID', url: url.toString() }); + }); +} + +async function doAsyncTest(t, scope) { + let reg = await service_worker_unregister_and_register(t, worker, scope); + + t.add_cleanup(() => service_worker_unregister(t, scope)); + + await wait_for_state(t, reg.installing, 'activated'); + + // Load the scope as a frame. We expect this in turn to have a nested + // iframe. The service worker will intercept the load of the nested + // iframe and populate its body with the client ID of the initial + // about:blank document it sees via clients.matchAll(). + let frame = await createFrameWithNestedWindow(scope); + let initialResult = frame.contentWindow.nested().document.body.textContent; + assert_false(initialResult.startsWith('failure:'), `result: ${initialResult}`); + + assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL, + frame.contentWindow.nested().navigator.serviceWorker.controller.scriptURL, + 'nested about:blank should have same controlling service worker'); + + // Next, ask the service worker to find the final client ID for the fully + // loaded nested frame. + let nestedURL = new URL(frame.contentWindow.nested().location); + let finalResult = await getClientIdByURL(reg.active, nestedURL); + assert_false(finalResult.startsWith('failure:'), `result: ${finalResult}`); + + // If the nested frame doesn't have a URL to load, then there is no fetch + // event and the body should be empty. We can't verify the final client ID + // against anything. + if (nestedURL.href === 'about:blank' || + nestedURL.href === 'about:srcdoc') { + assert_equals('', initialResult, 'about:blank text content should be blank'); + } + + // If the nested URL is not about:blank, though, then the fetch event handler + // should have populated the body with the client id of the initial about:blank. + // Verify the final client id matches. + else { + assert_equals(initialResult, finalResult, 'client ID values should match'); + } + + frame.remove(); +} + +promise_test(async function(t) { + // Execute a test where the nested frame is simply loaded normally. + await doAsyncTest(t, 'resources/about-blank-replacement-frame.py'); +}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' + + 'matches final Client.'); + +promise_test(async function(t) { + // Execute a test where the nested frame is modified immediately by + // its parent. In this case we add a message listener so the service + // worker can ping the client to verify its existence. This ping-pong + // check is performed during the initial load and when verifying the + // final loaded client. + await doAsyncTest(t, 'resources/about-blank-replacement-ping-frame.py'); +}, 'Initial about:blank modified by parent is controlled, exposed to ' + + 'clients.matchAll(), and matches final Client.'); + +promise_test(async function(t) { + // Execute a test where the nested window is a popup window instead of + // an iframe. This should behave the same as the simple iframe case. + await doAsyncTest(t, 'resources/about-blank-replacement-popup-frame.py'); +}, 'Popup initial about:blank is controlled, exposed to clients.matchAll(), and ' + + 'matches final Client.'); + +promise_test(async function(t) { + const scope = 'resources/about-blank-replacement-uncontrolled-nested-frame.html'; + + let reg = await service_worker_unregister_and_register(t, worker, scope); + + t.add_cleanup(() => service_worker_unregister(t, scope)); + + await wait_for_state(t, reg.installing, 'activated'); + + // Load the scope as a frame. We expect this in turn to have a nested + // iframe. Unlike the other tests in this file the nested iframe URL + // is not covered by a service worker scope. It should end up as + // uncontrolled even though its initial about:blank is controlled. + let frame = await createFrameWithNestedWindow(scope); + let nested = frame.contentWindow.nested(); + let initialResult = nested.document.body.textContent; + + // The nested iframe should not have been intercepted by the service + // worker. The empty.html nested frame has "hello world" for its body. + assert_equals(initialResult.trim(), 'hello world', `result: ${initialResult}`); + + assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null, + 'outer frame should be controlled'); + + assert_equals(nested.navigator.serviceWorker.controller, null, + 'nested frame should not be controlled'); + + frame.remove(); +}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' + + 'final Client is not controlled by a service worker.'); + +promise_test(async function(t) { + // Execute a test where the nested frame is an iframe without a src + // attribute. This simple nested about:blank should still inherit the + // controller and be visible to clients.matchAll(). + await doAsyncTest(t, 'resources/about-blank-replacement-blank-nested-frame.html'); +}, 'Simple about:blank is controlled and is exposed to clients.matchAll().'); + +promise_test(async function(t) { + // Execute a test where the nested frame is an iframe using a non-empty + // srcdoc containing only a tag pair so its textContent is still empty. + // This nested iframe should still inherit the controller and be visible + // to clients.matchAll(). + await doAsyncTest(t, 'resources/about-blank-replacement-srcdoc-nested-frame.html'); +}, 'Nested about:srcdoc is controlled and is exposed to clients.matchAll().'); + +promise_test(async function(t) { + // Execute a test where the nested frame is dynamically added without a src + // attribute. This simple nested about:blank should still inherit the + // controller and be visible to clients.matchAll(). + await doAsyncTest(t, 'resources/about-blank-replacement-blank-dynamic-nested-frame.html'); +}, 'Dynamic about:blank is controlled and is exposed to clients.matchAll().'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html b/testing/web-platform/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html new file mode 100644 index 0000000000..016a52c13c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<title>Service Worker: registration events</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/blank.html'; + var registration; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + var sw = registration.installing; + + return new Promise(t.step_func(function(resolve) { + sw.onstatechange = t.step_func(function() { + if (sw.state === 'installed') { + assert_equals(registration.active, null, + 'installed event should be fired before activating service worker'); + resolve(); + } + }); + })); + }) + .catch(unreached_rejection(t)); + }, 'installed event should be fired before activating service worker'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/activation-after-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/activation-after-registration.https.html new file mode 100644 index 0000000000..29f97e3e3f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/activation-after-registration.https.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<title>Service Worker: Activation occurs after registration</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +promise_test(function(t) { + var scope = 'resources/blank.html'; + var registration; + + return service_worker_unregister_and_register( + t, 'resources/empty-worker.js', scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + assert_equals( + r.installing.state, + 'installing', + 'worker should be in the "installing" state upon registration'); + return wait_for_state(t, r.installing, 'activated'); + }); +}, 'activation occurs after registration'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/activation.https.html b/testing/web-platform/tests/service-workers/service-worker/activation.https.html new file mode 100644 index 0000000000..278454d338 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/activation.https.html @@ -0,0 +1,168 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<title>service worker: activation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +// Returns {registration, iframe}, where |registration| has an active and +// waiting worker. The active worker controls |iframe| and has an inflight +// message event that can be finished by calling +// |registration.active.postMessage('go')|. +function setup_activation_test(t, scope, worker_url) { + var registration; + var iframe; + return navigator.serviceWorker.getRegistration(scope) + .then(r => { + if (r) + return r.unregister(); + }) + .then(() => { + // Create an in-scope iframe. Do this prior to registration to avoid + // racing between an update triggered by navigation and the update() + // call below. + return with_iframe(scope); + }) + .then(f => { + iframe = f; + // Create an active worker. + return navigator.serviceWorker.register(worker_url, { scope: scope }); + }) + .then(r => { + registration = r; + add_result_callback(() => registration.unregister()); + return wait_for_state(t, r.installing, 'activated'); + }) + .then(() => { + // Check that the frame was claimed. + assert_not_equals( + iframe.contentWindow.navigator.serviceWorker.controller, null); + // Create an in-flight request. + registration.active.postMessage('wait'); + // Now there is both a controllee and an in-flight request. + // Initiate an update. + return registration.update(); + }) + .then(() => { + // Wait for a waiting worker. + return wait_for_state(t, registration.installing, 'installed'); + }) + .then(() => { + return wait_for_activation_on_sample_scope(t, self); + }) + .then(() => { + assert_not_equals(registration.waiting, null); + assert_not_equals(registration.active, null); + return Promise.resolve({registration: registration, iframe: iframe}); + }); +} +promise_test(t => { + var scope = 'resources/no-controllee'; + var worker_url = 'resources/mint-new-worker.py'; + var registration; + var iframe; + var new_worker; + return setup_activation_test(t, scope, worker_url) + .then(result => { + registration = result.registration; + iframe = result.iframe; + // Finish the in-flight request. + registration.active.postMessage('go'); + return wait_for_activation_on_sample_scope(t, self); + }) + .then(() => { + // The new worker is still waiting. Remove the frame and it should + // activate. + new_worker = registration.waiting; + assert_equals(new_worker.state, 'installed'); + var reached_active = wait_for_state(t, new_worker, 'activating'); + iframe.remove(); + return reached_active; + }) + .then(() => { + assert_equals(new_worker, registration.active); + }); + }, 'loss of controllees triggers activation'); +promise_test(t => { + var scope = 'resources/no-request'; + var worker_url = 'resources/mint-new-worker.py'; + var registration; + var iframe; + var new_worker; + return setup_activation_test(t, scope, worker_url) + .then(result => { + registration = result.registration; + iframe = result.iframe; + // Remove the iframe. + iframe.remove(); + return new Promise(resolve => setTimeout(resolve, 0)); + }) + .then(() => { + // Finish the request. + new_worker = registration.waiting; + var reached_active = wait_for_state(t, new_worker, 'activating'); + registration.active.postMessage('go'); + return reached_active; + }) + .then(() => { + assert_equals(registration.active, new_worker); + }); + }, 'finishing a request triggers activation'); +promise_test(t => { + var scope = 'resources/skip-waiting'; + var worker_url = 'resources/mint-new-worker.py?skip-waiting'; + var registration; + var iframe; + var new_worker; + return setup_activation_test(t, scope, worker_url) + .then(result => { + registration = result.registration; + iframe = result.iframe; + // Finish the request. The iframe does not need to be removed because + // skipWaiting() was called. + new_worker = registration.waiting; + var reached_active = wait_for_state(t, new_worker, 'activating'); + registration.active.postMessage('go'); + return reached_active; + }) + .then(() => { + assert_equals(registration.active, new_worker); + // Remove the iframe. + iframe.remove(); + }); + }, 'skipWaiting bypasses no controllee requirement'); + +// This test is not really about activation, but otherwise is very +// similar to the other tests here. +promise_test(t => { + var scope = 'resources/unregister'; + var worker_url = 'resources/mint-new-worker.py'; + var registration; + var iframe; + var new_worker; + return setup_activation_test(t, scope, worker_url) + .then(result => { + registration = result.registration; + iframe = result.iframe; + // Remove the iframe. + iframe.remove(); + return registration.unregister(); + }) + .then(() => { + // The unregister operation should wait for the active worker to + // finish processing its events before clearing the registration. + new_worker = registration.waiting; + var reached_redundant = wait_for_state(t, new_worker, 'redundant'); + registration.active.postMessage('go'); + return reached_redundant; + }) + .then(() => { + assert_equals(registration.installing, null); + assert_equals(registration.waiting, null); + assert_equals(registration.active, null); + }); + }, 'finishing a request triggers unregister'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/active.https.html b/testing/web-platform/tests/service-workers/service-worker/active.https.html new file mode 100644 index 0000000000..350a34b802 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/active.https.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<title>ServiceWorker: navigator.serviceWorker.active</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +const SCRIPT = 'resources/empty-worker.js'; +const SCOPE = 'resources/blank.html'; + +// "active" is set +promise_test(async t => { + + t.add_cleanup(async() => { + if (frame) + frame.remove(); + if (registration) + await registration.unregister(); + }); + + await service_worker_unregister(t, SCOPE); + const frame = await with_iframe(SCOPE); + const registration = + await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE}); + await wait_for_state(t, registration.installing, 'activating'); + const container = frame.contentWindow.navigator.serviceWorker; + assert_equals(container.controller, null, 'controller'); + assert_equals(registration.active.state, 'activating', + 'registration.active'); + assert_equals(registration.waiting, null, 'registration.waiting'); + assert_equals(registration.installing, null, 'registration.installing'); + + // FIXME: Add a test for a frame created after installation. + // Should the existing frame ("frame") block activation? +}, 'active is set'); + +// Tests that the ServiceWorker objects returned from active attribute getter +// that represent the same service worker are the same objects. +promise_test(async t => { + const registration1 = + await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + const registration2 = await navigator.serviceWorker.getRegistration(SCOPE); + assert_equals(registration1.active, registration2.active, + 'ServiceWorkerRegistration.active should return the same ' + + 'object'); + await registration1.unregister(); +}, 'The ServiceWorker objects returned from active attribute getter that ' + + 'represent the same service worker are the same objects'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-affect-other-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-affect-other-registration.https.html new file mode 100644 index 0000000000..52555ac271 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/claim-affect-other-registration.https.html @@ -0,0 +1,136 @@ +<!doctype html> +<meta charset=utf-8> +<title>Service Worker: claim() should affect other registration</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +promise_test(function(t) { + var frame; + + var init_scope = 'resources/blank.html?affect-other'; + var init_worker_url = 'resources/empty.js'; + var init_registration; + var init_workers = [undefined, undefined]; + + var claim_scope = 'resources/blank.html?affect-other-registration'; + var claim_worker_url = 'resources/claim-worker.js'; + var claim_registration; + var claim_worker; + + return Promise.resolve() + // Register the first service worker to init_scope. + .then(() => navigator.serviceWorker.register(init_worker_url + '?v1', + {scope: init_scope})) + .then(r => { + init_registration = r; + init_workers[0] = r.installing; + return Promise.resolve() + .then(() => wait_for_state(t, init_workers[0], 'activated')) + .then(() => assert_array_equals([init_registration.active, + init_registration.waiting, + init_registration.installing], + [init_workers[0], + null, + null], + 'Wrong workers.')); + }) + + // Create an iframe as the client of the first service worker of init_scope. + .then(() => with_iframe(claim_scope)) + .then(f => frame = f) + + // Check the controller. + .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration( + normalizeURL(init_scope))) + .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller, + r.active, + '.controller should belong to init_scope.')) + + // Register the second service worker to init_scope. + .then(() => navigator.serviceWorker.register(init_worker_url + '?v2', + {scope: init_scope})) + .then(r => { + assert_equals(r, init_registration, 'Should be the same registration'); + init_workers[1] = r.installing; + return Promise.resolve() + .then(() => wait_for_state(t, init_workers[1], 'installed')) + .then(() => assert_array_equals([init_registration.active, + init_registration.waiting, + init_registration.installing], + [init_workers[0], + init_workers[1], + null], + 'Wrong workers.')); + }) + + // Register a service worker to claim_scope. + .then(() => navigator.serviceWorker.register(claim_worker_url, + {scope: claim_scope})) + .then(r => { + claim_registration = r; + claim_worker = r.installing; + return wait_for_state(t, claim_worker, 'activated') + }) + + // Let claim_worker claim the created iframe. + .then(function() { + var channel = new MessageChannel(); + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = t.step_func(function(e) { + assert_equals(e.data, 'PASS', + 'Worker call to claim() should fulfill.'); + resolve(); + }); + }); + + claim_worker.postMessage({port: channel.port2}, [channel.port2]); + return saw_message; + }) + + // Check the controller. + .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration( + normalizeURL(claim_scope))) + .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller, + r.active, + '.controller should belong to claim_scope.')) + + // Check the status of created registrations and service workers. + .then(() => wait_for_state(t, init_workers[1], 'activated')) + .then(() => { + assert_array_equals([claim_registration.active, + claim_registration.waiting, + claim_registration.installing], + [claim_worker, + null, + null], + 'claim_worker should be the only worker.') + + assert_array_equals([init_registration.active, + init_registration.waiting, + init_registration.installing], + [init_workers[1], + null, + null], + 'The waiting sw should become the active worker.') + + assert_array_equals([init_workers[0].state, + init_workers[1].state, + claim_worker.state], + ['redundant', + 'activated', + 'activated'], + 'Wrong worker states.'); + }) + + // Cleanup and finish testing. + .then(() => frame.remove()) + .then(() => Promise.all([ + init_registration.unregister(), + claim_registration.unregister() + ])) + .then(() => t.done()); +}, 'claim() should affect the originally controlling registration.'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-fetch.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-fetch.https.html new file mode 100644 index 0000000000..ae0082df06 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/claim-fetch.https.html @@ -0,0 +1,90 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +async function tryFetch(fetchFunc, path) { + let response; + try { + response = await fetchFunc(path); + } catch (err) { + throw (`fetch() threw: ${err}`); + } + + let responseText; + try { + responseText = await response.text(); + } catch (err) { + throw (`text() threw: ${err}`); + } + + return responseText; +} + +promise_test(async function(t) { + const scope = 'resources/'; + const script = 'resources/claim-worker.js'; + const resource = 'simple.txt'; + + // Create the test frame. + const frame = await with_iframe('resources/blank.html'); + t.add_cleanup(() => frame.remove()); + + // Check the controller and test with fetch. + assert_equals(frame.contentWindow.navigator.controller, undefined, + 'Should have no controller.'); + let response; + try { + response = await tryFetch(frame.contentWindow.fetch, resource); + } catch (err) { + assert_unreached(`uncontrolled fetch failed: ${err}`); + } + assert_equals(response, 'a simple text file\n', + 'fetch() should not be intercepted.'); + + // Register a service worker. + const registration = + await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => registration.unregister()); + const worker = registration.installing; + await wait_for_state(t, worker, 'activated'); + + // Register a controllerchange event to wait until the controller is updated + // and check if the frame is controlled by a service worker. + const controllerChanged = new Promise((resolve) => { + frame.contentWindow.navigator.serviceWorker.oncontrollerchange = () => { + resolve(frame.contentWindow.navigator.serviceWorker.controller); + }; + }); + + // Tell the service worker to claim the iframe. + const sawMessage = new Promise((resolve) => { + const channel = new MessageChannel(); + channel.port1.onmessage = t.step_func((event) => { + resolve(event.data); + }); + worker.postMessage({port: channel.port2}, [channel.port2]); + }); + const data = await sawMessage; + assert_equals(data, 'PASS', 'Worker call to claim() should fulfill.'); + + // Check if the controller is updated after claim() and test with fetch. + const controller = await controllerChanged; + assert_true(controller instanceof frame.contentWindow.ServiceWorker, + 'iframe should be controlled.'); + try { + response = await tryFetch(frame.contentWindow.fetch, resource); + } catch (err) { + assert_unreached(`controlled fetch failed: ${err}`); + } + assert_equals(response, 'Intercepted!', + 'fetch() should be intercepted.'); +}, 'fetch() should be intercepted after the client is claimed.'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-not-using-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-not-using-registration.https.html new file mode 100644 index 0000000000..fd61d05ba4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/claim-not-using-registration.https.html @@ -0,0 +1,131 @@ +<!DOCTYPE html> +<title>Service Worker: claim client not using registration</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +promise_test(function(t) { + var init_scope = 'resources/blank.html?not-using-init'; + var claim_scope = 'resources/blank.html?not-using'; + var init_worker_url = 'resources/empty.js'; + var claim_worker_url = 'resources/claim-worker.js'; + var claim_worker, claim_registration, frame1, frame2; + return service_worker_unregister_and_register( + t, init_worker_url, init_scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, init_scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return Promise.all( + [with_iframe(init_scope), with_iframe(claim_scope)]); + }) + .then(function(frames) { + frame1 = frames[0]; + frame2 = frames[1]; + assert_equals( + frame1.contentWindow.navigator.serviceWorker.controller.scriptURL, + normalizeURL(init_worker_url), + 'Frame1 controller should not be null'); + assert_equals( + frame2.contentWindow.navigator.serviceWorker.controller, null, + 'Frame2 controller should be null'); + return navigator.serviceWorker.register(claim_worker_url, + {scope: claim_scope}); + }) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, claim_scope); + }); + + claim_worker = registration.installing; + claim_registration = registration; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + var saw_controllerchanged = new Promise(function(resolve) { + frame2.contentWindow.navigator.serviceWorker.oncontrollerchange = + function() { resolve(); } + }); + var channel = new MessageChannel(); + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = t.step_func(function(e) { + assert_equals(e.data, 'PASS', + 'Worker call to claim() should fulfill.'); + resolve(); + }); + }); + claim_worker.postMessage({port: channel.port2}, [channel.port2]); + return Promise.all([saw_controllerchanged, saw_message]); + }) + .then(function() { + assert_equals( + frame1.contentWindow.navigator.serviceWorker.controller.scriptURL, + normalizeURL(init_worker_url), + 'Frame1 should not be influenced'); + assert_equals( + frame2.contentWindow.navigator.serviceWorker.controller.scriptURL, + normalizeURL(claim_worker_url), + 'Frame2 should be controlled by the new registration'); + frame1.remove(); + frame2.remove(); + return claim_registration.unregister(); + }); + }, 'Test claim client which is not using registration'); + +promise_test(function(t) { + var scope = 'resources/blank.html?longer-matched'; + var claim_scope = 'resources/blank.html?longer'; + var claim_worker_url = 'resources/claim-worker.js'; + var installing_worker_url = 'resources/empty-worker.js'; + var frame, claim_worker; + return with_iframe(scope) + .then(function(f) { + frame = f; + return navigator.serviceWorker.register( + claim_worker_url, {scope: claim_scope}); + }) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, claim_scope); + }); + + claim_worker = registration.installing; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return navigator.serviceWorker.register( + installing_worker_url, {scope: scope}); + }) + .then(function() { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + var channel = new MessageChannel(); + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = t.step_func(function(e) { + assert_equals(e.data, 'PASS', + 'Worker call to claim() should fulfill.'); + resolve(); + }); + }); + claim_worker.postMessage({port: channel.port2}, [channel.port2]); + return saw_message; + }) + .then(function() { + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller, null, + 'Frame should not be claimed when a longer-matched ' + + 'registration exists'); + frame.remove(); + }); + }, 'Test claim client when there\'s a longer-matched registration not ' + + 'already used by the page'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html new file mode 100644 index 0000000000..f5f44886ba --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html @@ -0,0 +1,71 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +promise_test(function(t) { + var frame; + var resource = 'simple.txt'; + + var worker; + var scope = 'resources/'; + var script = 'resources/claim-worker.js'; + + return Promise.resolve() + // Create the test iframe with a shared worker. + .then(() => with_iframe('resources/claim-shared-worker-fetch-iframe.html')) + .then(f => frame = f) + + // Check the controller and test with fetch in the shared worker. + .then(() => assert_equals(frame.contentWindow.navigator.controller, + undefined, + 'Should have no controller.')) + .then(() => frame.contentWindow.fetch_in_shared_worker(resource)) + .then(response_text => assert_equals(response_text, + 'a simple text file\n', + 'fetch() should not be intercepted.')) + // Register a service worker. + .then(() => service_worker_unregister_and_register(t, script, scope)) + .then(r => { + t.add_cleanup(() => service_worker_unregister(t, scope)); + + worker = r.installing; + + return wait_for_state(t, worker, 'activated') + }) + // Let the service worker claim the iframe and the shared worker. + .then(() => { + var channel = new MessageChannel(); + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = t.step_func(function(e) { + assert_equals(e.data, 'PASS', + 'Worker call to claim() should fulfill.'); + resolve(); + }); + }); + worker.postMessage({port: channel.port2}, [channel.port2]); + return saw_message; + }) + + // Check the controller and test with fetch in the shared worker. + .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(scope)) + .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller, + r.active, + 'Test iframe should be claimed.')) + // TODO(horo): Check the SharedWorker's navigator.seviceWorker.controller. + .then(() => frame.contentWindow.fetch_in_shared_worker(resource)) + .then(response_text => + assert_equals(response_text, + 'Intercepted!', + 'fetch() in the shared worker should be intercepted.')) + + // Cleanup this testcase. + .then(() => frame.remove()); +}, 'fetch() in SharedWorker should be intercepted after the client is claimed.') + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-using-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-using-registration.https.html new file mode 100644 index 0000000000..8a2a6ff25c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/claim-using-registration.https.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<title>Service Worker: claim client using registration</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +promise_test(function(t) { + var scope = 'resources/'; + var frame_url = 'resources/blank.html?using-different-registration'; + var url1 = 'resources/empty.js'; + var url2 = 'resources/claim-worker.js'; + var worker, sw_registration, frame; + return service_worker_unregister_and_register(t, url1, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(frame_url); + }) + .then(function(f) { + frame = f; + return navigator.serviceWorker.register(url2, {scope: frame_url}); + }) + .then(function(registration) { + worker = registration.installing; + sw_registration = registration; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + var saw_controllerchanged = new Promise(function(resolve) { + frame.contentWindow.navigator.serviceWorker.oncontrollerchange = + function() { resolve(); } + }); + var channel = new MessageChannel(); + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = t.step_func(function(e) { + assert_equals(e.data, 'PASS', + 'Worker call to claim() should fulfill.'); + resolve(); + }); + }); + worker.postMessage({port: channel.port2}, [channel.port2]); + return Promise.all([saw_controllerchanged, saw_message]); + }) + .then(function() { + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller.scriptURL, + normalizeURL(url2), + 'Frame1 controller scriptURL should be changed to url2'); + frame.remove(); + return sw_registration.unregister(); + }); + }, 'Test worker claims client which is using another registration'); + +promise_test(function(t) { + var scope = 'resources/blank.html?using-same-registration'; + var url1 = 'resources/empty.js'; + var url2 = 'resources/claim-worker.js'; + var frame, worker; + return service_worker_unregister_and_register(t, url1, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(f) { + frame = f; + return navigator.serviceWorker.register(url2, {scope: scope}); + }) + .then(function(registration) { + worker = registration.installing; + return wait_for_state(t, registration.installing, 'installed'); + }) + .then(function() { + var channel = new MessageChannel(); + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = t.step_func(function(e) { + assert_equals(e.data, 'FAIL: exception: InvalidStateError', + 'Worker call to claim() should reject with ' + + 'InvalidStateError'); + resolve(); + }); + }); + worker.postMessage({port: channel.port2}, [channel.port2]); + return saw_message; + }) + .then(function() { + frame.remove(); + }); + }, 'Test for the waiting worker claims a client which is using the the ' + + 'same registration'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-with-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-with-redirect.https.html new file mode 100644 index 0000000000..fd89cb9b00 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/claim-with-redirect.https.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<title>Service Worker: Claim() when update happens after redirect</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +var host_info = get_host_info(); +var BASE_URL = host_info['HTTPS_ORIGIN'] + base_path(); +var OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path(); + +var WORKER_URL = OTHER_BASE_URL + 'resources/update-claim-worker.py' +var SCOPE_URL = OTHER_BASE_URL + 'resources/redirect.py' +var OTHER_IFRAME_URL = OTHER_BASE_URL + + 'resources/claim-with-redirect-iframe.html'; + +// Different origin from the registration +var REDIRECT_TO_URL = BASE_URL + + 'resources/claim-with-redirect-iframe.html?redirected'; + +var REGISTER_IFRAME_URL = OTHER_IFRAME_URL + '?register=' + + encodeURIComponent(WORKER_URL) + '&' + + 'scope=' + encodeURIComponent(SCOPE_URL); +var REDIRECT_IFRAME_URL = SCOPE_URL + '?Redirect=' + + encodeURIComponent(REDIRECT_TO_URL); +var UPDATE_IFRAME_URL = OTHER_IFRAME_URL + '?update=' + + encodeURIComponent(SCOPE_URL); +var UNREGISTER_IFRAME_URL = OTHER_IFRAME_URL + '?unregister=' + + encodeURIComponent(SCOPE_URL); + +var waiting_resolver = undefined; + +addEventListener('message', e => { + if (waiting_resolver !== undefined) { + waiting_resolver(e.data); + } + }); + +function assert_with_iframe(url, expected_message) { + return new Promise(resolve => { + waiting_resolver = resolve; + with_iframe(url); + }) + .then(data => assert_equals(data.message, expected_message)); +} + +// This test checks behavior when browser got a redirect header from in-scope +// page and navigated to out-of-scope page which has a different origin from any +// registrations. +promise_test(t => { + return assert_with_iframe(REGISTER_IFRAME_URL, 'registered') + .then(() => assert_with_iframe(REDIRECT_IFRAME_URL, 'redirected')) + .then(() => assert_with_iframe(UPDATE_IFRAME_URL, 'updated')) + .then(() => assert_with_iframe(UNREGISTER_IFRAME_URL, 'unregistered')); + }, 'Claim works after redirection to another origin'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-worker-fetch.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-worker-fetch.https.html new file mode 100644 index 0000000000..7cb26c742b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/claim-worker-fetch.https.html @@ -0,0 +1,83 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +promise_test((t) => { + return runTest(t, 'resources/claim-worker-fetch-iframe.html'); +}, 'fetch() in Worker should be intercepted after the client is claimed.'); + +promise_test((t) => { + return runTest(t, 'resources/claim-nested-worker-fetch-iframe.html'); +}, 'fetch() in nested Worker should be intercepted after the client is claimed.'); + +promise_test((t) => { + return runTest(t, 'resources/claim-blob-url-worker-fetch-iframe.html'); +}, 'fetch() in blob URL Worker should be intercepted after the client is claimed.'); + +promise_test((t) => { + return runTest(t, 'resources/nested-blob-url-workers.html'); +}, 'fetch() in nested blob URL Worker created from a blob URL Worker should be intercepted after the client is claimed.'); + +promise_test((t) => { + return runTest(t, 'resources/nested-worker-created-from-blob-url-worker.html'); +}, 'fetch() in nested Worker created from a blob URL Worker should be intercepted after the client is claimed.'); + +promise_test((t) => { + return runTest(t, 'resources/nested-blob-url-worker-created-from-worker.html'); +}, 'fetch() in nested blob URL Worker created from a Worker should be intercepted after the client is claimed.'); + +async function runTest(t, iframe_url) { + const resource = 'simple.txt'; + const scope = 'resources/'; + const script = 'resources/claim-worker.js'; + + // Create the test iframe with a dedicated worker. + const frame = await with_iframe(iframe_url); + t.add_cleanup(_ => frame.remove()); + + // Check the controller and test with fetch in the worker. + assert_equals(frame.contentWindow.navigator.controller, + undefined, 'Should have no controller.'); + { + const response_text = await frame.contentWindow.fetch_in_worker(resource); + assert_equals(response_text, 'a simple text file\n', + 'fetch() should not be intercepted.'); + } + + // Register a service worker. + const reg = await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(_ => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + // Let the service worker claim the iframe and the worker. + const channel = new MessageChannel(); + const saw_message = new Promise(function(resolve) { + channel.port1.onmessage = t.step_func(function(e) { + assert_equals(e.data, 'PASS', 'Worker call to claim() should fulfill.'); + resolve(); + }); + }); + reg.active.postMessage({port: channel.port2}, [channel.port2]); + await saw_message; + + // Check the controller and test with fetch in the worker. + const reg2 = + await frame.contentWindow.navigator.serviceWorker.getRegistration(scope); + assert_equals(frame.contentWindow.navigator.serviceWorker.controller, + reg2.active, 'Test iframe should be claimed.'); + + { + // TODO(horo): Check the worker's navigator.seviceWorker.controller. + const response_text = await frame.contentWindow.fetch_in_worker(resource); + assert_equals(response_text, 'Intercepted!', + 'fetch() in the worker should be intercepted.'); + } +} + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/client-id.https.html b/testing/web-platform/tests/service-workers/service-worker/client-id.https.html new file mode 100644 index 0000000000..b93b341899 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/client-id.https.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<title>Service Worker: Client.id</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var scope = 'resources/blank.html?client-id'; +var frame1, frame2; + +promise_test(function(t) { + return service_worker_unregister_and_register( + t, 'resources/client-id-worker.js', scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(scope + '#1'); }) + .then(function(f) { + frame1 = f; + // To be sure Clients.matchAll() iterates in the same order. + f.focus(); + return with_iframe(scope + '#2'); + }) + .then(function(f) { + frame2 = f; + var channel = new MessageChannel(); + + return new Promise(function(resolve, reject) { + channel.port1.onmessage = resolve; + channel.port1.onmessageerror = reject; + f.contentWindow.navigator.serviceWorker.controller.postMessage( + {port:channel.port2}, [channel.port2]); + }); + }) + .then(on_message); + }, 'Client.id returns the client\'s ID.'); + +function on_message(e) { + // The result of two sequential clients.matchAll() calls in the SW. + // 1st matchAll() results in e.data[0], e.data[1]. + // 2nd matchAll() results in e.data[2], e.data[3]. + assert_equals(e.data.length, 4); + // All should be string values. + assert_equals(typeof e.data[0], 'string'); + assert_equals(typeof e.data[1], 'string'); + assert_equals(typeof e.data[2], 'string'); + assert_equals(typeof e.data[3], 'string'); + // Different clients should have different ids. + assert_not_equals(e.data[0], e.data[1]); + assert_not_equals(e.data[2], e.data[3]); + // Same clients should have an identical id. + assert_equals(e.data[0], e.data[2]); + assert_equals(e.data[1], e.data[3]); + frame1.remove(); + frame2.remove(); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/client-navigate.https.html b/testing/web-platform/tests/service-workers/service-worker/client-navigate.https.html new file mode 100644 index 0000000000..f40a08635c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/client-navigate.https.html @@ -0,0 +1,107 @@ +<!doctype html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<title>Service Worker: WindowClient.navigate</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + function wait_for_message(msg) { + return new Promise(function(resolve, reject) { + var get_message_data = function get_message_data(e) { + window.removeEventListener("message", get_message_data); + resolve(e.data); + } + window.addEventListener("message", get_message_data, false); + }); + } + + function run_test(controller, clientId, test) { + return new Promise(function(resolve, reject) { + var channel = new MessageChannel(); + channel.port1.onmessage = function(e) { + resolve(e.data); + }; + var message = { + port: channel.port2, + test: test, + clientId: clientId, + }; + controller.postMessage( + message, [channel.port2]); + }); + } + + async function with_controlled_iframe_and_url(t, name, f) { + const SCRIPT = "resources/client-navigate-worker.js"; + const SCOPE = "resources/client-navigate-frame.html"; + + // Register service worker and wait for it to become activated + const registration = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + t.add_cleanup(() => registration.unregister()); + const worker = registration.installing; + await wait_for_state(t, worker, 'activated'); + + // Create child iframe and make sure we register a listener for the message + // it sends before it's created + const client_id_promise = wait_for_message(); + const iframe = await with_iframe(SCOPE); + t.add_cleanup(() => iframe.remove()); + const { id } = await client_id_promise; + + // Run the test in the service worker and fetch it + const { result, url } = await run_test(worker, id, name); + fetch_tests_from_worker(worker); + assert_equals(result, name); + + // Hand over the iframe and URL from the service worker to the callback + await f(iframe, url); + } + + promise_test(function(t) { + return with_controlled_iframe_and_url(t, 'test_client_navigate_success', async (iframe, url) => { + assert_equals( + url, new URL("resources/client-navigated-frame.html", + location).toString()); + assert_equals( + iframe.contentWindow.location.href, + new URL("resources/client-navigated-frame.html", + location).toString()); + }); + }, "Frame location should update on successful navigation"); + + promise_test(function(t) { + return with_controlled_iframe_and_url(t, 'test_client_navigate_redirect', async (iframe, url) => { + assert_equals(url, ""); + assert_throws_dom("SecurityError", function() { return iframe.contentWindow.location.href }); + }); + }, "Frame location should not be accessible after redirect"); + + promise_test(function(t) { + return with_controlled_iframe_and_url(t, 'test_client_navigate_cross_origin', async (iframe, url) => { + assert_equals(url, ""); + assert_throws_dom("SecurityError", function() { return iframe.contentWindow.location.href }); + }); + }, "Frame location should not be accessible after cross-origin navigation"); + + promise_test(function(t) { + return with_controlled_iframe_and_url(t, 'test_client_navigate_about_blank', async (iframe, url) => { + assert_equals( + iframe.contentWindow.location.href, + new URL("resources/client-navigate-frame.html", + location).toString()); + iframe.contentWindow.document.body.style = "background-color: green" + }); + }, "Frame location should not update on failed about:blank navigation"); + + promise_test(function(t) { + return with_controlled_iframe_and_url(t, 'test_client_navigate_mixed_content', async (iframe, url) => { + assert_equals( + iframe.contentWindow.location.href, + new URL("resources/client-navigate-frame.html", + location).toString()); + iframe.contentWindow.document.body.style = "background-color: green" + }); + }, "Frame location should not update on failed mixed-content navigation"); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html b/testing/web-platform/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html new file mode 100644 index 0000000000..97a2fcf98f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<title>Service Worker: client.url of a worker created from a blob URL</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +const SCRIPT = 'resources/client-url-of-blob-url-worker.js'; +const SCOPE = 'resources/client-url-of-blob-url-worker.html'; + +promise_test(async (t) => { + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + t.add_cleanup(_ => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + const frame = await with_iframe(SCOPE); + t.add_cleanup(_ => frame.remove()); + assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, + null, 'frame should be controlled'); + + const response = await frame.contentWindow.createAndFetchFromBlobWorker(); + + assert_not_equals(response.result, 'one worker client should exist', + 'worker client should exist'); + assert_equals(response.result, response.expected, + 'client.url and worker location href should be the same'); + +}, 'Client.url of a blob URL worker should be a blob URL.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-get-client-types.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-get-client-types.https.html new file mode 100644 index 0000000000..63e3e51b32 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-get-client-types.https.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.get with window and worker clients</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var scope = 'resources/clients-get-client-types'; +var frame_url = scope + '-frame.html'; +var shared_worker_url = scope + '-shared-worker.js'; +var worker_url = scope + '-worker.js'; +var client_ids = []; +var registration; +var frame; +promise_test(function(t) { + return service_worker_unregister_and_register( + t, 'resources/clients-get-worker.js', scope) + .then(function(r) { + registration = r; + add_completion_callback(function() { registration.unregister(); }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(frame_url); + }) + .then(function(f) { + frame = f; + add_completion_callback(function() { frame.remove(); }); + frame.focus(); + return wait_for_clientId(); + }) + .then(function(client_id) { + client_ids.push(client_id); + return new Promise(function(resolve) { + var w = new SharedWorker(shared_worker_url); + w.port.onmessage = function(e) { + resolve(e.data.clientId); + }; + }); + }) + .then(function(client_id) { + client_ids.push(client_id); + var channel = new MessageChannel(); + var w = new Worker(worker_url); + w.postMessage({cmd:'GetClientId', port:channel.port2}, + [channel.port2]); + return new Promise(function(resolve) { + channel.port1.onmessage = function(e) { + resolve(e.data.clientId); + }; + }); + }) + .then(function(client_id) { + client_ids.push(client_id); + var channel = new MessageChannel(); + frame.contentWindow.postMessage('StartWorker', '*', [channel.port2]); + return new Promise(function(resolve) { + channel.port1.onmessage = function(e) { + resolve(e.data.clientId); + }; + }); + }) + .then(function(client_id) { + client_ids.push(client_id); + var saw_message = new Promise(function(resolve) { + navigator.serviceWorker.onmessage = resolve; + }); + registration.active.postMessage({clientIds: client_ids}); + return saw_message; + }) + .then(function(e) { + assert_equals(e.data.length, expected.length); + // We use these assert_not_equals because assert_array_equals doesn't + // print the error description when passed an undefined value. + assert_not_equals(e.data[0], undefined, + 'Window client should not be undefined'); + assert_array_equals(e.data[0], expected[0], 'Window client'); + assert_not_equals(e.data[1], undefined, + 'Shared worker client should not be undefined'); + assert_array_equals(e.data[1], expected[1], 'Shared worker client'); + assert_not_equals(e.data[2], undefined, + 'Worker(Started by main frame) client should not be undefined'); + assert_array_equals(e.data[2], expected[2], + 'Worker(Started by main frame) client'); + assert_not_equals(e.data[3], undefined, + 'Worker(Started by sub frame) client should not be undefined'); + assert_array_equals(e.data[3], expected[3], + 'Worker(Started by sub frame) client'); + }); + }, 'Test Clients.get() with window and worker clients'); + +function wait_for_clientId() { + return new Promise(function(resolve) { + function get_client_id(e) { + window.removeEventListener('message', get_client_id); + resolve(e.data.clientId); + } + window.addEventListener('message', get_client_id, false); + }); +} + +var expected = [ + // visibilityState, focused, url, type, frameType + ['visible', true, normalizeURL(scope) + '-frame.html', 'window', 'nested'], + [undefined, undefined, normalizeURL(scope) + '-shared-worker.js', 'sharedworker', 'none'], + [undefined, undefined, normalizeURL(scope) + '-worker.js', 'worker', 'none'], + [undefined, undefined, normalizeURL(scope) + '-frame-worker.js', 'worker', 'none'] +]; +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-get-cross-origin.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-get-cross-origin.https.html new file mode 100644 index 0000000000..1e4acfb286 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-get-cross-origin.https.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.get across origins</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var host_info = get_host_info(); + +var scope = 'resources/clients-get-frame.html'; +var other_origin_iframe = host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + + 'resources/clients-get-cross-origin-frame.html'; +// The ID of a client from the same origin as us. +var my_origin_client_id; +// This test asserts the behavior of the Client API in cases where the client +// belongs to a foreign origin. It does this by creating an iframe with a +// foreign origin which connects to a server worker in the current origin. +promise_test(function(t) { + return service_worker_unregister_and_register( + t, 'resources/clients-get-worker.js', scope) + .then(function(registration) { + add_completion_callback(function() { registration.unregister(); }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + // Create a same-origin client and use it to populate |my_origin_client_id|. + .then(function(frame1) { + add_completion_callback(function() { frame1.remove(); }); + return new Promise(function(resolve, reject) { + function get_client_id(e) { + window.removeEventListener('message', get_client_id); + resolve(e.data.clientId); + } + window.addEventListener('message', get_client_id, false); + }); + }) + // Create a cross-origin client. We'll communicate with this client to + // test the cross-origin service worker's behavior. + .then(function(client_id) { + my_origin_client_id = client_id; + return with_iframe(other_origin_iframe); + }) + // Post the 'getClientId' message to the cross-origin client. The client + // will then ask its service worker to look up |my_origin_client_id| via + // Clients#get. Since this client ID is for a different origin, we expect + // the client to not be found. + .then(function(frame2) { + add_completion_callback(function() { frame2.remove(); }); + + frame2.contentWindow.postMessage( + {clientId: my_origin_client_id, type: 'getClientId'}, + host_info['HTTPS_REMOTE_ORIGIN'] + ); + + return new Promise(function(resolve) { + window.addEventListener('message', function(e) { + if (e.data && e.data.type === 'clientId') { + resolve(e.data.value); + } + }); + }); + }) + .then(function(client_id) { + assert_equals(client_id, undefined, 'iframe client ID'); + }); + }, 'Test Clients.get() cross origin'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-get-resultingClientId.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-get-resultingClientId.https.html new file mode 100644 index 0000000000..3419cf14b5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-get-resultingClientId.https.html @@ -0,0 +1,177 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test clients.get(resultingClientId)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +const scope = "resources/"; +let worker; + +// Setup. Keep this as the first promise_test. +promise_test(async (t) => { + const registration = await service_worker_unregister_and_register( + t, 'resources/get-resultingClientId-worker.js', + scope); + worker = registration.installing; + await wait_for_state(t, worker, 'activated'); +}, 'global setup'); + +// Sends |command| to the worker and returns a promise that resolves to its +// response. There should only be one inflight command at a time. +async function sendCommand(command) { + const saw_message = new Promise((resolve) => { + navigator.serviceWorker.onmessage = (event) => { + resolve(event.data); + }; + }); + worker.postMessage(command); + return saw_message; +} + +// Wrapper for 'startTest' command. Tells the worker a test is starting, +// so it resets state and keeps itself alive until 'finishTest'. +async function startTest(t) { + const result = await sendCommand({command: 'startTest'}); + assert_equals(result, 'ok', 'startTest'); + + t.add_cleanup(async () => { + return finishTest(); + }); +} + +// Wrapper for 'finishTest' command. +async function finishTest() { + const result = await sendCommand({command: 'finishTest'}); + assert_equals(result, 'ok', 'finishTest'); +} + +// Wrapper for 'getResultingClient' command. Tells the worker to return +// clients.get(event.resultingClientId) for the navigation that occurs +// during this test. +// +// The return value describes how clients.get() settled. It also includes +// |queriedId| which is the id passed to clients.get() (the resultingClientId +// in this case). +// +// Example value: +// { +// queriedId: 'abc', +// promiseState: fulfilled, +// promiseValue: client, +// client: { +// id: 'abc', +// url: '//example.com/client' +// } +// } +async function getResultingClient() { + return sendCommand({command: 'getResultingClient'}); +} + +// Wrapper for 'getClient' command. Tells the worker to return +// clients.get(|id|). The return value is as in the getResultingClient() +// documentation. +async function getClient(id) { + return sendCommand({command: 'getClient', id: id}); +} + +// Navigates to |url|. Returns the result of clients.get() on the +// resultingClientId. +async function navigateAndGetResultingClient(t, url) { + const resultPromise = getResultingClient(); + const frame = await with_iframe(url); + t.add_cleanup(() => { + frame.remove(); + }); + const result = await resultPromise; + const resultingClientId = result.queriedId; + + // First test clients.get(event.resultingClientId) inside the fetch event. The + // behavior of this is subtle due to the use of iframes and about:blank + // replacement. The spec probably requires that it resolve to the original + // about:blank client, and that later that client should be discarded after + // load if the load was to another origin. Implementations might differ. For + // now, this test just asserts that the promise resolves. See + // https://github.com/w3c/ServiceWorker/issues/1385. + assert_equals(result.promiseState, 'fulfilled', + 'get(event.resultingClientId) in the fetch event should fulfill'); + + // Test clients.get() on the previous resultingClientId again. By this + // time the load finished, so it's more straightforward how this promise + // should settle. Return the result of this promise. + return await getClient(resultingClientId); +} + +// Test get(resultingClientId) in the basic same-origin case. +promise_test(async (t) => { + await startTest(t); + + const url = new URL('resources/empty.html', window.location); + const result = await navigateAndGetResultingClient(t, url); + assert_equals(result.promiseState, 'fulfilled', 'promiseState'); + assert_equals(result.promiseValue, 'client', 'promiseValue'); + assert_equals(result.client.url, url.href, 'client.url',); + assert_equals(result.client.id, result.queriedId, 'client.id'); +}, 'get(resultingClientId) for same-origin document'); + +// Test get(resultingClientId) when the response redirects to another origin. +promise_test(async (t) => { + await startTest(t); + + // Navigate to a URL that redirects to another origin. + const base_url = new URL('.', window.location); + const host_info = get_host_info(); + const other_origin_url = new URL(base_url.pathname + 'resources/empty.html', + host_info['HTTPS_REMOTE_ORIGIN']); + const url = new URL('resources/empty.html', window.location); + const pipe = `status(302)|header(Location, ${other_origin_url})`; + url.searchParams.set('pipe', pipe); + + // The original reserved client should have been discarded on cross-origin + // redirect. + const result = await navigateAndGetResultingClient(t, url); + assert_equals(result.promiseState, 'fulfilled', 'promiseState'); + assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue'); +}, 'get(resultingClientId) on cross-origin redirect'); + +// Test get(resultingClientId) when the document is sandboxed to a unique +// origin using a CSP HTTP response header. +promise_test(async (t) => { + await startTest(t); + + // Navigate to a URL that has CSP sandboxing set in the HTTP response header. + const url = new URL('resources/empty.html', window.location); + const pipe = 'header(Content-Security-Policy, sandbox)'; + url.searchParams.set('pipe', pipe); + + // The original reserved client should have been discarded upon loading + // the sandboxed document. + const result = await navigateAndGetResultingClient(t, url); + assert_equals(result.promiseState, 'fulfilled', 'promiseState'); + assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue'); +}, 'get(resultingClientId) for document sandboxed by CSP header'); + +// Test get(resultingClientId) when the document is sandboxed with +// allow-same-origin. +promise_test(async (t) => { + await startTest(t); + + // Navigate to a URL that has CSP sandboxing set in the HTTP response header. + const url = new URL('resources/empty.html', window.location); + const pipe = 'header(Content-Security-Policy, sandbox allow-same-origin)'; + url.searchParams.set('pipe', pipe); + + // The client should be the original reserved client, as it's same-origin. + const result = await navigateAndGetResultingClient(t, url); + assert_equals(result.promiseState, 'fulfilled', 'promiseState'); + assert_equals(result.promiseValue, 'client', 'promiseValue'); + assert_equals(result.client.url, url.href, 'client.url',); + assert_equals(result.client.id, result.queriedId, 'client.id'); +}, 'get(resultingClientId) for document sandboxed by CSP header with allow-same-origin'); + +// Cleanup. Keep this as the last promise_test. +promise_test(async (t) => { + return service_worker_unregister(t, scope); +}, 'global cleanup'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-get.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-get.https.html new file mode 100644 index 0000000000..4cfbf595ca --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-get.https.html @@ -0,0 +1,154 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.get</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +function wait_for_clientId() { + return new Promise(function(resolve, reject) { + window.onmessage = e => { + resolve(e.data.clientId); + }; + }); +} + +promise_test(async t => { + // Register service worker. + const scope = 'resources/clients-get-frame.html'; + const client_ids = []; + const registration = await service_worker_unregister_and_register( + t, 'resources/clients-get-worker.js', scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + // Prepare for test cases. + // Case 1: frame1 which is focused. + const frame1 = await with_iframe(scope + '#1'); + t.add_cleanup(() => frame1.remove()); + frame1.focus(); + client_ids.push(await wait_for_clientId()); + // Case 2: frame2 which is not focused. + const frame2 = await with_iframe(scope + '#2'); + t.add_cleanup(() => frame2.remove()); + client_ids.push(await wait_for_clientId()); + // Case 3: invalid id. + client_ids.push('invalid-id'); + + // Call clients.get() for each id on the service worker. + const message_event = await new Promise(resolve => { + navigator.serviceWorker.onmessage = resolve; + registration.active.postMessage({clientIds: client_ids}); + }); + + const expected = [ + // visibilityState, focused, url, type, frameType + ['visible', true, normalizeURL(scope) + '#1', 'window', 'nested'], + ['visible', false, normalizeURL(scope) + '#2', 'window', 'nested'], + undefined + ]; + assert_equals(message_event.data.length, 3); + assert_array_equals(message_event.data[0], expected[0]); + assert_array_equals(message_event.data[1], expected[1]); + assert_equals(message_event.data[2], expected[2]); +}, 'Test Clients.get()'); + +promise_test(async t => { + // Register service worker. + const scope = 'resources/simple.html'; + const registration = await service_worker_unregister_and_register( + t, 'resources/clients-get-resultingClientId-worker.js', scope) + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + const worker = registration.active; + + // Load frame within the scope. + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + frame.focus(); + + // Get resulting client id. + const resultingClientId = await new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { + if (e.data.msg == 'getResultingClientId') { + resolve(e.data.resultingClientId); + } + }; + worker.postMessage({msg: 'getResultingClientId'}); + }); + + // Query service worker for clients.get(resultingClientId). + const isResultingClientUndefined = await new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { + if (e.data.msg == 'getIsResultingClientUndefined') { + resolve(e.data.isResultingClientUndefined); + } + }; + worker.postMessage({msg: 'getIsResultingClientUndefined', + resultingClientId}); + }); + + assert_false( + isResultingClientUndefined, + 'Clients.get(FetchEvent.resultingClientId) resolved with a Client'); +}, 'Test successful Clients.get(FetchEvent.resultingClientId)'); + +promise_test(async t => { + // Register service worker. + const scope = 'resources/simple.html?fail'; + const registration = await service_worker_unregister_and_register( + t, 'resources/clients-get-resultingClientId-worker.js', scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + // Load frame, and destroy it while loading. + const worker = registration.active; + let frame = document.createElement('iframe'); + frame.src = scope; + t.add_cleanup(() => { + if (frame) { + frame.remove(); + } + }); + + await new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { + // The service worker posts a message to remove the iframe during fetch + // event. + if (e.data.msg == 'destroyResultingClient') { + frame.remove(); + frame = null; + worker.postMessage({msg: 'resultingClientDestroyed'}); + resolve(); + } + }; + document.body.appendChild(frame); + }); + + resultingDestroyedClientId = await new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { + // The worker sends a message back when it receives the message + // 'resultingClientDestroyed' with the resultingClientId. + if (e.data.msg == 'resultingClientDestroyedAck') { + assert_equals(frame, null, 'Frame should be destroyed at this point.'); + resolve(e.data.resultingDestroyedClientId); + } + }; + }); + + // Query service worker for clients.get(resultingDestroyedClientId). + const isResultingClientUndefined = await new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { + if (e.data.msg == 'getIsResultingClientUndefined') { + resolve(e.data.isResultingClientUndefined); + } + }; + worker.postMessage({msg: 'getIsResultingClientUndefined', + resultingClientId: resultingDestroyedClientId }); + }); + + assert_true( + isResultingClientUndefined, + 'Clients.get(FetchEvent.resultingClientId) resolved with `undefined`'); +}, 'Test unsuccessful Clients.get(FetchEvent.resultingClientId)'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html new file mode 100644 index 0000000000..c29bac8b89 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.matchAll with a blob URL worker client</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +const SCRIPT = 'resources/clients-matchall-worker.js'; + +promise_test(async (t) => { + const scope = 'resources/clients-matchall-blob-url-worker.html'; + + const reg = await service_worker_unregister_and_register(t, SCRIPT, scope); + t.add_cleanup(_ => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + const frame = await with_iframe(scope); + t.add_cleanup(_ => frame.remove()); + + { + const message = await frame.contentWindow.waitForWorker(); + assert_equals(message.data, 'Worker is ready.', + 'Worker should reply to the message.'); + } + + const channel = new MessageChannel(); + const message = await new Promise(resolve => { + channel.port1.onmessage = resolve; + frame.contentWindow.navigator.serviceWorker.controller.postMessage( + {port: channel.port2, options: {type: 'worker'}}, [channel.port2]); + }); + + checkMessageEvent(message); + +}, 'Test Clients.matchAll() with a blob URL worker client.'); + +promise_test(async (t) => { + const scope = 'resources/blank.html'; + + const reg = await service_worker_unregister_and_register(t, SCRIPT, scope); + t.add_cleanup(_ => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + 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); + + { + const message = await new Promise(resolve => { + worker.onmessage = resolve; + worker.postMessage("Ping to worker."); + }); + assert_equals(message.data, 'Worker is ready.', + 'Worker should reply to the message.'); + } + + const channel = new MessageChannel(); + const message = await new Promise(resolve => { + channel.port1.onmessage = resolve; + reg.active.postMessage( + {port: channel.port2, + options: {includeUncontrolled: true, type: 'worker'}}, + [channel.port2] + ); + }); + + checkMessageEvent(message); + +}, 'Test Clients.matchAll() with an uncontrolled blob URL worker client.'); + +function checkMessageEvent(e) { + assert_equals(e.data.length, 1); + + const workerClient = e.data[0]; + assert_equals(workerClient[0], undefined); // visibilityState + assert_equals(workerClient[1], undefined); // focused + assert_true(workerClient[2].includes('blob:')); // url + assert_equals(workerClient[3], 'worker'); // type + assert_equals(workerClient[4], 'none'); // frameType +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-client-types.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-client-types.https.html new file mode 100644 index 0000000000..54f182b620 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-client-types.https.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.matchAll with various clientTypes</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +const scope = 'resources/clients-matchall-client-types'; +const iframe_url = scope + '-iframe.html'; +const shared_worker_url = scope + '-shared-worker.js'; +const dedicated_worker_url = scope + '-dedicated-worker.js'; + +/* visibilityState, focused, url, type, frameType */ +const expected_only_window = [ + ['visible', true, new URL(iframe_url, location).href, 'window', 'nested'] +]; +const expected_only_shared_worker = [ + [undefined, undefined, new URL(shared_worker_url, location).href, 'sharedworker', 'none'] +]; +const expected_only_dedicated_worker = [ + [undefined, undefined, new URL(dedicated_worker_url, location).href, 'worker', 'none'] +]; + +// These are explicitly sorted by URL in the service worker script. +const expected_all_clients = [ + expected_only_dedicated_worker[0], + expected_only_window[0], + expected_only_shared_worker[0], +]; + +async function test_matchall(frame, expected, query_options) { + // Make sure the frame gets focus. + frame.focus(); + const data = await new Promise(resolve => { + const channel = new MessageChannel(); + channel.port1.onmessage = e => resolve(e.data); + frame.contentWindow.navigator.serviceWorker.controller.postMessage( + {port:channel.port2, options:query_options}, + [channel.port2]); + }); + + if (typeof data === 'string') { + throw new Error(data); + } + + assert_equals(data.length, expected.length, 'result count'); + + for (let i = 0; i < data.length; ++i) { + assert_array_equals(data[i], expected[i]); + } +} + +promise_test(async t => { + const registration = await service_worker_unregister_and_register( + t, 'resources/clients-matchall-worker.js', scope); + t.add_cleanup(_ => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(iframe_url); + t.add_cleanup(_ => frame.remove()); + await test_matchall(frame, expected_only_window, {}); + await test_matchall(frame, expected_only_window, {type:'window'}); +}, 'Verify matchAll() with window client type'); + +promise_test(async t => { + const registration = await service_worker_unregister_and_register( + t, 'resources/clients-matchall-worker.js', scope); + t.add_cleanup(_ => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(iframe_url); + t.add_cleanup(_ => frame.remove()); + + // Set up worker clients. + const shared_worker = await new Promise((resolve, reject) => { + const w = new SharedWorker(shared_worker_url); + w.onerror = e => reject(e.message); + w.port.onmessage = _ => resolve(w); + }); + const dedicated_worker = await new Promise((resolve, reject) => { + const w = new Worker(dedicated_worker_url); + w.onerror = e => reject(e.message); + w.onmessage = _ => resolve(w); + w.postMessage('Start'); + }); + + await test_matchall(frame, expected_only_window, {}); + await test_matchall(frame, expected_only_window, {type:'window'}); + await test_matchall(frame, expected_only_shared_worker, + {type:'sharedworker'}); + await test_matchall(frame, expected_only_dedicated_worker, {type:'worker'}); + await test_matchall(frame, expected_all_clients, {type:'all'}); +}, 'Verify matchAll() with {window, sharedworker, worker} client types'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html new file mode 100644 index 0000000000..a61c8af701 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.matchAll with exact controller</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +const scope = 'resources/blank.html?clients-matchAll'; +let frames = []; + +function checkWorkerClients(worker, expected) { + return new Promise((resolve, reject) => { + let channel = new MessageChannel(); + channel.port1.onmessage = evt => { + try { + assert_equals(evt.data.length, expected.length); + for (let i = 0; i < expected.length; ++i) { + assert_array_equals(evt.data[i], expected[i]); + } + resolve(); + } catch (e) { + reject(e); + } + }; + + worker.postMessage({port:channel.port2}, [channel.port2]); + }); +} + +let expected = [ + // visibilityState, focused, url, type, frameType + ['visible', true, new URL(scope + '#1', location).toString(), 'window', 'nested'], + ['visible', false, new URL(scope + '#2', location).toString(), 'window', 'nested'] +]; + +promise_test(t => { + let script = 'resources/clients-matchall-worker.js'; + return service_worker_unregister_and_register(t, script, scope) + .then(registration => { + t.add_cleanup(() => service_worker_unregister(t, scope)); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(_ => with_iframe(scope + '#1') ) + .then(frame1 => { + frames.push(frame1); + frame1.focus(); + return with_iframe(scope + '#2'); + }) + .then(frame2 => { + frames.push(frame2); + return navigator.serviceWorker.register(script + '?updated', { scope: scope }); + }) + .then(registration => { + return wait_for_state(t, registration.installing, 'installed') + .then(_ => registration); + }) + .then(registration => { + return Promise.all([ + checkWorkerClients(registration.waiting, []), + checkWorkerClients(registration.active, expected), + ]); + }) + .then(_ => { + frames.forEach(f => f.remove() ); + }); +}, 'Test Clients.matchAll() with exact controller'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-frozen.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-frozen.https.html new file mode 100644 index 0000000000..479c28a60f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-frozen.https.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.matchAll</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var scope = 'resources/clients-frame-freeze.html'; +var windows = []; +var expected_window_1 = + {visibilityState: 'visible', focused: false, lifecycleState: "frozen", url: new URL(scope + '#1', location).toString(), type: 'window', frameType: 'top-level'}; +var expected_window_2 = + {visibilityState: 'visible', focused: false, lifecycleState: "active", url: new URL(scope + '#2', location).toString(), type: 'window', frameType: 'top-level'}; +function with_window(url, name) { + return new Promise(function(resolve) { + var child = window.open(url, name); + window.onmessage = () => {resolve(child)}; + }); +} + +promise_test(function(t) { + return service_worker_unregister_and_register( + t, 'resources/clients-matchall-worker.js', scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_window(scope + '#1', 'Child 1'); }) + .then(function(window1) { + windows.push(window1); + return with_window(scope + '#2', 'Child 2'); + }) + .then(function(window2) { + windows.push(window2); + return new Promise(function(resolve) { + window.onmessage = resolve; + windows[0].postMessage('freeze'); + }); + }) + .then(function() { + var channel = new MessageChannel(); + + return new Promise(function(resolve) { + channel.port1.onmessage = resolve; + windows[1].navigator.serviceWorker.controller.postMessage( + {port:channel.port2, includeLifecycleState: true}, [channel.port2]); + }); + }) + .then(function(e) { + assert_equals(e.data.length, 2); + // No specific order is required, so support inversion. + if (e.data[0][3] == new URL(scope + '#2', location)) { + assert_object_equals(e.data[0], expected_window_2); + assert_object_equals(e.data[1], expected_window_1); + } else { + assert_object_equals(e.data[0], expected_window_1); + assert_object_equals(e.data[1], expected_window_2); + } + }); +}, 'Test Clients.matchAll()'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html new file mode 100644 index 0000000000..9f34e5709e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html @@ -0,0 +1,117 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.matchAll with includeUncontrolled</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +function test_matchall(service_worker, expected, query_options) { + expected.sort((a, b) => a[2] > b[2] ? 1 : -1); + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + channel.port1.onmessage = e => { + const data = e.data.filter(info => { + return info[2].indexOf('clients-matchall') > -1; + }); + data.sort((a, b) => a[2] > b[2] ? 1 : -1); + assert_equals(data.length, expected.length); + for (let i = 0; i < data.length; i++) + assert_array_equals(data[i], expected[i]); + resolve(); + }; + service_worker.postMessage({port:channel.port2, options:query_options}, + [channel.port2]); + }); +} + +// Run clients.matchAll without and with includeUncontrolled=true. +// (We want to run the two tests sequentially in the same promise_test +// so that we can use the same set of iframes without intefering each other. +promise_test(async t => { + // |base_url| is out-of-scope. + const base_url = 'resources/blank.html?clients-matchall'; + const scope = base_url + '-includeUncontrolled'; + + const registration = + await service_worker_unregister_and_register( + t, 'resources/clients-matchall-worker.js', scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + const service_worker = registration.installing; + await wait_for_state(t, service_worker, 'activated'); + + // Creates 3 iframes, 2 for in-scope and 1 for out-of-scope. + let frames = []; + frames.push(await with_iframe(base_url)); + frames.push(await with_iframe(scope + '#1')); + frames.push(await with_iframe(scope + '#2')); + + // Make sure we have focus for '#2' frame and its parent window. + frames[2].focus(); + frames[2].contentWindow.focus(); + + const expected_without_include_uncontrolled = [ + // visibilityState, focused, url, type, frameType + ['visible', false, new URL(scope + '#1', location).toString(), 'window', 'nested'], + ['visible', true, new URL(scope + '#2', location).toString(), 'window', 'nested'] + ]; + const expected_with_include_uncontrolled = [ + // visibilityState, focused, url, type, frameType + ['visible', true, location.href, 'window', 'top-level'], + ['visible', false, new URL(scope + '#1', location).toString(), 'window', 'nested'], + ['visible', true, new URL(scope + '#2', location).toString(), 'window', 'nested'], + ['visible', false, new URL(base_url, location).toString(), 'window', 'nested'] + ]; + + await test_matchall(service_worker, expected_without_include_uncontrolled); + await test_matchall(service_worker, expected_with_include_uncontrolled, + { includeUncontrolled: true }); +}, 'Verify matchAll() with windows respect includeUncontrolled'); + +// TODO: Add tests for clients.matchAll for dedicated workers. + +async function create_shared_worker(script_url) { + const shared_worker = new SharedWorker(script_url); + const msgEvent = await new Promise(r => shared_worker.port.onmessage = r); + assert_equals(msgEvent.data, 'started'); + return shared_worker; +} + +// Run clients.matchAll for shared workers without and with +// includeUncontrolled=true. +promise_test(async t => { + const script_url = 'resources/clients-matchall-client-types-shared-worker.js'; + const uncontrolled_script_url = + new URL(script_url + '?uncontrolled', location).toString(); + const controlled_script_url = + new URL(script_url + '?controlled', location).toString(); + + // Start a shared worker that is not controlled by a service worker. + const uncontrolled_shared_worker = + await create_shared_worker(uncontrolled_script_url); + + // Register a service worker. + const registration = + await service_worker_unregister_and_register( + t, 'resources/clients-matchall-worker.js', script_url); + t.add_cleanup(() => service_worker_unregister(t, script_url)); + const service_worker = registration.installing; + await wait_for_state(t, service_worker, 'activated'); + + // Start another shared worker controlled by the service worker. + await create_shared_worker(controlled_script_url); + + const expected_without_include_uncontrolled = [ + // visibilityState, focused, url, type, frameType + [undefined, undefined, controlled_script_url, 'sharedworker', 'none'], + ]; + const expected_with_include_uncontrolled = [ + // visibilityState, focused, url, type, frameType + [undefined, undefined, controlled_script_url, 'sharedworker', 'none'], + [undefined, undefined, uncontrolled_script_url, 'sharedworker', 'none'], + ]; + + await test_matchall(service_worker, expected_without_include_uncontrolled, + { type: 'sharedworker' }); + await test_matchall(service_worker, expected_with_include_uncontrolled, + { includeUncontrolled: true, type: 'sharedworker' }); +}, 'Verify matchAll() with shared workers respect includeUncontrolled'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html new file mode 100644 index 0000000000..8705f85b56 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.matchAll on script evaluation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(function(t) { + var script = 'resources/clients-matchall-on-evaluation-worker.js'; + var scope = 'resources/blank.html?clients-matchAll-on-evaluation'; + + var saw_message = new Promise(function(resolve) { + navigator.serviceWorker.onmessage = function(e) { + assert_equals(e.data, 'matched'); + resolve(); + }; + }); + + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + add_completion_callback(function() { registration.unregister(); }); + return saw_message; + }); + }, 'Test Clients.matchAll() on script evaluation'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-order.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-order.https.html new file mode 100644 index 0000000000..ec650f2264 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-order.https.html @@ -0,0 +1,427 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.matchAll ordering</title> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +// Utility function for URLs this test will open. +function makeURL(name, num, type) { + let u = new URL('resources/empty.html', location); + u.searchParams.set('name', name); + if (num !== undefined) { + u.searchParams.set('q', num); + } + if (type === 'nested') { + u.searchParams.set('nested', true); + } + return u.href; +} + +// Non-test URLs that will be open during each test. The harness URLs +// are from the WPT harness. The "extra" URL is a final window opened +// by the test. +const EXTRA_URL = makeURL('extra'); +const TEST_HARNESS_URL = location.href; +const TOP_HARNESS_URL = new URL('/testharness_runner.html', location).href; + +// Utility function to open an iframe in the target parent window. We +// can't just use with_iframe() because it does not support a configurable +// parent window. +function openFrame(parentWindow, url) { + return new Promise(resolve => { + let frame = parentWindow.document.createElement('iframe'); + frame.src = url; + parentWindow.document.body.appendChild(frame); + + frame.contentWindow.addEventListener('load', evt => { + resolve(frame); + }, { once: true }); + }); +} + +// Utility function to open a window and wait for it to load. The +// window may optionally have a nested iframe as well. Returns +// a result like `{ top: <frame ref> nested: <nested frame ref if present> }`. +function openFrameConfig(opts) { + let url = new URL(opts.url, location.href); + return openFrame(window, url.href).then(top => { + if (!opts.withNested) { + return { top: top }; + } + + url.searchParams.set('nested', true); + return openFrame(top.contentWindow, url.href).then(nested => { + return { top: top, nested: nested }; + }); + }); +} + +// Utility function that takes a list of configurations and opens the +// corresponding windows in sequence. An array of results is returned. +function openFrameConfigList(optList) { + let resultList = []; + function openNextWindow(optList, nextWindow) { + if (nextWindow >= optList.length) { + return resultList; + } + return openFrameConfig(optList[nextWindow]).then(result => { + resultList.push(result); + return openNextWindow(optList, nextWindow + 1); + }); + } + return openNextWindow(optList, 0); +} + +// Utility function that focuses the given entry in window result list. +function executeFocus(frameResultList, opts) { + return new Promise(resolve => { + let w = frameResultList[opts.index][opts.type]; + let target = w.contentWindow ? w.contentWindow : w; + target.addEventListener('focus', evt => { + resolve(); + }, { once: true }); + target.focus(); + }); +} + +// Utility function that performs a list of focus commands in sequence +// based on the window result list. +function executeFocusList(frameResultList, optList) { + function executeNextCommand(frameResultList, optList, nextCommand) { + if (nextCommand >= optList.length) { + return; + } + return executeFocus(frameResultList, optList[nextCommand]).then(_ => { + return executeNextCommand(frameResultList, optList, nextCommand + 1); + }); + } + return executeNextCommand(frameResultList, optList, 0); +} + +// Perform a `clients.matchAll()` in the service worker with the given +// options dictionary. +function doMatchAll(worker, options) { + return new Promise(resolve => { + let channel = new MessageChannel(); + channel.port1.onmessage = evt => { + resolve(evt.data); + }; + worker.postMessage({ port: channel.port2, options: options, disableSort: true }, + [channel.port2]); + }); +} + +// Function that performs a single test case. It takes a configuration object +// describing the windows to open, how to focus them, the matchAll options, +// and the resulting expectations. See the test cases for examples of how to +// use this. +function matchAllOrderTest(t, opts) { + let script = 'resources/clients-matchall-worker.js'; + let worker; + let frameResultList; + let extraWindowResult; + return service_worker_unregister_and_register(t, script, opts.scope).then(swr => { + t.add_cleanup(() => service_worker_unregister(t, opts.scope)); + + worker = swr.installing; + return wait_for_state(t, worker, 'activated'); + }).then(_ => { + return openFrameConfigList(opts.frameConfigList); + }).then(results => { + frameResultList = results; + return openFrameConfig({ url: EXTRA_URL }); + }).then(result => { + extraWindowResult = result; + return executeFocusList(frameResultList, opts.focusConfigList); + }).then(_ => { + return doMatchAll(worker, opts.matchAllOptions); + }).then(data => { + assert_equals(data.length, opts.expected.length); + for (let i = 0; i < data.length; ++i) { + assert_equals(data[i][2], opts.expected[i], 'expected URL index ' + i); + } + }).then(_ => { + frameResultList.forEach(result => result.top.remove()); + extraWindowResult.top.remove(); + }).catch(e => { + if (frameResultList) { + frameResultList.forEach(result => result.top.remove()); + } + if (extraWindowResult) { + extraWindowResult.top.remove(); + } + throw(e); + }); +} + +// ---------- +// Test cases +// ---------- + +promise_test(t => { + let name = 'no-focus-controlled-windows'; + let opts = { + scope: makeURL(name), + + frameConfigList: [ + { url: makeURL(name, 0), withNested: false }, + { url: makeURL(name, 1), withNested: false }, + { url: makeURL(name, 2), withNested: false }, + ], + + focusConfigList: [ + // no focus commands + ], + + matchAllOptions: { + includeUncontrolled: false + }, + + expected: [ + makeURL(name, 0), + makeURL(name, 1), + makeURL(name, 2), + ], + }; + + return matchAllOrderTest(t, opts); +}, 'Clients.matchAll() returns non-focused controlled windows in creation order.'); + +promise_test(t => { + let name = 'focus-controlled-windows-1'; + let opts = { + scope: makeURL(name), + + frameConfigList: [ + { url: makeURL(name, 0), withNested: false }, + { url: makeURL(name, 1), withNested: false }, + { url: makeURL(name, 2), withNested: false }, + ], + + focusConfigList: [ + { index: 0, type: 'top' }, + { index: 1, type: 'top' }, + { index: 2, type: 'top' }, + ], + + matchAllOptions: { + includeUncontrolled: false + }, + + expected: [ + makeURL(name, 2), + makeURL(name, 1), + makeURL(name, 0), + ], + }; + + return matchAllOrderTest(t, opts); +}, 'Clients.matchAll() returns controlled windows in focus order. Case 1.'); + +promise_test(t => { + let name = 'focus-controlled-windows-2'; + let opts = { + scope: makeURL(name), + + frameConfigList: [ + { url: makeURL(name, 0), withNested: false }, + { url: makeURL(name, 1), withNested: false }, + { url: makeURL(name, 2), withNested: false }, + ], + + focusConfigList: [ + { index: 2, type: 'top' }, + { index: 1, type: 'top' }, + { index: 0, type: 'top' }, + ], + + matchAllOptions: { + includeUncontrolled: false + }, + + expected: [ + makeURL(name, 0), + makeURL(name, 1), + makeURL(name, 2), + ], + }; + + return matchAllOrderTest(t, opts); +}, 'Clients.matchAll() returns controlled windows in focus order. Case 2.'); + +promise_test(t => { + let name = 'no-focus-uncontrolled-windows'; + let opts = { + scope: makeURL(name + '-outofscope'), + + frameConfigList: [ + { url: makeURL(name, 0), withNested: false }, + { url: makeURL(name, 1), withNested: false }, + { url: makeURL(name, 2), withNested: false }, + ], + + focusConfigList: [ + // no focus commands + ], + + matchAllOptions: { + includeUncontrolled: true + }, + + expected: [ + // The harness windows have been focused, so appear first + TEST_HARNESS_URL, + TOP_HARNESS_URL, + + // Test frames have not been focused, so appear in creation order + makeURL(name, 0), + makeURL(name, 1), + makeURL(name, 2), + EXTRA_URL, + ], + }; + + return matchAllOrderTest(t, opts); +}, 'Clients.matchAll() returns non-focused uncontrolled windows in creation order.'); + +promise_test(t => { + let name = 'focus-uncontrolled-windows-1'; + let opts = { + scope: makeURL(name + '-outofscope'), + + frameConfigList: [ + { url: makeURL(name, 0), withNested: false }, + { url: makeURL(name, 1), withNested: false }, + { url: makeURL(name, 2), withNested: false }, + ], + + focusConfigList: [ + { index: 0, type: 'top' }, + { index: 1, type: 'top' }, + { index: 2, type: 'top' }, + ], + + matchAllOptions: { + includeUncontrolled: true + }, + + expected: [ + // The test harness window is a parent of all test frames. It will + // always have the same focus time or later as its frames. So it + // appears first. + TEST_HARNESS_URL, + + makeURL(name, 2), + makeURL(name, 1), + makeURL(name, 0), + + // The overall harness has been focused + TOP_HARNESS_URL, + + // The extra frame was never focused + EXTRA_URL, + ], + }; + + return matchAllOrderTest(t, opts); +}, 'Clients.matchAll() returns uncontrolled windows in focus order. Case 1.'); + +promise_test(t => { + let name = 'focus-uncontrolled-windows-2'; + let opts = { + scope: makeURL(name + '-outofscope'), + + frameConfigList: [ + { url: makeURL(name, 0), withNested: false }, + { url: makeURL(name, 1), withNested: false }, + { url: makeURL(name, 2), withNested: false }, + ], + + focusConfigList: [ + { index: 2, type: 'top' }, + { index: 1, type: 'top' }, + { index: 0, type: 'top' }, + ], + + matchAllOptions: { + includeUncontrolled: true + }, + + expected: [ + // The test harness window is a parent of all test frames. It will + // always have the same focus time or later as its frames. So it + // appears first. + TEST_HARNESS_URL, + + makeURL(name, 0), + makeURL(name, 1), + makeURL(name, 2), + + // The overall harness has been focused + TOP_HARNESS_URL, + + // The extra frame was never focused + EXTRA_URL, + ], + }; + + return matchAllOrderTest(t, opts); +}, 'Clients.matchAll() returns uncontrolled windows in focus order. Case 2.'); + +promise_test(t => { + let name = 'focus-controlled-nested-windows'; + let opts = { + scope: makeURL(name), + + frameConfigList: [ + { url: makeURL(name, 0), withNested: true }, + { url: makeURL(name, 1), withNested: true }, + { url: makeURL(name, 2), withNested: true }, + ], + + focusConfigList: [ + { index: 0, type: 'top' }, + + // Note, some browsers don't let programmatic focus of a frame unless + // an ancestor window is already focused. So focus the window and + // then the frame. + { index: 1, type: 'top' }, + { index: 1, type: 'nested' }, + + { index: 2, type: 'top' }, + ], + + matchAllOptions: { + includeUncontrolled: false + }, + + expected: [ + // Focus order for window 2, but not its frame. We only focused + // the window. + makeURL(name, 2), + + // Window 1 is next via focus order, but the window is always + // shown first here. The window gets its last focus time updated + // when the frame is focused. Since the times match between the + // two it falls back to creation order. The window was created + // before the frame. This behavior is being discussed in: + // https://github.com/w3c/ServiceWorker/issues/1080 + makeURL(name, 1), + makeURL(name, 1, 'nested'), + + // Focus order for window 0, but not its frame. We only focused + // the window. + makeURL(name, 0), + + // Creation order of the frames since they are not focused by + // default when they are created. + makeURL(name, 0, 'nested'), + makeURL(name, 2, 'nested'), + ], + }; + + return matchAllOrderTest(t, opts); +}, 'Clients.matchAll() returns controlled windows and frames in focus order.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall.https.html new file mode 100644 index 0000000000..ce44f1924d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall.https.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.matchAll</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var scope = 'resources/blank.html?clients-matchAll'; +var frames = []; +promise_test(function(t) { + return service_worker_unregister_and_register( + t, 'resources/clients-matchall-worker.js', scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(scope + '#1'); }) + .then(function(frame1) { + frames.push(frame1); + frame1.focus(); + return with_iframe(scope + '#2'); + }) + .then(function(frame2) { + frames.push(frame2); + var channel = new MessageChannel(); + + return new Promise(function(resolve) { + channel.port1.onmessage = resolve; + frame2.contentWindow.navigator.serviceWorker.controller.postMessage( + {port:channel.port2}, [channel.port2]); + }); + }) + .then(onMessage); +}, 'Test Clients.matchAll()'); + +var expected = [ + // visibilityState, focused, url, type, frameType + ['visible', true, new URL(scope + '#1', location).toString(), 'window', 'nested'], + ['visible', false, new URL(scope + '#2', location).toString(), 'window', 'nested'] +]; + +function onMessage(e) { + assert_equals(e.data.length, 2); + assert_array_equals(e.data[0], expected[0]); + assert_array_equals(e.data[1], expected[1]); + frames.forEach(function(f) { f.remove(); }); +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html b/testing/web-platform/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html new file mode 100644 index 0000000000..7e2a604621 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html @@ -0,0 +1,44 @@ +<html> +<head> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +</head> +<body> +<script> +promise_test(async (test) => { + const registration = await navigator.serviceWorker.register("postMessage-client-worker.js", { scope : 'resources' }); + activeWorker = registration.active; + if (activeWorker) + return; + + activeWorker = registration.installing; + await new Promise(resolve => { + activeWorker.addEventListener('statechange', () => { + if (activeWorker.state === "activated") + resolve(); + }); + }); +}, "Register service worker"); + +promise_test(async (test) => { + const worker = new Worker('resources/controlled-worker-postMessage.js'); + const event = await new Promise((resolve, reject) => { + test.step_timeout(() => reject("test timed out"), 3000); + worker.onmessage = resolve; + }); + assert_equals(event.data, 0); +}, "Verify dedicated worker gets messages if setting event listener early"); + +promise_test(async (test) => { + const worker = new Worker('resources/controlled-worker-late-postMessage.js?repeatMessages'); + const event = await new Promise((resolve, reject) => { + test.step_timeout(() => reject("test timed out"), 3000); + worker.onmessage = resolve; + }); + assert_not_equals(event.data, "No message received"); + assert_true(event.data > 0); +}, "Verify dedicated worker does not get all messages if not setting event listener early"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html b/testing/web-platform/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html new file mode 100644 index 0000000000..8f39b7fdbf --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html @@ -0,0 +1,67 @@ +<html> +<head> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +</head> +<body> +<script> +promise_test(async (test) => { + const registration = await navigator.serviceWorker.register("postMessage-client-worker.js", { scope : 'resources' }); + activeWorker = registration.active; + if (activeWorker) + return; + + activeWorker = registration.installing; + await new Promise(resolve => { + activeWorker.addEventListener('statechange', () => { + if (activeWorker.state === "activated") + resolve(); + }); + }); +}, "Register service worker"); + +function with_iframe(test, url) { + return new Promise(function(resolve, reject) { + test.step_timeout(() => reject("with_iframe timed out"), 2000); + var frame = document.createElement('iframe'); + frame.className = 'test-iframe'; + frame.src = url; + frame.onload = function() { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +promise_test(async (test) => { + const frame = await with_iframe(test, 'resources/controlled-frame-postMessage.html'); + let counter = 0; + while (counter++ < 100 && frame.contentWindow.messageData == undefined) + await new Promise(resolve => test.step_timeout(resolve, 50)); + assert_equals(frame.contentWindow.messageData, 0); + frame.remove(); +}, "Verify frame gets early messages if setting synchronously message event listener"); + +promise_test(async (test) => { + const frame = await with_iframe(test, 'resources/controlled-frame-postMessage.html?repeatMessages'); + let counter = 0; + while (counter++ < 100 && frame.contentWindow.messageData == undefined) + await new Promise(resolve => test.step_timeout(resolve, 50)); + assert_not_equals(frame.contentWindow.messageData, 0); + frame.remove(); +}, "Verify frame does not get all messages if not setting event listener early"); + +promise_test(async (test) => { + const frame = await with_iframe(test, 'resources/controlled-frame-postMessage.html?repeatMessages&listener'); + let counter = 0; + while (counter++ < 100 && frame.contentWindow.messageData.length < 5) + await new Promise(resolve => test.step_timeout(resolve, 50)); + + assert_less_than(counter, 100); + data = frame.contentWindow.messageData; + for (let cptr = 1; cptr < data.length; cptr++) + assert_true(data[cptr - 1] < data[cptr]); + frame.remove(); +}, "Verify frame does get messages in order"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/controller-on-disconnect.https.html b/testing/web-platform/tests/service-workers/service-worker/controller-on-disconnect.https.html new file mode 100644 index 0000000000..f23dfe71ba --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/controller-on-disconnect.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<title>Service Worker: Controller on load</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +promise_test(function(t) { + var url = 'resources/empty-worker.js'; + var scope = 'resources/blank.html'; + var registration; + var controller; + var frame; + return service_worker_unregister_and_register(t, url, scope) + .then(function(swr) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = swr; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope) + }) + .then(function(f) { + frame = f; + var w = frame.contentWindow; + var swc = w.navigator.serviceWorker; + assert_true(swc.controller instanceof w.ServiceWorker, + 'controller should be a ServiceWorker object'); + + frame.remove(); + + assert_equals(swc.controller, null, + 'disconnected frame should not be controlled'); + }); +}, 'controller is cleared on disconnected window'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/controller-on-load.https.html b/testing/web-platform/tests/service-workers/service-worker/controller-on-load.https.html new file mode 100644 index 0000000000..e4c5e5f81f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/controller-on-load.https.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<title>Service Worker: Controller on load</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +promise_test(function(t) { + var url = 'resources/empty-worker.js'; + var scope = 'resources/blank.html'; + var registration; + var controller; + var frame; + return service_worker_unregister_and_register(t, url, scope) + .then(function(swr) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = swr; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(f) { + frame = f; + var w = frame.contentWindow; + controller = w.navigator.serviceWorker.controller; + assert_true(controller instanceof w.ServiceWorker, + 'controller should be a ServiceWorker object'); + assert_equals(controller.scriptURL, normalizeURL(url)); + + // objects from different windows should not be equal + assert_not_equals(controller, registration.active); + + return w.navigator.serviceWorker.getRegistration(); + }) + .then(function(frameRegistration) { + // SW objects from same window should be equal + assert_equals(frameRegistration.active, controller); + frame.remove(); + }); +}, 'controller is set for a controlled document'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/controller-on-reload.https.html b/testing/web-platform/tests/service-workers/service-worker/controller-on-reload.https.html new file mode 100644 index 0000000000..2e966d4257 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/controller-on-reload.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<title>Service Worker: Controller on reload</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +promise_test(function(t) { + const iframe_scope = 'blank.html'; + const scope = 'resources/' + iframe_scope; + var frame; + var registration; + var controller; + return service_worker_unregister(t, scope) + .then(function() { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return with_iframe(scope); + }) + .then(function(f) { + frame = f; + return frame.contentWindow.navigator.serviceWorker.register( + 'empty-worker.js', {scope: iframe_scope}); + }) + .then(function(swr) { + registration = swr; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + var w = frame.contentWindow; + assert_equals(w.navigator.serviceWorker.controller, null, + 'controller should be null until the document is ' + + 'reloaded'); + return new Promise(function(resolve) { + frame.onload = function() { resolve(); } + w.location.reload(); + }); + }) + .then(function() { + var w = frame.contentWindow; + controller = w.navigator.serviceWorker.controller; + assert_true(controller instanceof w.ServiceWorker, + 'controller should be a ServiceWorker object upon reload'); + + // objects from separate windows should not be equal + assert_not_equals(controller, registration.active); + + return w.navigator.serviceWorker.getRegistration(iframe_scope); + }) + .then(function(frameRegistration) { + assert_equals(frameRegistration.active, controller); + frame.remove(); + }); + }, 'controller is set upon reload after registration'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html b/testing/web-platform/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html new file mode 100644 index 0000000000..d947139c9e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: controller without a fetch event handler</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<body> +<script> +let registration; +let frame; +const host_info = get_host_info(); +const remote_base_url = + new URL(`${host_info.HTTPS_REMOTE_ORIGIN}${base_path()}resources/`); + +promise_test(async t => { + const script = 'resources/empty.js' + const scope = 'resources/'; + + promise_test(async t => { + if (frame) + frame.remove(); + + if (registration) + await registration.unregister(); + }, 'cleanup global state'); + + registration = await + service_worker_unregister_and_register(t, script, scope); + await wait_for_state(t, registration.installing, 'activated'); + frame = await with_iframe(scope + 'blank.html'); +}, 'global setup'); + +promise_test(async t => { + const url = new URL('cors-approved.txt', remote_base_url); + const response = await frame.contentWindow.fetch(url, {mode:'no-cors'}); + const text = await response.text(); + assert_equals(text, ''); +}, 'cross-origin request, no-cors mode'); + + +promise_test(async t => { + const url = new URL('cors-denied.txt', remote_base_url); + const response = frame.contentWindow.fetch(url); + await promise_rejects_js(t, frame.contentWindow.TypeError, response); +}, 'cross-origin request, cors denied'); + +promise_test(async t => { + const url = new URL('cors-approved.txt', remote_base_url); + response = await frame.contentWindow.fetch(url); + let text = await response.text(); + text = text.trim(); + assert_equals(text, 'plaintext'); +}, 'cross-origin request, cors approved'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/credentials.https.html b/testing/web-platform/tests/service-workers/service-worker/credentials.https.html new file mode 100644 index 0000000000..0a90dc2897 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/credentials.https.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Credentials for service worker scripts</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="/cookies/resources/cookie-helper.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +// Check if the service worker's script has appropriate credentials for a new +// worker and byte-for-byte checking. + +const SCOPE = 'resources/in-scope'; +const COOKIE_NAME = `service-worker-credentials-${Math.random()}`; + +promise_test(async t => { + // Set-Cookies for path=/. + await fetch( + `/cookies/resources/set-cookie.py?name=${COOKIE_NAME}&path=%2F`); +}, 'Set cookies as initialization'); + +async function get_cookies(worker) { + worker.postMessage('get cookie'); + const message = await new Promise(resolve => + navigator.serviceWorker.addEventListener('message', resolve)); + return message.data; +} + +promise_test(async t => { + const key = token(); + const registration = await service_worker_unregister_and_register( + t, `resources/echo-cookie-worker.py?key=${key}`, SCOPE); + t.add_cleanup(() => registration.unregister()); + const worker = registration.installing; + + const cookies = await get_cookies(worker); + assert_equals(cookies[COOKIE_NAME], '1', 'new worker has credentials'); + + await registration.update(); + const updated_worker = registration.installing; + const updated_cookies = await get_cookies(updated_worker); + assert_equals(updated_cookies[COOKIE_NAME], '1', + 'updated worker has credentials'); +}, 'Main script should have credentials'); + +promise_test(async t => { + const key = token(); + const registration = await service_worker_unregister_and_register( + t, `resources/import-echo-cookie-worker.js?key=${key}`, SCOPE); + t.add_cleanup(() => registration.unregister()); + const worker = registration.installing; + + const cookies = await get_cookies(worker); + assert_equals(cookies[COOKIE_NAME], '1', 'new worker has credentials'); + + await registration.update(); + const updated_worker = registration.installing; + const updated_cookies = await get_cookies(updated_worker); + assert_equals(updated_cookies[COOKIE_NAME], '1', + 'updated worker has credentials'); +}, 'Imported script should have credentials'); + +promise_test(async t => { + const key = token(); + const registration = await service_worker_unregister_and_register( + t, `resources/import-echo-cookie-worker-module.py?key=${key}`, SCOPE, {type: 'module'}); + t.add_cleanup(() => registration.unregister()); + const worker = registration.installing; + + const cookies = await get_cookies(worker); + assert_equals(cookies[COOKIE_NAME], undefined, 'new module worker should not have credentials'); + + await registration.update(); + const updated_worker = registration.installing; + const updated_cookies = await get_cookies(updated_worker); + assert_equals(updated_cookies[COOKIE_NAME], undefined, + 'updated worker should not have credentials'); +}, 'Module with an imported statement should not have credentials'); + +promise_test(async t => { + const key = token(); + const registration = await service_worker_unregister_and_register( +t, `resources/echo-cookie-worker.py?key=${key}`, SCOPE, {type: 'module'}); + t.add_cleanup(() => registration.unregister()); + const worker = registration.installing; + + const cookies = await get_cookies(worker); + assert_equals(cookies[COOKIE_NAME], undefined, 'new module worker should not have credentials'); + + await registration.update(); + const updated_worker = registration.installing; + const updated_cookies = await get_cookies(updated_worker); + assert_equals(updated_cookies[COOKIE_NAME], undefined, + 'updated worker should not have credentials'); +}, 'Script with service worker served as modules should not have credentials'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/data-iframe.html b/testing/web-platform/tests/service-workers/service-worker/data-iframe.html new file mode 100644 index 0000000000..d767d57434 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/data-iframe.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<title>Service Workers in data iframes</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body></body> +<script> +'use strict'; + +promise_test(t => { + const url = encodeURI(`data:text/html,<!DOCTYPE html> + <script> + parent.postMessage({ isDefined: 'serviceWorker' in navigator }, '*'); + </` + `script>`); + var p = new Promise((resolve, reject) => { + window.addEventListener('message', event => { + resolve(event.data.isDefined); + }); + }); + with_iframe(url); + return p.then(isDefined => { + assert_false(isDefined, 'navigator.serviceWorker should not be defined in iframe'); + }); +}, 'navigator.serviceWorker is not available in a data: iframe'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/data-transfer-files.https.html b/testing/web-platform/tests/service-workers/service-worker/data-transfer-files.https.html new file mode 100644 index 0000000000..c503a28f96 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/data-transfer-files.https.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Post a file in a navigation controlled by a service worker</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<iframe id=testframe name=testframe></iframe> +<form id=testform method=post action="/html/semantics/forms/form-submission-0/resources/file-submission.py" target=testframe enctype="multipart/form-data"> +<input name=testinput id=testinput type=file> +</form> +<script> +// Test that DataTransfer with a File entry works when posted to a +// service worker that falls back to network. Regression test for +// https://crbug.com/944145. +promise_test(async (t) => { + const scope = '/html/semantics/forms/form-submission-0/resources/'; + const header = `pipe=header(Service-Worker-Allowed,${scope})`; + const script = `resources/fetch-event-network-fallback-worker.js?${header}`; + + const registration = await service_worker_unregister_and_register( + t, script, scope); + await wait_for_state(t, registration.installing, 'activated'); + + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(new File(['foobar'], 'name')); + assert_equals(1, dataTransfer.files.length); + + testinput.files = dataTransfer.files; + testform.submit(); + + const data = await new Promise(resolve => { + onmessage = e => { + if (e.source !== testframe) return; + resolve(e.data); + }; + }); + assert_equals(data, "foobar"); +}, 'Posting a File in a navigation handled by a service worker'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html b/testing/web-platform/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html new file mode 100644 index 0000000000..2144f48271 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<title>DedicatedWorker: ServiceWorker interception</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +// Note that Chrome cannot pass these tests because of https://crbug.com/731599. + +function service_worker_interception_test(url, description) { + promise_test(async t => { + // Register a service worker whose scope includes |url|. + const kServiceWorkerScriptURL = + 'resources/service-worker-interception-service-worker.js'; + const registration = await service_worker_unregister_and_register( + t, kServiceWorkerScriptURL, url); + add_result_callback(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + // Start a dedicated worker for |url|. The top-level script request and any + // module imports should be intercepted by the service worker. + const worker = new Worker(url, { type: 'module' }); + const msg_event = await new Promise(resolve => worker.onmessage = resolve); + assert_equals(msg_event.data, 'LOADED_FROM_SERVICE_WORKER'); + }, description); +} + +service_worker_interception_test( + 'resources/service-worker-interception-network-worker.js', + 'Top-level module loading should be intercepted by a service worker.'); + +service_worker_interception_test( + 'resources/service-worker-interception-static-import-worker.js', + 'Static import should be intercepted by a service worker.'); + +service_worker_interception_test( + 'resources/service-worker-interception-dynamic-import-worker.js', + 'Dynamic import should be intercepted by a service worker.'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/detached-context.https.html b/testing/web-platform/tests/service-workers/service-worker/detached-context.https.html new file mode 100644 index 0000000000..ce8e4cc840 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/detached-context.https.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service WorkerRegistration from a removed iframe</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +</body> +<script> +// NOTE: This file tests corner case behavior that might not be defined in the +// spec. See https://github.com/w3c/ServiceWorker/issues/1221 + +promise_test(t => { + const url = 'resources/blank.html'; + const scope_for_iframe = 'removed-registration' + const scope_for_main = 'resources/' + scope_for_iframe; + const script = 'resources/empty-worker.js'; + let frame; + let resolvedCount = 0; + + return service_worker_unregister(t, scope_for_main) + .then(() => { + return with_iframe(url); + }) + .then(f => { + frame = f; + return navigator.serviceWorker.register(script, + {scope: scope_for_main}); + }) + .then(r => { + add_completion_callback(() => { r.unregister(); }); + return wait_for_state(t, r.installing, 'activated'); + }) + .then(() => { + return frame.contentWindow.navigator.serviceWorker.getRegistration( + scope_for_iframe); + }) + .then(r => { + frame.remove(); + assert_equals(r.installing, null); + assert_equals(r.waiting, null); + assert_equals(r.active.state, 'activated'); + assert_equals(r.scope, normalizeURL(scope_for_main)); + r.onupdatefound = () => { /* empty */ }; + + // We want to verify that unregister() and update() do not + // resolve on a detached registration. We can't check for + // an explicit rejection, though, because not all browsers + // fire rejection callbacks on detached promises. Instead + // we wait for a sample scope to install, activate, and + // unregister before declaring that the promises did not + // resolve. + r.unregister().then(() => resolvedCount += 1, + () => {}); + r.update().then(() => resolvedCount += 1, + () => {}); + return wait_for_activation_on_sample_scope(t, window); + }) + .then(() => { + assert_equals(resolvedCount, 0, + 'methods called on a detached registration should not resolve'); + frame.remove(); + }) + }, 'accessing a ServiceWorkerRegistration from a removed iframe'); + +promise_test(t => { + const script = 'resources/empty-worker.js'; + const scope = 'resources/scope/serviceworker-from-detached'; + + return service_worker_unregister_and_register(t, script, scope) + .then(registration => { + add_completion_callback(() => { registration.unregister(); }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { return with_iframe(scope); }) + .then(frame => { + const worker = frame.contentWindow.navigator.serviceWorker.controller; + const ctor = frame.contentWindow.DOMException; + frame.remove(); + assert_equals(worker.scriptURL, normalizeURL(script)); + assert_equals(worker.state, 'activated'); + worker.onstatechange = () => { /* empty */ }; + assert_throws_dom( + 'InvalidStateError', + ctor, + () => { worker.postMessage(''); }, + 'postMessage on a detached client should throw an exception.'); + }); + }, 'accessing a ServiceWorker object from a removed iframe'); + +promise_test(t => { + const iframe = document.createElement('iframe'); + iframe.src = 'resources/blank.html'; + document.body.appendChild(iframe); + const f = iframe.contentWindow.Function; + function get_navigator() { + return f('return navigator')(); + } + return new Promise(resolve => { + assert_equals(iframe.contentWindow.navigator, get_navigator()); + iframe.src = 'resources/blank.html?navigate-to-new-url'; + iframe.onload = resolve; + }).then(function() { + assert_not_equals(get_navigator().serviceWorker, null); + assert_equals( + get_navigator().serviceWorker, + iframe.contentWindow.navigator.serviceWorker); + iframe.remove(); + }); + }, 'accessing navigator.serviceWorker on a detached iframe'); + +test(t => { + const iframe = document.createElement('iframe'); + iframe.src = 'resources/blank.html'; + document.body.appendChild(iframe); + const f = iframe.contentWindow.Function; + function get_navigator() { + return f('return navigator')(); + } + assert_not_equals(get_navigator().serviceWorker, null); + iframe.remove(); + assert_not_equals(get_navigator().serviceWorker, null); + }, 'accessing navigator on a removed frame'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html b/testing/web-platform/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html new file mode 100644 index 0000000000..581dbeca97 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>embed and object are not intercepted</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<body> +<script> +let registration; + +const kScript = 'resources/embed-and-object-are-not-intercepted-worker.js'; +const kScope = 'resources/'; + +promise_test(t => { + return service_worker_unregister_and_register(t, kScript, kScope) + .then(registration => { + promise_test(() => { + return registration.unregister(); + }, 'restore global state'); + + return wait_for_state(t, registration.installing, 'activated'); + }) + }, 'initialize global state'); + +promise_test(t => { + let frame; + return with_iframe('resources/embed-is-not-intercepted-iframe.html') + .then(f => { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + return frame.contentWindow.test_promise; + }) + .then(result => { + assert_equals(result, 'request for embedded content was not intercepted'); + }); + }, 'requests for EMBED elements of embedded HTML content should not be intercepted by service workers'); + +promise_test(t => { + let frame; + return with_iframe('resources/object-is-not-intercepted-iframe.html') + .then(f => { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + return frame.contentWindow.test_promise; + }) + .then(result => { + assert_equals(result, 'request for embedded content was not intercepted'); + }); + }, 'requests for OBJECT elements of embedded HTML content should not be intercepted by service workers'); + +promise_test(t => { + let frame; + return with_iframe('resources/embed-image-is-not-intercepted-iframe.html') + .then(f => { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + return frame.contentWindow.test_promise; + }) + .then(result => { + assert_equals(result, 'request was not intercepted'); + }); + }, 'requests for EMBED elements of an image should not be intercepted by service workers'); + +promise_test(t => { + let frame; + return with_iframe('resources/object-image-is-not-intercepted-iframe.html') + .then(f => { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + return frame.contentWindow.test_promise; + }) + .then(result => { + assert_equals(result, 'request was not intercepted'); + }); + }, 'requests for OBJECT elements of an image should not be intercepted by service workers'); + +promise_test(t => { + let frame; + return with_iframe('resources/object-navigation-is-not-intercepted-iframe.html') + .then(f => { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + return frame.contentWindow.test_promise; + }) + .then(result => { + assert_equals(result, 'request for embedded content was not intercepted'); + }); + }, 'post-load navigation of OBJECT elements should not be intercepted by service workers'); + + +promise_test(t => { + let frame; + return with_iframe('resources/embed-navigation-is-not-intercepted-iframe.html') + .then(f => { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + return frame.contentWindow.test_promise; + }) + .then(result => { + assert_equals(result, 'request for embedded content was not intercepted'); + }); + }, 'post-load navigation of EMBED elements should not be intercepted by service workers'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html b/testing/web-platform/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html new file mode 100644 index 0000000000..04e98266b4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html @@ -0,0 +1,120 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +function sync_message(worker, message, transfer) { + let wait = new Promise((res, rej) => { + navigator.serviceWorker.addEventListener('message', function(e) { + if (e.data === 'ACK') { + res(); + } else { + rej(); + } + }); + }); + worker.postMessage(message, transfer); + return wait; +} + +function runTest(test, step, testBody) { + var scope = './resources/' + step; + var script = 'resources/extendable-event-async-waituntil.js?' + scope; + return service_worker_unregister_and_register(test, script, scope) + .then(function(registration) { + test.add_cleanup(function() { + return service_worker_unregister(test, scope); + }); + + let worker = registration.installing; + var channel = new MessageChannel(); + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = function(e) { resolve(e.data); } + }); + + return wait_for_state(test, worker, 'activated') + .then(function() { + return sync_message(worker, { step: 'init', port: channel.port2 }, + [channel.port2]); + }) + .then(function() { return testBody(worker); }) + .then(function() { return saw_message; }) + .then(function(output) { + assert_equals(output.result, output.expected); + }) + .then(function() { return sync_message(worker, { step: 'done' }); }); + }); +} + +function msg_event_test(scope, test) { + var testBody = function(worker) { + return sync_message(worker, { step: scope }); + }; + return runTest(test, scope, testBody); +} + +promise_test(msg_event_test.bind(this, 'no-current-extension-different-task'), + 'Test calling waitUntil in a task at the end of the event handler without an existing extension throws'); + +promise_test(msg_event_test.bind(this, 'no-current-extension-different-microtask'), + 'Test calling waitUntil in a microtask at the end of the event handler without an existing extension suceeds'); + +promise_test(msg_event_test.bind(this, 'current-extension-different-task'), + 'Test calling waitUntil in a different task an existing extension succeeds'); + +promise_test(msg_event_test.bind(this, 'during-event-dispatch-current-extension-expired-same-microtask-turn'), + 'Test calling waitUntil at the end of an existing extension promise handler succeeds (event is still being dispatched)'); + +promise_test(msg_event_test.bind(this, 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra'), + 'Test calling waitUntil in a microtask at the end of an existing extension promise handler succeeds (event is still being dispatched)'); + +promise_test(msg_event_test.bind(this, 'after-event-dispatch-current-extension-expired-same-microtask-turn'), + 'Test calling waitUntil in an existing extension promise handler succeeds (event is not being dispatched)'); + +promise_test(msg_event_test.bind(this, 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra'), + 'Test calling waitUntil in a microtask at the end of an existing extension promise handler throws (event is not being dispatched)'); + +promise_test(msg_event_test.bind(this, 'current-extension-expired-different-task'), + 'Test calling waitUntil after the current extension expired in a different task fails'); + +promise_test(msg_event_test.bind(this, 'script-extendable-event'), + 'Test calling waitUntil on a script constructed ExtendableEvent throws exception'); + +promise_test(function(t) { + var testBody = function(worker) { + return with_iframe('./resources/pending-respondwith-async-waituntil'); + } + return runTest(t, 'pending-respondwith-async-waituntil', testBody); + }, 'Test calling waitUntil asynchronously with pending respondWith promise.'); + +promise_test(function(t) { + var testBody = function(worker) { + return with_iframe('./resources/during-event-dispatch-respondwith-microtask-sync-waituntil'); + } + return runTest(t, 'during-event-dispatch-respondwith-microtask-sync-waituntil', testBody); + }, 'Test calling waitUntil synchronously inside microtask of respondWith promise (event is being dispatched).'); + +promise_test(function(t) { + var testBody = function(worker) { + return with_iframe('./resources/during-event-dispatch-respondwith-microtask-async-waituntil'); + } + return runTest(t, 'during-event-dispatch-respondwith-microtask-async-waituntil', testBody); + }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise (event is being dispatched).'); + +promise_test(function(t) { + var testBody = function(worker) { + return with_iframe('./resources/after-event-dispatch-respondwith-microtask-sync-waituntil'); + } + return runTest(t, 'after-event-dispatch-respondwith-microtask-sync-waituntil', testBody); + }, 'Test calling waitUntil synchronously inside microtask of respondWith promise (event is not being dispatched).'); + +promise_test(function(t) { + var testBody = function(worker) { + return with_iframe('./resources/after-event-dispatch-respondwith-microtask-async-waituntil'); + } + return runTest(t, 'after-event-dispatch-respondwith-microtask-async-waituntil', testBody); + }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise (event is not being dispatched).'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/extendable-event-waituntil.https.html b/testing/web-platform/tests/service-workers/service-worker/extendable-event-waituntil.https.html new file mode 100644 index 0000000000..33b4eac5c1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/extendable-event-waituntil.https.html @@ -0,0 +1,140 @@ +<!DOCTYPE html> +<title>ExtendableEvent: waitUntil</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +function runTest(test, scope, onRegister) { + var script = 'resources/extendable-event-waituntil.js?' + scope; + return service_worker_unregister_and_register(test, script, scope) + .then(function(registration) { + test.add_cleanup(function() { + return service_worker_unregister(test, scope); + }); + + return onRegister(registration.installing); + }); +} + +// Sends a SYN to the worker and asynchronously listens for an ACK; sets +// |obj.synced| to true once ack'd. +function syncWorker(worker, obj) { + var channel = new MessageChannel(); + worker.postMessage({port: channel.port2}, [channel.port2]); + return new Promise(function(resolve) { + channel.port1.onmessage = resolve; + }).then(function(e) { + var message = e.data; + assert_equals(message, 'SYNC', + 'Should receive sync message from worker.'); + obj.synced = true; + channel.port1.postMessage('ACK'); + }); +} + +promise_test(function(t) { + // Passing scope as the test switch for worker script. + var scope = 'resources/install-fulfilled'; + var onRegister = function(worker) { + var obj = {}; + + return Promise.all([ + syncWorker(worker, obj), + wait_for_state(t, worker, 'installed') + ]).then(function() { + assert_true( + obj.synced, + 'state should be "installed" after the waitUntil promise ' + + 'for "oninstall" is fulfilled.'); + service_worker_unregister_and_done(t, scope); + }); + }; + return runTest(t, scope, onRegister); + }, 'Test install event waitUntil fulfilled'); + +promise_test(function(t) { + var scope = 'resources/install-multiple-fulfilled'; + var onRegister = function(worker) { + var obj1 = {}; + var obj2 = {}; + + return Promise.all([ + syncWorker(worker, obj1), + syncWorker(worker, obj2), + wait_for_state(t, worker, 'installed') + ]).then(function() { + assert_true( + obj1.synced && obj2.synced, + 'state should be "installed" after all waitUntil promises ' + + 'for "oninstall" are fulfilled.'); + }); + }; + return runTest(t, scope, onRegister); + }, 'Test ExtendableEvent multiple waitUntil fulfilled.'); + +promise_test(function(t) { + var scope = 'resources/install-reject-precedence'; + var onRegister = function(worker) { + var obj1 = {}; + var obj2 = {}; + + return Promise.all([ + syncWorker(worker, obj1) + .then(function() { + syncWorker(worker, obj2); + }), + wait_for_state(t, worker, 'redundant') + ]).then(function() { + assert_true( + obj1.synced, + 'The "redundant" state was entered after the first "extend ' + + 'lifetime promise" resolved.' + ); + assert_true( + obj2.synced, + 'The "redundant" state was entered after the third "extend ' + + 'lifetime promise" resolved.' + ); + }); + }; + return runTest(t, scope, onRegister); + }, 'Test ExtendableEvent waitUntil reject precedence.'); + +promise_test(function(t) { + var scope = 'resources/activate-fulfilled'; + var onRegister = function(worker) { + var obj = {}; + return wait_for_state(t, worker, 'activating') + .then(function() { + return Promise.all([ + syncWorker(worker, obj), + wait_for_state(t, worker, 'activated') + ]); + }) + .then(function() { + assert_true( + obj.synced, + 'state should be "activated" after the waitUntil promise ' + + 'for "onactivate" is fulfilled.'); + }); + }; + return runTest(t, scope, onRegister); + }, 'Test activate event waitUntil fulfilled'); + +promise_test(function(t) { + var scope = 'resources/install-rejected'; + var onRegister = function(worker) { + return wait_for_state(t, worker, 'redundant'); + }; + return runTest(t, scope, onRegister); + }, 'Test install event waitUntil rejected'); + +promise_test(function(t) { + var scope = 'resources/activate-rejected'; + var onRegister = function(worker) { + return wait_for_state(t, worker, 'activated'); + }; + return runTest(t, scope, onRegister); + }, 'Test activate event waitUntil rejected.'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-audio-tainting.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-audio-tainting.https.html new file mode 100644 index 0000000000..9821759bc7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-audio-tainting.https.html @@ -0,0 +1,47 @@ +<!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="resources/test-helpers.sub.js?pipe=sub"></script> +<script> +promise_test(async (t) => { + const SCOPE = 'resources/empty.html'; + const SCRIPT = 'resources/fetch-rewrite-worker.js'; + const host_info = get_host_info(); + const REMOTE_ORIGIN = host_info.HTTPS_REMOTE_ORIGIN; + + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + await wait_for_state(t, reg.installing, 'activated'); + const frame = await with_iframe(SCOPE); + + const doc = frame.contentDocument; + const win = frame.contentWindow; + + const context = new win.AudioContext(); + try { + context.suspend(); + const audio = doc.createElement('audio'); + audio.autoplay = true; + const source = context.createMediaElementSource(audio); + const spn = context.createScriptProcessor(16384, 1, 1); + source.connect(spn).connect(context.destination); + const url = `${REMOTE_ORIGIN}/webaudio/resources/sin_440Hz_-6dBFS_1s.wav`; + audio.src = '/test?url=' + encodeURIComponent(url); + doc.body.appendChild(audio); + + await new Promise((resolve) => { + audio.addEventListener('playing', resolve); + }); + await context.resume(); + const event = await new Promise((resolve) => { + spn.addEventListener('audioprocess', resolve); + }); + const data = event.inputBuffer.getChannelData(0); + for (const e of data) { + assert_equals(e, 0); + } + } finally { + context.close(); + } + }, 'Verify CORS XHR of fetch() in a Service Worker'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html new file mode 100644 index 0000000000..dab2153baa --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html @@ -0,0 +1,57 @@ +<!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="resources/test-helpers.sub.js"></script> +<meta charset="utf-8"> +<title>canvas tainting when written twice</title> +<script> +function loadImage(doc, url) { + return new Promise((resolve, reject) => { + const image = doc.createElement('img'); + image.onload = () => { resolve(image); } + image.onerror = () => { reject('failed to load: ' + url); }; + image.src = url; + }); +} + +// Tests that a canvas is tainted after it's written to with both a clear image +// and opaque image from the same URL. A bad implementation might cache the +// info of the clear image and assume the opaque image is also clear because +// it's from the same URL. See https://crbug.com/907047 for details. +promise_test(async (t) => { + // Set up a service worker and a controlled iframe. + const script = 'resources/fetch-canvas-tainting-double-write-worker.js'; + const scope = 'resources/fetch-canvas-tainting-double-write-iframe.html'; + const registration = await service_worker_unregister_and_register( + t, script, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + const iframe = await with_iframe(scope); + t.add_cleanup(() => iframe.remove()); + + // Load the same cross-origin image URL through the controlled iframe and + // this uncontrolled frame. The service worker responds with a same-origin + // image for the controlled iframe, so it is cleartext. + const imagePath = base_path() + 'resources/fetch-access-control.py?PNGIMAGE'; + const imageUrl = get_host_info()['HTTPS_REMOTE_ORIGIN'] + imagePath; + const clearImage = await loadImage(iframe.contentDocument, imageUrl); + const opaqueImage = await loadImage(document, imageUrl); + + // Set up a canvas for testing tainting. + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.width = clearImage.width; + canvas.height = clearImage.height; + + // The clear image and the opaque image have the same src URL. But... + + // ... the clear image doesn't taint the canvas. + context.drawImage(clearImage, 0, 0); + assert_true(canvas.toDataURL().length > 0); + + // ... the opaque image taints the canvas. + context.drawImage(opaqueImage, 0, 0); + assert_throws_dom('SecurityError', () => { canvas.toDataURL(); }); +}, 'canvas is tainted after writing both a non-opaque image and an opaque image from the same URL'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html new file mode 100644 index 0000000000..2132381122 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: canvas tainting of the fetched image using cached responses</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script src="resources/fetch-canvas-tainting-tests.js"></script> +<body> +<script> +do_canvas_tainting_tests({ + resource_path: base_path() + 'resources/fetch-access-control.py?PNGIMAGE', + cache: true +}); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html new file mode 100644 index 0000000000..57dc7d98ca --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: canvas tainting of the fetched image</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script src="resources/fetch-canvas-tainting-tests.js"></script> +<body> +<script> +do_canvas_tainting_tests({ + resource_path: base_path() + 'resources/fetch-access-control.py?PNGIMAGE', + cache: false +}); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html new file mode 100644 index 0000000000..c37e8e5624 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: canvas tainting of the fetched video using cache responses</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script src="resources/fetch-canvas-tainting-tests.js"></script> +<body> +<script> +do_canvas_tainting_tests({ + resource_path: base_path() + 'resources/fetch-access-control.py?VIDEO', + cache: true +}); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html new file mode 100644 index 0000000000..0ee41e96f2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Canvas tainting due to video whose responses are fetched via a service worker including range requests</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script src="/common/get-host-info.sub.js"></script> +<body> +<script> +// These tests try to test canvas tainting due to a <video> element. The video +// src URL is same-origin as the page, but the response is fetched via a service +// worker that does tricky things like returning opaque responses from another +// origin. Furthermore, this tests range requests so there are multiple +// responses. +// +// We test range requests by having the server return 206 Partial Content to the +// first request (which doesn't necessarily have a "Range" header or one with a +// byte range). Then the <video> element automatically makes ranged requests +// (the "Range" HTTP request header specifies a byte range). The server responds +// to these with 206 Partial Content for the given range. +let unique = 0; // See https://bugzilla.mozilla.org/show_bug.cgi?id=1129121 +function range_request_test(script, expected, description, + {remote_origin_src} = {}) { + promise_test(t => { + let frame; + let registration; + add_result_callback(() => { + if (frame) frame.remove(); + if (registration) registration.unregister(); + }); + + const scope = 'resources/fetch-canvas-tainting-iframe.html'; + return service_worker_unregister_and_register(t, script, scope) + .then(r => { + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { + return with_iframe(scope); + }) + .then(f => { + frame = f; + // Add "?PartialContent&VIDEO" to get a video resource from the + // server using range requests. + let video_url = + 'fetch-access-control.py?PartialContent&VIDEO=' + unique++; + if (remote_origin_src) { + video_url = get_host_info().HTTPS_REMOTE_ORIGIN + + base_path() + 'resources/' + video_url; + } + return frame.contentWindow.create_test_case_promise(video_url); + }) + .then(result => { + assert_equals(result, expected); + }); + }, description); +} + +// We want to consider a number of scenarios: +// (1) Range responses come from a single origin, the same-origin as the page. +// The canvas should not be tainted. +range_request_test( + 'resources/fetch-event-network-fallback-worker.js', + 'NOT_TAINTED', + 'range responses from single origin (same-origin)'); + +// (2) Range responses come from a single origin, cross-origin from the page +// (and without CORS sharing). This is not possible to test, since service +// worker can't make a request with a "Range" HTTP header in no-cors mode. + +// (3) Range responses come from multiple origins. The first response comes from +// cross-origin (and without CORS sharing, so is opaque). Subsequent +// responses come from same-origin. This should result in a load error, as regardless of canvas +// loading range requests from multiple opaque origins can reveal information across those origins. +range_request_test( + 'resources/range-request-to-different-origins-worker.js', + 'LOAD_ERROR', + 'range responses from multiple origins (cross-origin first)'); + +// (4) Range responses come from multiple origins. The first response comes from +// same-origin. Subsequent responses come from cross-origin (and without +// CORS sharing). Like (2) this is not possible since the service worker +// cannot make range requests cross-origin. + +// (5) Range responses come from a single origin, with a mix of opaque and +// non-opaque responses. The first request uses 'no-cors' mode to +// receive an opaque response, and subsequent range requests use 'cors' +// to receive non-opaque responses. The canvas should be tainted. +range_request_test( + 'resources/range-request-with-different-cors-modes-worker.js', + 'TAINTED', + 'range responses from single origin with both opaque and non-opaque responses'); + +// (6) Range responses come from a single origin, with a mix of opaque and +// non-opaque responses. The first request uses 'cors' mode to +// receive an non-opaque response, and subsequent range requests use +// 'no-cors' to receive non-opaque responses. Like (2) this is not possible. + +// (7) The first Range response is synthesized using the Response constructor. +// Subsequent responses are same-origin, not handled by the service worker. +range_request_test( + 'resources/range-request-with-synth-head-worker.js', + 'NOT_TAINTED', + 'synth and same-origin fallback range responses'); + +// (8) The first Range response is synthesized using the Response constructor. +// Subsequent responses are cross-origin, not handled by the service worker, +// no-cors and opaque. +range_request_test( + 'resources/range-request-with-synth-head-worker.js', + 'LOAD_ERROR', + 'synth and cross-origin fallback range responses', + {remote_origin_src: true}); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html new file mode 100644 index 0000000000..e8c23a2edd --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: canvas tainting of the fetched video</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script src="resources/fetch-canvas-tainting-tests.js"></script> +<body> +<script> +do_canvas_tainting_tests({ + resource_path: base_path() + 'resources/fetch-access-control.py?VIDEO', + cache: false +}); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html new file mode 100644 index 0000000000..317b02175f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<title>Service Worker: CORS-exposed header names should be transferred correctly</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(async function(t) { + const SCOPE = 'resources/simple.html'; + const SCRIPT = 'resources/fetch-cors-exposed-header-names-worker.js'; + const host_info = get_host_info(); + + const URL = get_host_info().HTTPS_REMOTE_ORIGIN + + '/service-workers/service-worker/resources/simple.txt?pipe=' + + 'header(access-control-allow-origin,*)|' + + 'header(access-control-expose-headers,*)|' + + 'header(foo,bar)|' + + 'header(set-cookie,X)'; + + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + await wait_for_state(t, reg.installing, 'activated'); + const frame = await with_iframe(SCOPE); + + const response = await frame.contentWindow.fetch(URL); + const headers = response.headers; + assert_equals(headers.get('foo'), 'bar'); + assert_equals(headers.get('set-cookie'), null); + assert_equals(headers.get('access-control-expose-headers'), '*'); + }, 'CORS-exposed header names for a response from sw'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-cors-xhr.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-cors-xhr.https.html new file mode 100644 index 0000000000..f8ff445673 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-cors-xhr.https.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<title>Service Worker: CORS XHR of fetch()</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<body> +<script> +promise_test(function(t) { + var SCOPE = 'resources/fetch-cors-xhr-iframe.html'; + var SCRIPT = 'resources/fetch-rewrite-worker.js'; + var host_info = get_host_info(); + + return login_https(t) + .then(function() { + return service_worker_unregister_and_register(t, SCRIPT, SCOPE); + }) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, SCOPE); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(function(frame) { + t.add_cleanup(function() { + frame.remove(); + }); + + return new Promise(function(resolve, reject) { + var channel = new MessageChannel(); + channel.port1.onmessage = (event) => { + if (event.data === 'done') { + resolve(); + return; + } + test(() => { + assert_true(event.data.result); + }, event.data.testName); + }; + frame.contentWindow.postMessage({}, + host_info['HTTPS_ORIGIN'], + [channel.port2]); + }); + }); + }, 'Verify CORS XHR of fetch() in a Service Worker'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-csp.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-csp.https.html new file mode 100644 index 0000000000..9e7b242b69 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-csp.https.html @@ -0,0 +1,138 @@ +<!DOCTYPE html> +<title>Service Worker: CSP control of fetch()</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> + +function assert_resolves(promise, description) { + return promise.catch(function(reason) { + throw new Error(description + ' - ' + reason.message); + }); +} + +function assert_rejects(promise, description) { + return promise.then( + function() { throw new Error(description); }, + function() {}); +} + +promise_test(function(t) { + var SCOPE = 'resources/fetch-csp-iframe.html'; + var SCRIPT = 'resources/fetch-rewrite-worker.js'; + var host_info = get_host_info(); + var IMAGE_PATH = + base_path() + 'resources/fetch-access-control.py?PNGIMAGE'; + var IMAGE_URL = host_info['HTTPS_ORIGIN'] + IMAGE_PATH; + var REMOTE_IMAGE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_PATH; + var REDIRECT_URL = + host_info['HTTPS_ORIGIN'] + base_path() + 'resources/redirect.py'; + var frame; + + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, SCOPE); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe( + SCOPE + '?' + + encodeURIComponent('img-src ' + host_info['HTTPS_ORIGIN'] + + '; script-src \'unsafe-inline\'')); + }) + .then(function(f) { + frame = f; + return assert_resolves( + frame.contentWindow.load_image(IMAGE_URL), + 'Allowed scope image resource should be loaded.'); + }) + .then(function() { + return assert_rejects( + frame.contentWindow.load_image(REMOTE_IMAGE_URL), + 'Disallowed scope image resource should not be loaded.'); + }) + .then(function() { + return assert_resolves( + frame.contentWindow.load_image( + // The request for IMAGE_URL will be fetched in SW. + './sample?url=' + encodeURIComponent(IMAGE_URL)), + 'Allowed scope image resource which was fetched via SW should ' + + 'be loaded.'); + }) + .then(function() { + return assert_rejects( + frame.contentWindow.load_image( + // The request for REMOTE_IMAGE_URL will be fetched in SW. + './sample?mode=no-cors&url=' + + encodeURIComponent(REMOTE_IMAGE_URL)), + 'Disallowed scope image resource which was fetched via SW ' + + 'should not be loaded.'); + }) + .then(function() { + frame.remove(); + return with_iframe( + SCOPE + '?' + + encodeURIComponent( + 'img-src ' + REDIRECT_URL + + '; script-src \'unsafe-inline\'')); + }) + .then(function(f) { + frame = f; + return assert_resolves( + frame.contentWindow.load_image( + // Set 'ignore' not to call respondWith() in the SW. + REDIRECT_URL + '?ignore&Redirect=' + + encodeURIComponent(IMAGE_URL)), + 'When the request was redirected, CSP match algorithm should ' + + 'ignore the path component of the URL.'); + }) + .then(function() { + return assert_resolves( + frame.contentWindow.load_image( + // This request will be fetched via SW and redirected by + // redirect.php. + REDIRECT_URL + '?Redirect=' + encodeURIComponent(IMAGE_URL)), + 'When the request was redirected via SW, CSP match algorithm ' + + 'should ignore the path component of the URL.'); + }) + .then(function() { + return assert_resolves( + frame.contentWindow.load_image( + // The request for IMAGE_URL will be fetched in SW. + REDIRECT_URL + '?url=' + encodeURIComponent(IMAGE_URL)), + 'When the request was fetched via SW, CSP match algorithm ' + + 'should ignore the path component of the URL.'); + }) + .then(function() { + return assert_resolves( + frame.contentWindow.fetch(IMAGE_URL + "&fetch1", { mode: 'no-cors'}), + 'Allowed scope fetch resource should be loaded.'); + }) + .then(function() { + return assert_resolves( + frame.contentWindow.fetch( + // The request for IMAGE_URL will be fetched in SW. + './sample?url=' + encodeURIComponent(IMAGE_URL + '&fetch2'), { mode: 'no-cors'}), + 'Allowed scope fetch resource which was fetched via SW should be loaded.'); + }) + .then(function() { + return assert_rejects( + frame.contentWindow.fetch(REMOTE_IMAGE_URL + "&fetch3", { mode: 'no-cors'}), + 'Disallowed scope fetch resource should not be loaded.'); + }) + .then(function() { + return assert_rejects( + frame.contentWindow.fetch( + // The request for REMOTE_IMAGE_URL will be fetched in SW. + './sample?url=' + encodeURIComponent(REMOTE_IMAGE_URL + '&fetch4'), { mode: 'no-cors'}), + 'Disallowed scope fetch resource which was fetched via SW should not be loaded.'); + }) + .then(function() { + frame.remove(); + }); + }, 'Verify CSP control of fetch() in a Service Worker'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-error.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-error.https.html new file mode 100644 index 0000000000..e9fdf57431 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-error.https.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> +<head> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +</head> +<body> +<script> +const scope = "./resources/in-scope"; + +promise_test(async (test) => { + const registration = await service_worker_unregister_and_register( + test, "./resources/fetch-error-worker.js", scope); + promise_test(async () => registration.unregister(), + "Unregister service worker"); + await wait_for_state(test, registration.installing, 'activated'); +}, "Setup service worker"); + +promise_test(async (test) => { + const iframe = await with_iframe(scope); + test.add_cleanup(() => iframe.remove()); + const response = await iframe.contentWindow.fetch("fetch-error-test"); + return promise_rejects_js(test, iframe.contentWindow.TypeError, + response.text(), 'text() should reject'); +}, "Make sure a load that makes progress does not time out"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-add-async.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-add-async.https.html new file mode 100644 index 0000000000..ac13e4f416 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-add-async.https.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<title>Service Worker: Fetch event added asynchronously doesn't throw</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +service_worker_test( + 'resources/fetch-event-add-async-worker.js'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html new file mode 100644 index 0000000000..4812d8a915 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<title>ServiceWorker: navigator.serviceWorker.waiting</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +promise_test(function(t) { + var scope = + 'resources/fetch-event-after-navigation-within-page-iframe.html' + + '?hashchange'; + var worker = 'resources/simple-intercept-worker.js'; + var frame; + + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + frame = f; + return frame.contentWindow.fetch_url('simple.txt'); + }) + .then(function(response) { + assert_equals(response, 'intercepted by service worker'); + frame.contentWindow.location.hash = 'foo'; + return frame.contentWindow.fetch_url('simple.txt'); + }) + .then(function(response) { + assert_equals(response, 'intercepted by service worker'); + frame.remove(); + }) + }, 'Service Worker should respond to fetch event after the hash changes'); + +promise_test(function(t) { + var scope = + 'resources/fetch-event-after-navigation-within-page-iframe.html' + + '?pushState'; + var worker = 'resources/simple-intercept-worker.js'; + var frame; + + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + frame = f; + return frame.contentWindow.fetch_url('simple.txt'); + }) + .then(function(response) { + assert_equals(response, 'intercepted by service worker'); + frame.contentWindow.history.pushState('', '', 'bar'); + return frame.contentWindow.fetch_url('simple.txt'); + }) + .then(function(response) { + assert_equals(response, 'intercepted by service worker'); + frame.remove(); + }) + }, 'Service Worker should respond to fetch event after the pushState'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html new file mode 100644 index 0000000000..d9147f8549 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<html> +<title>respondWith cannot be called asynchronously</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +// This file has tests that call respondWith() asynchronously. + +let frame; +let worker; +const script = 'resources/fetch-event-async-respond-with-worker.js'; +const scope = 'resources/simple.html'; + +// Global setup: this must be the first promise_test. +promise_test(async (t) => { + const registration = + await service_worker_unregister_and_register(t, script, scope); + worker = registration.installing; + await wait_for_state(t, worker, 'activated'); + frame = await with_iframe(scope); +}, 'global setup'); + +// Waits for a single message from the service worker and then removes the +// message handler. Not safe for concurrent use. +function wait_for_message() { + return new Promise((resolve) => { + const handler = (event) => { + navigator.serviceWorker.removeEventListener('message', handler); + resolve(event.data); + }; + navigator.serviceWorker.addEventListener('message', handler); + }); +} + +// Does one test case. It fetches |url|. The service worker gets a fetch event +// for |url| and attempts to call respondWith() asynchronously. It reports back +// to the test whether an exception was thrown. +async function do_test(url) { + // Send a message to tell the worker a new test case is starting. + const message = wait_for_message(); + worker.postMessage('initializeMessageHandler'); + const response = await message; + assert_equals(response, 'messageHandlerInitialized'); + + // Start a fetch. + const fetchPromise = frame.contentWindow.fetch(url); + + // Receive the test result from the service worker. + const result = wait_for_message(); + await fetchPromise.then(()=> {}, () => {}); + return result; +}; + +promise_test(async (t) => { + const result = await do_test('respondWith-in-task'); + assert_true(result.didThrow, 'should throw'); + assert_equals(result.error, 'InvalidStateError'); +}, 'respondWith in a task throws InvalidStateError'); + +promise_test(async (t) => { + const result = await do_test('respondWith-in-microtask'); + assert_equals(result.didThrow, false, 'should not throw'); +}, 'respondWith in a microtask does not throw'); + +// Global cleanup: the final promise_test. +promise_test(async (t) => { + if (frame) + frame.remove(); + await service_worker_unregister(t, scope); +}, 'global cleanup'); +</script> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-handled.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-handled.https.html new file mode 100644 index 0000000000..08b88ce377 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-handled.https.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<html> +<title>Service Worker: FetchEvent.handled</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +let frame = null; +let worker = null; +const script = 'resources/fetch-event-handled-worker.js'; +const scope = 'resources/simple.html'; +const channel = new MessageChannel(); + +// Wait for a message from the service worker and removes the message handler. +function wait_for_message_from_worker() { + return new Promise((resolve) => channel.port2.onmessage = (event) => resolve(event.data)); +} + +// Global setup: this must be the first promise_test. +promise_test(async (t) => { + const registration = + await service_worker_unregister_and_register(t, script, scope); + worker = registration.installing; + if (!worker) + worker = registration.active; + worker.postMessage({port:channel.port1}, [channel.port1]); + await wait_for_state(t, worker, 'activated'); +}, 'global setup'); + +promise_test(async (t) => { + const promise = with_iframe(scope); + const message = await wait_for_message_from_worker(); + frame = await promise; + assert_equals(message, 'RESOLVED'); +}, 'FetchEvent.handled should resolve when respondWith() is not called for a' + + ' navigation request'); + +promise_test(async (t) => { + frame.contentWindow.fetch('sample.txt?respondWith-not-called'); + const message = await wait_for_message_from_worker(); + assert_equals(message, 'RESOLVED'); +}, 'FetchEvent.handled should resolve when respondWith() is not called for a' + + ' sub-resource request'); + +promise_test(async (t) => { + frame.contentWindow.fetch( + 'sample.txt?respondWith-not-called-and-event-canceled').catch((e) => {}); + const message = await wait_for_message_from_worker(); + assert_equals(message, 'REJECTED'); +}, 'FetchEvent.handled should reject when respondWith() is not called and the' + + ' event is canceled'); + +promise_test(async (t) => { + frame.contentWindow.fetch( + 'sample.txt?respondWith-called-and-promise-resolved'); + const message = await wait_for_message_from_worker(); + assert_equals(message, 'RESOLVED'); +}, 'FetchEvent.handled should resolve when the promise provided' + + ' to respondWith() is resolved'); + +promise_test(async (t) => { + frame.contentWindow.fetch( + 'sample.txt?respondWith-called-and-promise-resolved-to-invalid-response') + .catch((e) => {}); + const message = await wait_for_message_from_worker(); + assert_equals(message, 'REJECTED'); +}, 'FetchEvent.handled should reject when the promise provided' + + ' to respondWith() is resolved to an invalid response'); + +promise_test(async (t) => { + frame.contentWindow.fetch( + 'sample.txt?respondWith-called-and-promise-rejected').catch((e) => {}); + const message = await wait_for_message_from_worker(); + assert_equals(message, 'REJECTED'); +}, 'FetchEvent.handled should reject when the promise provided to' + + ' respondWith() is rejected'); + +// Global cleanup: the final promise_test. +promise_test(async (t) => { + if (frame) + frame.remove(); + await service_worker_unregister(t, scope); +}, 'global cleanup'); +</script> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html new file mode 100644 index 0000000000..3cf5922f39 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<body> +<p>Click <a href="resources/install-worker.html?isHistoryNavigation&script=fetch-event-test-worker.js">this link</a>. + Once you see "method = GET,..." in the page, go to another page, and then go back to the page using the Backward button. + You should see "method = GET, isHistoryNavigation = true". +</p> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html new file mode 100644 index 0000000000..401939b3cb --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<body> +<p>Click <a href="resources/install-worker.html?isHistoryNavigation&script=fetch-event-test-worker.js">this link</a>. + Once you see "method = GET,..." in the page, go back to this page using the Backward button, and then go to the second page using the Forward button. + You should see "method = GET, isHistoryNavigation = true". +</p> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html new file mode 100644 index 0000000000..cf1feccf6e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html @@ -0,0 +1,31 @@ +<!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="resources/test-helpers.sub.js"></script> +<body> +<script> +const worker = 'resources/fetch-event-test-worker.js'; + +promise_test(async (t) => { + const scope = 'resources/simple.html?isReloadNavigation'; + + const reg = await service_worker_unregister_and_register(t, worker, scope); + await wait_for_state(t, reg.installing, 'activated'); + const frame = await with_iframe(scope); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isReloadNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentDocument.body.innerText = + 'Reload this frame manually!'; + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isReloadNavigation = true'); + frame.remove(); + await reg.unregister(); +}, 'FetchEvent#request.isReloadNavigation is true for manual reload.'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html new file mode 100644 index 0000000000..a349f07c36 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<body> +<p>Click <a href="resources/install-worker.html?isReloadNavigation&script=fetch-event-test-worker.js">this link</a>. + Once you see "method = GET,..." in the page, reload the page. + You will see "method = GET, isReloadNavigation = true". +</p> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-network-error.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-network-error.https.html new file mode 100644 index 0000000000..fea2ad1e3c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-network-error.https.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<title>Service Worker: Fetch event network error</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var resolve_test_done; + +var test_done_promise = new Promise(function(resolve) { + resolve_test_done = resolve; + }); + +// Called by the child frame. +function notify_test_done(result) { + resolve_test_done(result); +} + +promise_test(function(t) { + var scope = 'resources/fetch-event-network-error-controllee-iframe.html'; + var script = 'resources/fetch-event-network-error-worker.js'; + var frame; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(f) { + frame = f; + return test_done_promise; + }) + .then(function(result) { + frame.remove(); + assert_equals(result, 'PASS'); + }); + }, 'Rejecting the fetch event or using preventDefault() causes a network ' + + 'error'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-redirect.https.html new file mode 100644 index 0000000000..5229284757 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-redirect.https.html @@ -0,0 +1,1038 @@ +<!DOCTYPE html> +<title>Service Worker: Fetch Event Redirect Handling</title> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +// ------------------------ +// Utilities for testing non-navigation requests that are intercepted with +// a redirect. + +const host_info = get_host_info(); +const kScript = 'resources/fetch-rewrite-worker.js'; +const kScope = host_info['HTTPS_ORIGIN'] + base_path() + + 'resources/blank.html?fetch-event-redirect'; +let frame; + +function redirect_fetch_test(t, test) { + const hostKeySuffix = test['url_credentials'] ? '_WITH_CREDS' : ''; + const successPath = base_path() + 'resources/success.py'; + + let acaOrigin = ''; + let host = host_info['HTTPS_ORIGIN' + hostKeySuffix]; + if (test['redirect_dest'] === 'no-cors') { + host = host_info['HTTPS_REMOTE_ORIGIN' + hostKeySuffix] + } else if (test['redirect_dest'] === 'cors') { + acaOrigin = '?ACAOrigin=' + encodeURIComponent(host_info['HTTPS_ORIGIN']); + host = host_info['HTTPS_REMOTE_ORIGIN' + hostKeySuffix] + } + + const dest = '?Redirect=' + encodeURIComponent(host + successPath + acaOrigin); + const expectedTypeParam = + test['expected_type'] + ? '&expected_type=' + test['expected_type'] + : ''; + const expectedRedirectedParam = + test['expected_redirected'] + ? '&expected_redirected=' + test['expected_redirected'] + : ''; + const url = '/' + test.name + + '?url=' + encodeURIComponent('redirect.py' + dest) + + expectedTypeParam + expectedRedirectedParam + const request = new Request(url, test.request_init); + + if (test.should_reject) { + return promise_rejects_js( + t, + frame.contentWindow.TypeError, + frame.contentWindow.fetch(request), + 'Must fail to fetch: url=' + url); + } + return frame.contentWindow.fetch(request).then((response) => { + assert_equals(response.type, test.expected_type, + 'response.type'); + assert_equals(response.redirected, test.expected_redirected, + 'response.redirected'); + if (response.type === 'opaque' || response.type === 'opaqueredirect') { + return; + } + return response.json().then((json) => { + assert_equals(json.result, 'success', 'JSON result must be "success".'); + }); + }); +} + +// Set up the service worker and the frame. +promise_test(t => { + return service_worker_unregister_and_register(t, kScript, kScope) + .then(registration => { + promise_test(() => { + return registration.unregister(); + }, 'restore global state'); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { + return with_iframe(kScope); + }) + .then(f => { + frame = f; + add_completion_callback(() => { f.remove(); }); + }); + }, 'initialize global state'); + +// ------------------------ +// Test every combination of: +// - RequestMode (same-origin, cors, no-cors) +// - RequestRedirect (manual, follow, error) +// - redirect destination origin (same-origin, cors, no-cors) +// - redirect destination credentials (no user/pass, user/pass) +// +// TODO: add navigation requests +// TODO: add redirects to data URI and verify same-origin data-URL flag behavior +// TODO: add test where original redirect URI is cross-origin +// TODO: verify final method is correct for 301, 302, and 303 +// TODO: verify CORS redirect results in all further redirects being +// considered cross origin + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-cors-redirects-to-sameorigin-nocreds', + redirect_dest: 'same-origin', + url_credentials: false, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'cors' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, cors mode Request redirected to ' + + 'same-origin without credentials should succeed opaqueredirect ' + + 'interception and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-cors-redirects-to-nocors-nocreds', + redirect_dest: 'no-cors', + url_credentials: false, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'cors' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, cors mode Request redirected to ' + + 'no-cors without credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-cors-redirects-to-cors-nocreds', + redirect_dest: 'cors', + url_credentials: false, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'cors' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, cors mode Request redirected to ' + + 'cors without credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-sameorigin-redirects-to-sameorigin-nocreds', + redirect_dest: 'same-origin', + url_credentials: false, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'same-origin' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' + + 'same-origin without credentials should succeed opaqueredirect ' + + 'interception and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-sameorigin-redirects-to-nocors-nocreds', + redirect_dest: 'no-cors', + url_credentials: false, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'same-origin' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' + + 'no-cors without credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-sameorigin-redirects-to-cors-nocreds', + redirect_dest: 'cors', + url_credentials: false, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'same-origin' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' + + 'cors without credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-nocors-redirects-to-sameorigin-nocreds', + redirect_dest: 'same-origin', + url_credentials: false, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'no-cors' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' + + 'same-origin without credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-nocors-redirects-to-nocors-nocreds', + redirect_dest: 'no-cors', + url_credentials: false, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'no-cors' + }, + // This should succeed because its redirecting from same-origin to + // cross-origin. Since the same-origin URL provides the location + // header the manual redirect mode should result in an opaqueredirect + // response. + should_reject: false + }); +}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' + + 'no-cors without credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-nocors-redirects-to-cors-nocreds', + redirect_dest: 'cors', + url_credentials: false, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'no-cors' + }, + // This should succeed because its redirecting from same-origin to + // cross-origin. Since the same-origin URL provides the location + // header the manual redirect mode should result in an opaqueredirect + // response. + should_reject: false + }); +}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' + + 'cors without credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-cors-redirects-to-sameorigin-creds', + redirect_dest: 'same-origin', + url_credentials: true, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'cors' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, cors mode Request redirected to ' + + 'same-origin with credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-cors-redirects-to-nocors-creds', + redirect_dest: 'no-cors', + url_credentials: true, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'cors' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, cors mode Request redirected to ' + + 'no-cors with credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-cors-redirects-to-cors-creds', + redirect_dest: 'cors', + url_credentials: true, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'cors' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, cors mode Request redirected to ' + + 'cors with credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-sameorigin-redirects-to-sameorigin-creds', + redirect_dest: 'same-origin', + url_credentials: true, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'same-origin' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' + + 'same-origin with credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-sameorigin-redirects-to-nocors-creds', + redirect_dest: 'no-cors', + url_credentials: true, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'same-origin' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' + + 'no-cors with credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-sameorigin-redirects-to-cors-creds', + redirect_dest: 'cors', + url_credentials: true, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'same-origin' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' + + 'cors with credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-nocors-redirects-to-sameorigin-creds', + redirect_dest: 'same-origin', + url_credentials: true, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'no-cors' + }, + should_reject: false + }); +}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' + + 'same-origin with credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-nocors-redirects-to-nocors-creds', + redirect_dest: 'no-cors', + url_credentials: true, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'no-cors' + }, + // This should succeed because its redirecting from same-origin to + // cross-origin. Since the same-origin URL provides the location + // header the manual redirect mode should result in an opaqueredirect + // response. + should_reject: false + }); +}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' + + 'no-cors with credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-manual-nocors-redirects-to-cors-creds', + redirect_dest: 'cors', + url_credentials: true, + expected_type: 'opaqueredirect', + expected_redirected: false, + request_init: { + redirect: 'manual', + mode: 'no-cors' + }, + // This should succeed because its redirecting from same-origin to + // cross-origin. Since the same-origin URL provides the location + // header the manual redirect mode should result in an opaqueredirect + // response. + should_reject: false + }); +}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' + + 'cors with credentials should succeed opaqueredirect interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-cors-redirects-to-sameorigin-nocreds', + redirect_dest: 'same-origin', + url_credentials: false, + expected_type: 'basic', + expected_redirected: true, + request_init: { + redirect: 'follow', + mode: 'cors' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, cors mode Request redirected to ' + + 'same-origin without credentials should succeed interception ' + + 'and response should be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-cors-redirects-to-nocors-nocreds', + redirect_dest: 'no-cors', + url_credentials: false, + request_init: { + redirect: 'follow', + mode: 'cors' + }, + // should reject because CORS requests require CORS headers on cross-origin + // resources + should_reject: true + }); +}, 'Non-navigation, follow redirect, cors mode Request redirected to ' + + 'no-cors without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-cors-redirects-to-cors-nocreds', + redirect_dest: 'cors', + url_credentials: false, + expected_type: 'cors', + expected_redirected: true, + request_init: { + redirect: 'follow', + mode: 'cors' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, cors mode Request redirected to ' + + 'cors without credentials should succeed interception ' + + 'and response should be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-sameorigin-redirects-to-sameorigin-nocreds', + redirect_dest: 'same-origin', + url_credentials: false, + expected_type: 'basic', + expected_redirected: true, + request_init: { + redirect: 'follow', + mode: 'same-origin' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' + + 'same-origin without credentials should succeed interception ' + + 'and response should be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-sameorigin-redirects-to-nocors-nocreds', + redirect_dest: 'no-cors', + url_credentials: false, + request_init: { + redirect: 'follow', + mode: 'same-origin' + }, + // should reject because same-origin requests cannot load cross-origin + // resources + should_reject: true + }); +}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' + + 'no-cors without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-sameorigin-redirects-to-cors-nocreds', + redirect_dest: 'cors', + url_credentials: false, + request_init: { + redirect: 'follow', + mode: 'same-origin' + }, + // should reject because same-origin requests cannot load cross-origin + // resources + should_reject: true + }); +}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' + + 'cors without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-nocors-redirects-to-sameorigin-nocreds', + redirect_dest: 'same-origin', + url_credentials: false, + expected_type: 'basic', + expected_redirected: true, + request_init: { + redirect: 'follow', + mode: 'no-cors' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' + + 'same-origin without credentials should succeed interception ' + + 'and response should be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-nocors-redirects-to-nocors-nocreds', + redirect_dest: 'no-cors', + url_credentials: false, + expected_type: 'opaque', + expected_redirected: false, + request_init: { + redirect: 'follow', + mode: 'no-cors' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' + + 'no-cors without credentials should succeed interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-nocors-redirects-to-cors-nocreds', + redirect_dest: 'cors', + url_credentials: false, + expected_type: 'opaque', + expected_redirected: false, + request_init: { + redirect: 'follow', + mode: 'no-cors' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' + + 'cors without credentials should succeed interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-cors-redirects-to-sameorigin-creds', + redirect_dest: 'same-origin', + url_credentials: true, + expected_type: 'basic', + expected_redirected: true, + request_init: { + redirect: 'follow', + mode: 'cors' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, cors mode Request redirected to ' + + 'same-origin with credentials should succeed interception ' + + 'and response should be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-cors-redirects-to-nocors-creds', + redirect_dest: 'no-cors', + url_credentials: true, + request_init: { + redirect: 'follow', + mode: 'cors' + }, + // should reject because CORS requests require CORS headers on cross-origin + // resources + should_reject: true + }); +}, 'Non-navigation, follow redirect, cors mode Request redirected to ' + + 'no-cors with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-cors-redirects-to-cors-creds', + redirect_dest: 'cors', + url_credentials: true, + request_init: { + redirect: 'follow', + mode: 'cors' + }, + // should reject because CORS requests do not allow user/pass entries in + // cross-origin URLs + // NOTE: https://github.com/whatwg/fetch/issues/112 + should_reject: true + }); +}, 'Non-navigation, follow redirect, cors mode Request redirected to ' + + 'cors with credentials should fail interception ' + + 'and response should be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-sameorigin-redirects-to-sameorigin-creds', + redirect_dest: 'same-origin', + url_credentials: true, + expected_type: 'basic', + expected_redirected: true, + request_init: { + redirect: 'follow', + mode: 'same-origin' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' + + 'same-origin with credentials should succeed interception ' + + 'and response should be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-sameorigin-redirects-to-nocors-creds', + redirect_dest: 'no-cors', + url_credentials: true, + request_init: { + redirect: 'follow', + mode: 'same-origin' + }, + // should reject because same-origin requests cannot load cross-origin + // resources + should_reject: true + }); +}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' + + 'no-cors with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-sameorigin-redirects-to-cors-creds', + redirect_dest: 'cors', + url_credentials: true, + request_init: { + redirect: 'follow', + mode: 'same-origin' + }, + // should reject because same-origin requests cannot load cross-origin + // resources + should_reject: true + }); +}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' + + 'cors with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-nocors-redirects-to-sameorigin-creds', + redirect_dest: 'same-origin', + url_credentials: true, + expected_type: 'basic', + expected_redirected: true, + request_init: { + redirect: 'follow', + mode: 'no-cors' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' + + 'same-origin with credentials should succeed interception ' + + 'and response should be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-nocors-redirects-to-nocors-creds', + redirect_dest: 'no-cors', + url_credentials: true, + expected_type: 'opaque', + expected_redirected: false, + request_init: { + redirect: 'follow', + mode: 'no-cors' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' + + 'no-cors with credentials should succeed interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-follow-nocors-redirects-to-cors-creds', + redirect_dest: 'cors', + url_credentials: true, + expected_type: 'opaque', + expected_redirected: false, + request_init: { + redirect: 'follow', + mode: 'no-cors' + }, + should_reject: false + }); +}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' + + 'cors with credentials should succeed interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-cors-redirects-to-sameorigin-nocreds', + redirect_dest: 'same-origin', + url_credentials: false, + request_init: { + redirect: 'error', + mode: 'cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, cors mode Request redirected to ' + + 'same-origin without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-cors-redirects-to-nocors-nocreds', + redirect_dest: 'no-cors', + url_credentials: false, + request_init: { + redirect: 'error', + mode: 'cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, cors mode Request redirected to ' + + 'no-cors without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-cors-redirects-to-cors-nocreds', + redirect_dest: 'cors', + url_credentials: false, + request_init: { + redirect: 'error', + mode: 'cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, cors mode Request redirected to ' + + 'cors without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-sameorigin-redirects-to-sameorigin-nocreds', + redirect_dest: 'same-origin', + url_credentials: false, + request_init: { + redirect: 'error', + mode: 'same-origin' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' + + 'same-origin without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-sameorigin-redirects-to-nocors-nocreds', + redirect_dest: 'no-cors', + url_credentials: false, + request_init: { + redirect: 'error', + mode: 'same-origin' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' + + 'no-cors without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-sameorigin-redirects-to-cors-nocreds', + redirect_dest: 'cors', + url_credentials: false, + request_init: { + redirect: 'error', + mode: 'same-origin' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' + + 'cors without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-nocors-redirects-to-sameorigin-nocreds', + redirect_dest: 'same-origin', + url_credentials: false, + request_init: { + redirect: 'error', + mode: 'no-cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' + + 'same-origin without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-nocors-redirects-to-nocors-nocreds', + redirect_dest: 'no-cors', + url_credentials: false, + request_init: { + redirect: 'error', + mode: 'no-cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' + + 'no-cors without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-nocors-redirects-to-cors-nocreds', + redirect_dest: 'cors', + url_credentials: false, + request_init: { + redirect: 'error', + mode: 'no-cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' + + 'cors without credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-cors-redirects-to-sameorigin-creds', + redirect_dest: 'same-origin', + url_credentials: true, + request_init: { + redirect: 'error', + mode: 'cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, cors mode Request redirected to ' + + 'same-origin with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-cors-redirects-to-nocors-creds', + redirect_dest: 'no-cors', + url_credentials: true, + request_init: { + redirect: 'error', + mode: 'cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, cors mode Request redirected to ' + + 'no-cors with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-cors-redirects-to-cors-creds', + redirect_dest: 'cors', + url_credentials: true, + request_init: { + redirect: 'error', + mode: 'cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, cors mode Request redirected to ' + + 'cors with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-sameorigin-redirects-to-sameorigin-creds', + redirect_dest: 'same-origin', + url_credentials: true, + request_init: { + redirect: 'error', + mode: 'same-origin' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' + + 'same-origin with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-sameorigin-redirects-to-nocors-creds', + redirect_dest: 'no-cors', + url_credentials: true, + request_init: { + redirect: 'error', + mode: 'same-origin' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' + + 'no-cors with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-sameorigin-redirects-to-cors-creds', + redirect_dest: 'cors', + url_credentials: true, + request_init: { + redirect: 'error', + mode: 'same-origin' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' + + 'cors with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-nocors-redirects-to-sameorigin-creds', + redirect_dest: 'same-origin', + url_credentials: true, + request_init: { + redirect: 'error', + mode: 'no-cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' + + 'same-origin with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-nocors-redirects-to-nocors-creds', + redirect_dest: 'no-cors', + url_credentials: true, + request_init: { + redirect: 'error', + mode: 'no-cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' + + 'no-cors with credentials should fail interception ' + + 'and response should not be redirected'); + +promise_test(function(t) { + return redirect_fetch_test(t, { + name: 'nonav-error-nocors-redirects-to-cors-creds', + redirect_dest: 'cors', + url_credentials: true, + request_init: { + redirect: 'error', + mode: 'no-cors' + }, + // should reject because requests with 'error' RequestRedirect cannot be + // redirected. + should_reject: true + }); +}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' + + 'cors with credentials should fail interception and response should not ' + + 'be redirected'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html new file mode 100644 index 0000000000..af4b20a9a4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html @@ -0,0 +1,274 @@ +<!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="resources/test-helpers.sub.js"></script> +<body> +<script> +var worker = 'resources/fetch-event-test-worker.js'; + +function do_test(referrer, value, expected, name) +{ + test(() => { + assert_equals(value, expected); + }, name + (referrer ? " - Custom Referrer" : " - Default Referrer")); +} + +function run_referrer_policy_tests(frame, referrer, href, origin) { + return frame.contentWindow.fetch('resources/simple.html?referrerFull', + {method: "GET", referrer: referrer}) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + href + '\n' + + 'ReferrerPolicy: strict-origin-when-cross-origin', + 'Service Worker should respond to fetch with the referrer URL when a member of RequestInit is present'); + var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, + {method: "GET", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: \n' + + 'ReferrerPolicy: strict-origin-when-cross-origin', + 'Service Worker should respond to fetch with no referrer when a member of RequestInit is present with an HTTP request'); + return frame.contentWindow.fetch('resources/simple.html?referrerFull', + {referrerPolicy: "", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + href + '\n' + + 'ReferrerPolicy: strict-origin-when-cross-origin', + 'Service Worker should respond to fetch with the referrer with ""'); + var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, + {referrerPolicy: "", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: \n' + + 'ReferrerPolicy: strict-origin-when-cross-origin', + 'Service Worker should respond to fetch with no referrer with ""'); + return frame.contentWindow.fetch('resources/simple.html?referrerFull', + {referrerPolicy: "origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + origin + '/' + '\n' + + 'ReferrerPolicy: origin', + 'Service Worker should respond to fetch with the referrer origin with "origin" and a same origin request'); + var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, + {referrerPolicy: "origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + origin + '/' + '\n' + + 'ReferrerPolicy: origin', + 'Service Worker should respond to fetch with the referrer origin with "origin" and a cross origin request'); + return frame.contentWindow.fetch('resources/simple.html?referrerFull', + {referrerPolicy: "origin-when-cross-origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + href + '\n' + + 'ReferrerPolicy: origin-when-cross-origin', + 'Service Worker should respond to fetch with the referrer URL with "origin-when-cross-origin" and a same origin request'); + var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, + {referrerPolicy: "origin-when-cross-origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + origin + '/' + '\n' + + 'ReferrerPolicy: origin-when-cross-origin', + 'Service Worker should respond to fetch with the referrer origin with "origin-when-cross-origin" and a cross origin request'); + return frame.contentWindow.fetch('resources/simple.html?referrerFull', + {referrerPolicy: "no-referrer-when-downgrade", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + href + '\n' + + 'ReferrerPolicy: no-referrer-when-downgrade', + 'Service Worker should respond to fetch with no referrer with "no-referrer-when-downgrade" and a same origin request'); + var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, + {referrerPolicy: "no-referrer-when-downgrade", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: \n' + + 'ReferrerPolicy: no-referrer-when-downgrade', + 'Service Worker should respond to fetch with no referrer with "no-referrer-when-downgrade" and an HTTP request'); + var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, {referrerPolicy: "unsafe-url", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + href + '\n' + + 'ReferrerPolicy: unsafe-url', + 'Service Worker should respond to fetch with no referrer with "unsafe-url"'); + return frame.contentWindow.fetch('resources/simple.html?referrerFull', + {referrerPolicy: "no-referrer", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: \n' + + 'ReferrerPolicy: no-referrer', + 'Service Worker should respond to fetch with no referrer URL with "no-referrer"'); + return frame.contentWindow.fetch('resources/simple.html?referrerFull', + {referrerPolicy: "same-origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + href + '\n' + + 'ReferrerPolicy: same-origin', + 'Service Worker should respond to fetch with referrer URL with "same-origin" and a same origin request'); + var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, + {referrerPolicy: "same-origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: \n' + + 'ReferrerPolicy: same-origin', + 'Service Worker should respond to fetch with no referrer with "same-origin" and cross origin request'); + var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, + {referrerPolicy: "strict-origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + origin + '/' + '\n' + + 'ReferrerPolicy: strict-origin', + 'Service Worker should respond to fetch with the referrer origin with "strict-origin" and a HTTPS cross origin request'); + return frame.contentWindow.fetch('resources/simple.html?referrerFull', + {referrerPolicy: "strict-origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + origin + '/' + '\n' + + 'ReferrerPolicy: strict-origin', + 'Service Worker should respond to fetch with the referrer origin with "strict-origin" and a same origin request'); + var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, + {referrerPolicy: "strict-origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: \n' + + 'ReferrerPolicy: strict-origin', + 'Service Worker should respond to fetch with no referrer with "strict-origin" and a HTTP request'); + return frame.contentWindow.fetch('resources/simple.html?referrerFull', + {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + href + '\n' + + 'ReferrerPolicy: strict-origin-when-cross-origin', + 'Service Worker should respond to fetch with the referrer URL with "strict-origin-when-cross-origin" and a same origin request'); + var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, + {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: ' + origin + '/' + '\n' + + 'ReferrerPolicy: strict-origin-when-cross-origin', + 'Service Worker should respond to fetch with the referrer origin with "strict-origin-when-cross-origin" and a HTTPS cross origin request'); + var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() + + '/resources/simple.html?referrerFull'; + return frame.contentWindow.fetch(http_url, + {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer}); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + do_test(referrer, + response_text, + 'Referrer: \n' + + 'ReferrerPolicy: strict-origin-when-cross-origin', + 'Service Worker should respond to fetch with no referrer with "strict-origin-when-cross-origin" and a HTTP request'); + }); +} + +promise_test(function(t) { + var scope = 'resources/simple.html?referrerPolicy'; + var frame; + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + frame = f; + test(() => { + assert_equals(frame.contentDocument.body.textContent, 'ReferrerPolicy: strict-origin-when-cross-origin'); + }, 'Service Worker should respond to fetch with the default referrer policy'); + // First, run the referrer policy tests without passing a referrer in RequestInit. + return run_referrer_policy_tests(frame, undefined, frame.contentDocument.location.href, + frame.contentDocument.location.origin); + }) + .then(function() { + // Now, run the referrer policy tests while passing a referrer in RequestInit. + var referrer = get_host_info()['HTTPS_ORIGIN'] + base_path() + 'resources/fake-referrer'; + return run_referrer_policy_tests(frame, 'fake-referrer', referrer, + frame.contentDocument.location.origin); + }) + .then(function() { + frame.remove(); + }); + }, 'Service Worker responds to fetch event with the referrer policy'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html new file mode 100644 index 0000000000..05e2210524 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<title>Service Worker: FetchEvent.respondWith() argument type test.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var resolve_test_done; + +var test_done_promise = new Promise(function(resolve) { + resolve_test_done = resolve; + }); + +// Called by the child frame. +function notify_test_done(result) { + resolve_test_done(result); +} + +promise_test(function(t) { + var scope = 'resources/fetch-event-respond-with-argument-iframe.html'; + var script = 'resources/fetch-event-respond-with-argument-worker.js'; + var frame; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(f) { + frame = f; + return test_done_promise; + }) + .then(function(result) { + frame.remove(); + assert_equals(result, 'PASS'); + }); + }, 'respondWith() takes either a Response or a promise that resolves ' + + 'with a Response. Other values should raise a network error.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html new file mode 100644 index 0000000000..932f9030c5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>respondWith with a response whose body is being loaded from the network by chunks</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +const WORKER = 'resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js'; +const SCOPE = 'resources/fetch-event-respond-with-body-loaded-in-chunk-iframe.html'; + +promise_test(async t => { + var reg = await service_worker_unregister_and_register(t, WORKER, SCOPE); + add_completion_callback(() => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + let iframe = await with_iframe(SCOPE); + t.add_cleanup(() => iframe.remove()); + + let response = await iframe.contentWindow.fetch('body-in-chunk'); + assert_equals(await response.text(), 'TEST_TRICKLE\nTEST_TRICKLE\nTEST_TRICKLE\nTEST_TRICKLE\n'); +}, 'Respond by chunks with a Response being loaded'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html new file mode 100644 index 0000000000..645a29c9b4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>respondWith with a new Response</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +const WORKER = + 'resources/fetch-event-respond-with-custom-response-worker.js'; +const SCOPE = + 'resources/blank.html'; + +// Register a service worker, then create an iframe at url. +function iframeTest(url, callback, name) { + return promise_test(async t => { + const reg = await service_worker_unregister_and_register(t, WORKER, SCOPE); + add_completion_callback(() => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + const iframe = await with_iframe(url); + const iwin = iframe.contentWindow; + t.add_cleanup(() => iframe.remove()); + await callback(t, iwin); + }, name); +} + +iframeTest(SCOPE, async (t, iwin) => { + const response = await iwin.fetch('?type=string'); + assert_equals(await response.text(), 'PASS'); +}, 'Subresource built from a string'); + +iframeTest(SCOPE, async (t, iwin) => { + const response = await iwin.fetch('?type=blob'); + assert_equals(await response.text(), 'PASS'); +}, 'Subresource built from a blob'); + +iframeTest(SCOPE, async (t, iwin) => { + const response = await iwin.fetch('?type=buffer'); + assert_equals(await response.text(), 'PASS'); +}, 'Subresource built from a buffer'); + +iframeTest(SCOPE, async (t, iwin) => { + const response = await iwin.fetch('?type=buffer-view'); + assert_equals(await response.text(), 'PASS'); +}, 'Subresource built from a buffer-view'); + +iframeTest(SCOPE, async (t, iwin) => { + const response = await iwin.fetch('?type=form-data'); + const data = await response.formData(); + assert_equals(data.get('result'), 'PASS'); +}, 'Subresource built from form-data'); + +iframeTest(SCOPE, async (t, iwin) => { + const response = await iwin.fetch('?type=search-params'); + assert_equals(await response.text(), 'result=PASS'); +}, 'Subresource built from search-params'); + +// As above, but navigations + +iframeTest(SCOPE + '?type=string', (t, iwin) => { + assert_equals(iwin.document.body.textContent, 'PASS'); +}, 'Navigation resource built from a string'); + +iframeTest(SCOPE + '?type=blob', (t, iwin) => { + assert_equals(iwin.document.body.textContent, 'PASS'); +}, 'Navigation resource built from a blob'); + +iframeTest(SCOPE + '?type=buffer', (t, iwin) => { + assert_equals(iwin.document.body.textContent, 'PASS'); +}, 'Navigation resource built from a buffer'); + +iframeTest(SCOPE + '?type=buffer-view', (t, iwin) => { + assert_equals(iwin.document.body.textContent, 'PASS'); +}, 'Navigation resource built from a buffer-view'); + +// Note: not testing form data for a navigation as the boundary header is lost. + +iframeTest(SCOPE + '?type=search-params', (t, iwin) => { + assert_equals(iwin.document.body.textContent, 'result=PASS'); +}, 'Navigation resource built from search-params'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html new file mode 100644 index 0000000000..505cef2972 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>respondWith streams data to an intercepted fetch()</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +const WORKER = + 'resources/fetch-event-respond-with-partial-stream-worker.js'; +const SCOPE = + 'resources/fetch-event-respond-with-partial-stream-iframe.html'; + +promise_test(async t => { + let reg = await service_worker_unregister_and_register(t, WORKER, SCOPE) + add_completion_callback(() => reg.unregister()); + + await wait_for_state(t, reg.installing, 'activated'); + + let frame = await with_iframe(SCOPE); + t.add_cleanup(_ => frame.remove()); + + let response = await frame.contentWindow.fetch('partial-stream.txt'); + + let reader = response.body.getReader(); + + let encoder = new TextEncoder(); + let decoder = new TextDecoder(); + + let expected = 'partial-stream-content'; + let encodedExpected = encoder.encode(expected); + let received = ''; + let encodedReceivedLength = 0; + + // Accumulate response data from the service worker. We do this as a loop + // to allow the browser the flexibility of rebuffering if it chooses. We + // do expect to get the partial data within the test timeout period, though. + // The spec is a bit vague at the moment about this, but it seems reasonable + // that the browser should not stall the response stream when the service + // worker has only written a partial result, but not closed the stream. + while (encodedReceivedLength < encodedExpected.length) { + let chunk = await reader.read(); + assert_false(chunk.done, 'partial body stream should not be closed yet'); + + encodedReceivedLength += chunk.value.length; + received += decoder.decode(chunk.value); + } + + // Note, the spec may allow some re-buffering between the service worker + // and the outer intercepted fetch. We could relax this exact chunk value + // match if necessary. The goal, though, is to ensure the outer fetch is + // not completely blocked until the service worker body is closed. + assert_equals(received, expected, + 'should receive partial content through service worker interception'); + + reg.active.postMessage('done'); + + await reader.closed; + + }, 'respondWith() streams data to an intercepted fetch()'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html new file mode 100644 index 0000000000..4544a9e08f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>respondWith with a response built from a ReadableStream</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +const WORKER = 'resources/fetch-event-respond-with-readable-stream-chunk-worker.js'; +const SCOPE = 'resources/fetch-event-respond-with-readable-stream-chunk-iframe.html'; + +promise_test(async t => { + var reg = await service_worker_unregister_and_register(t, WORKER, SCOPE); + add_completion_callback(() => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + let iframe = await with_iframe(SCOPE); + t.add_cleanup(() => iframe.remove()); + + let response = await iframe.contentWindow.fetch('body-stream'); + assert_equals(await response.text(), 'chunk #1 chunk #2 chunk #3 chunk #4'); +}, 'Respond by chunks with a Response built from a ReadableStream'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html new file mode 100644 index 0000000000..4651258e6a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>respondWith with a response built from a ReadableStream</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/utils.js"></script> +<script> +'use strict'; + +const WORKER = + 'resources/fetch-event-respond-with-readable-stream-worker.js'; +const SCOPE = + 'resources/blank.html'; + +// Register a service worker, then create an iframe at url. +function iframeTest(url, callback, name) { + return promise_test(async t => { + const reg = await service_worker_unregister_and_register(t, WORKER, SCOPE); + add_completion_callback(() => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + const iframe = await with_iframe(url); + const iwin = iframe.contentWindow; + t.add_cleanup(() => iframe.remove()); + await callback(t, iwin); + }, name); +} + +iframeTest(SCOPE, async (t, iwin) => { + const response = await iwin.fetch('?stream'); + assert_equals(await response.text(), 'PASS'); +}, 'Subresource built from a ReadableStream'); + +iframeTest(SCOPE + '?stream', (t, iwin) => { + assert_equals(iwin.document.body.textContent, 'PASS'); +}, 'Main resource built from a ReadableStream'); + +iframeTest(SCOPE, async (t, iwin) => { + const response = await iwin.fetch('?stream&delay'); + assert_equals(await response.text(), 'PASS'); +}, 'Subresource built from a ReadableStream - delayed'); + +iframeTest(SCOPE + '?stream&delay', (t, iwin) => { + assert_equals(iwin.document.body.textContent, 'PASS'); +}, 'Main resource built from a ReadableStream - delayed'); + +iframeTest(SCOPE, async (t, iwin) => { + const response = await iwin.fetch('?stream&use-fetch-stream'); + assert_equals(await response.text(), 'PASS\n'); +}, 'Subresource built from a ReadableStream - fetch stream'); + +iframeTest(SCOPE + '?stream&use-fetch-stream', (t, iwin) => { + assert_equals(iwin.document.body.textContent, 'PASS\n'); +}, 'Main resource built from a ReadableStream - fetch stream'); + +for (const enqueue of [true, false]) { + const withStream = enqueue ? 'with nonempty stream' : 'with empty stream'; + iframeTest(SCOPE, async (t, iwin) => { + const id = token(); + let response = await iwin.fetch(`?stream&observe-cancel&id=${id}&enqueue=${enqueue}`); + response.body.cancel(); + + // Wait for a while to avoid a race between the cancel handling and the + // second fetch request. + await new Promise(r => step_timeout(r, 10)); + + response = await iwin.fetch(`?stream&query-cancel&id=${id}`); + assert_equals(await response.text(), 'cancelled'); + }, `Cancellation in the page should be observable in the service worker ${withStream}`); + + iframeTest(SCOPE, async (t, iwin) => { + const id = token(); + const controller = new AbortController(); + let response = await iwin.fetch(`?stream&observe-cancel&id=${id}&enqueue=${enqueue}`, { + signal: controller.signal + }); + controller.abort(); + + // Wait for a while to avoid a race between the cancel handling and the + // second fetch request. + await new Promise(r => step_timeout(r, 10)); + + response = await iwin.fetch(`?stream&query-cancel&id=${id}`); + assert_equals(await response.text(), 'cancelled'); + }, `Abort in the page should be observable in the service worker ${withStream}`); +} + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html new file mode 100644 index 0000000000..2a44811461 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>respondWith with response body having invalid chunks</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +const WORKER = + 'resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js'; +const SCOPE = + 'resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html'; + +// Called by the iframe when it has the reader promise we should watch. +var set_reader_promise; +let reader_promise = new Promise(resolve => set_reader_promise = resolve); + +var set_fetch_promise; +let fetch_promise = new Promise(resolve => set_fetch_promise = resolve); + +// This test creates an controlled iframe that makes a fetch request. The +// service worker returns a response with a body stream containing an invalid +// chunk. +promise_test(async t => { + // Start off the process. + let errorConstructor; + await service_worker_unregister_and_register(t, WORKER, SCOPE) + .then(reg => { + add_completion_callback(() => reg.unregister()); + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(() => with_iframe(SCOPE)) + .then(frame => { + t.add_cleanup(() => frame.remove()) + errorConstructor = frame.contentWindow.TypeError; + }); + + await promise_rejects_js(t, errorConstructor, reader_promise, + "read() should be rejected"); + // Fetch should complete properly, because the reader error is caught in + // the subframe. That is, there should be no errors _other_ than the + // reader! + return fetch_promise; + }, 'Response with a ReadableStream having non-Uint8Array chunks should be transferred as errored'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html new file mode 100644 index 0000000000..31fd616b6d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(function(t) { + var script = + 'resources/fetch-event-respond-with-stops-propagation-worker.js'; + var scope = 'resources/simple.html'; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(frame) { + t.add_cleanup(function() { frame.remove(); }); + var channel = new MessageChannel(); + var saw_message = new Promise(function(resolve) { + channel.port1.onmessage = function(e) { resolve(e.data); } + }); + var worker = frame.contentWindow.navigator.serviceWorker.controller; + + worker.postMessage({port: channel.port2}, [channel.port2]); + return saw_message; + }) + .then(function(message) { + assert_equals(message, 'PASS'); + }) + }, 'respondWith() invokes stopImmediatePropagation()'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html new file mode 100644 index 0000000000..d98fb22ff4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title></title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +promise_test(function(t) { + var scope = 'resources/fetch-event-throws-after-respond-with-iframe.html'; + var workerscript = 'resources/respond-then-throw-worker.js'; + var iframe; + return service_worker_unregister_and_register(t, workerscript, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated') + .then(() => reg.active); + }) + .then(function(worker) { + var channel = new MessageChannel(); + channel.port1.onmessage = function(e) { + assert_equals(e.data, 'SYNC', ' Should receive sync message.'); + channel.port1.postMessage('ACK'); + } + worker.postMessage({port: channel.port2}, [channel.port2]); + // The iframe will only be loaded after the sync is completed. + return with_iframe(scope); + }) + .then(function(frame) { + assert_true(frame.contentDocument.body.innerHTML.includes("intercepted")); + }) + }, 'Fetch event handler throws after a successful respondWith()'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html new file mode 100644 index 0000000000..15a2e95bd3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html @@ -0,0 +1,122 @@ +<!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="resources/test-helpers.sub.js"></script> +<body> +<script> +const worker = 'resources/fetch-event-within-sw-worker.js'; + +function wait(ms) { + return new Promise(r => setTimeout(r, ms)); +} + +function reset() { + for (const iframe of [...document.querySelectorAll('.test-iframe')]) { + iframe.remove(); + } + return navigator.serviceWorker.getRegistrations().then(registrations => { + return Promise.all(registrations.map(r => r.unregister())); + }).then(() => caches.keys()).then(cacheKeys => { + return Promise.all(cacheKeys.map(c => caches.delete(c))); + }); +} + +add_completion_callback(reset); + +function regReady(reg) { + return new Promise((resolve, reject) => { + if (reg.active) { + resolve(); + return; + } + const nextWorker = reg.waiting || reg.installing; + + nextWorker.addEventListener('statechange', () => { + if (nextWorker.state == 'redundant') { + reject(Error(`Service worker failed to install`)); + return; + } + if (nextWorker.state == 'activated') { + resolve(); + } + }); + }); +} + +function getCookies() { + return new Map( + document.cookie + .split(/;/g) + .map(c => c.trim().split('=').map(s => s.trim())) + ); +} + +function registerSwAndOpenFrame() { + return reset().then(() => navigator.serviceWorker.register(worker, {scope: 'resources/'})) + .then(reg => regReady(reg)) + .then(() => with_iframe('resources/simple.html')); +} + +function raceBroadcastAndCookie(channel, cookie) { + const initialCookie = getCookies().get(cookie); + let done = false; + + return Promise.race([ + new Promise(resolve => { + const bc = new BroadcastChannel(channel); + bc.onmessage = () => { + bc.close(); + resolve('broadcast'); + }; + }), + (function checkCookie() { + // Stop polling if the broadcast channel won + if (done == true) return; + if (getCookies().get(cookie) != initialCookie) return 'cookie'; + + return wait(200).then(checkCookie); + }()) + ]).then(val => { + done = true; + return val; + }); +} + +promise_test(() => { + return Notification.requestPermission().then(permission => { + if (permission != "granted") { + throw Error('You must allow notifications for this origin before running this test.'); + } + return registerSwAndOpenFrame(); + }).then(iframe => { + return Promise.resolve().then(() => { + // In this test, the service worker will ping the 'icon-request' channel + // if it intercepts a request for 'notification_icon.py'. If the request + // reaches the server it sets the 'notification' cookie to the value given + // in the URL. "raceBroadcastAndCookie" monitors both and returns which + // happens first. + const race = raceBroadcastAndCookie('icon-request', 'notification'); + const notification = new iframe.contentWindow.Notification('test', { + icon: `notification_icon.py?set-cookie-notification=${Math.random()}` + }); + notification.close(); + + return race.then(winner => { + assert_equals(winner, 'broadcast', 'The service worker intercepted the from-window notification icon request'); + }); + }).then(() => { + // Similar race to above, but this time the service worker requests the + // notification. + const race = raceBroadcastAndCookie('icon-request', 'notification'); + iframe.contentWindow.fetch(`show-notification?set-cookie-notification=${Math.random()}`); + + return race.then(winner => { + assert_equals(winner, 'broadcast', 'The service worker intercepted the from-service-worker notification icon request'); + }); + }) + }); +}, `Notification requests intercepted both from window and SW`); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw.https.html new file mode 100644 index 0000000000..0b52b18305 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw.https.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> + +<script> +const worker = 'resources/fetch-event-within-sw-worker.js'; + +async function registerSwAndOpenFrame(t) { + const registration = await navigator.serviceWorker.register( + worker, { scope: 'resources/' }); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + const frame = await with_iframe('resources/simple.html'); + t.add_cleanup(() => frame.remove()); + return frame; +} + +async function deleteCaches() { + const cacheKeys = await caches.keys(); + await Promise.all(cacheKeys.map(c => caches.delete(c))); +} + +promise_test(async t => { + t.add_cleanup(deleteCaches); + + const iframe = await registerSwAndOpenFrame(t); + const fetchText = + await iframe.contentWindow.fetch('sample.txt').then(r => r.text()); + + const cache = await iframe.contentWindow.caches.open('test'); + await cache.add('sample.txt'); + + const response = await cache.match('sample.txt'); + const cacheText = await (response ? response.text() : 'cache match failed'); + assert_equals(fetchText, 'intercepted', 'fetch intercepted'); + assert_equals(cacheText, 'intercepted', 'cache.add intercepted'); +}, 'Service worker intercepts requests from window'); + +promise_test(async t => { + const iframe = await registerSwAndOpenFrame(t); + const [fetchText, cacheText] = await Promise.all([ + iframe.contentWindow.fetch('sample.txt-inner-fetch').then(r => r.text()), + iframe.contentWindow.fetch('sample.txt-inner-cache').then(r => r.text()) + ]); + assert_equals(fetchText, 'Hello world\n', 'fetch within SW not intercepted'); + assert_equals(cacheText, 'Hello world\n', + 'cache.add within SW not intercepted'); +}, 'Service worker does not intercept fetch/cache requests within service ' + + 'worker'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.h2.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.h2.html new file mode 100644 index 0000000000..5cd381ec98 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.h2.html @@ -0,0 +1,112 @@ +<!DOCTYPE html> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +const worker = 'resources/fetch-event-test-worker.js'; + +const method = 'POST'; +const duplex = 'half'; + +function createBody(t) { + const rs = new ReadableStream({start(c) { + c.enqueue('i a'); + c.enqueue('m the request'); + step_timeout(t.step_func(() => { + c.enqueue(' body'); + c.close(); + }, 10)); + }}); + return rs.pipeThrough(new TextEncoderStream()); +} + +promise_test(async t => { + const scope = 'resources/'; + const registration = + await service_worker_unregister_and_register(t, worker, scope); + await wait_for_state(t, registration.installing, 'activated'); + + // This will happen after all other tests + promise_test(t => { + return registration.unregister(); + }, 'restore global state'); +}, 'global setup'); + +// Test that the service worker can read FetchEvent#body when it is made from +// a ReadableStream. It responds with request body it read. +promise_test(async t => { + const body = createBody(t); + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = `resources/simple.html?ignore&id=${token()}`; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + const response = await frame.contentWindow.fetch('simple.html?request-body', { + method, body, duplex}); + assert_equals(response.status, 200, 'status'); + const text = await response.text(); + assert_equals(text, 'i am the request body', 'body'); +}, 'The streaming request body is readable in the service worker.'); + +// Network fallback +promise_test(async t => { + const body = createBody(t); + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = `resources/simple.html?ignore&id=${token()}`; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + // Add "?ignore" so that the service worker falls back to + // echo-content.h2.py. + const echo_url = '/fetch/api/resources/echo-content.h2.py?ignore'; + const response = + await frame.contentWindow.fetch(echo_url, { method, body, duplex}); + assert_equals(response.status, 200, 'status'); + const text = await response.text(); + assert_equals(text, 'i am the request body', 'body'); +}, 'Network fallback for streaming upload.'); + +// When the streaming body is used in the service worker, network fallback +// fails. +promise_test(async t => { + const body = createBody(t); + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = `resources/simple.html?ignore&id=${token()}`; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + const echo_url = '/fetch/api/resources/echo-content.h2.py?use-and-ignore'; + const w = frame.contentWindow; + await promise_rejects_js(t, w.TypeError, w.fetch(echo_url, { + method, body, duplex})); +}, 'When the streaming request body is used, network fallback fails.'); + +// When the streaming body is used by clone() in the service worker, network +// fallback succeeds. +promise_test(async t => { + const body = createBody(t); + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = `resources/simple.html?ignore&id=${token()}`; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + // Add "?clone-and-ignore" so that the service worker falls back to + // echo-content.h2.py. + const echo_url = '/fetch/api/resources/echo-content.h2.py?clone-and-ignore'; + const response = await frame.contentWindow.fetch(echo_url, { + method, body, duplex}); + assert_equals(response.status, 200, 'status'); + const text = await response.text(); + assert_equals(text, 'i am the request body', 'body'); +}, 'Running clone() in the service worker does not prevent network fallback.'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.html new file mode 100644 index 0000000000..ce53f3c9bf --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.html @@ -0,0 +1,1000 @@ +<!DOCTYPE html> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +var worker = 'resources/fetch-event-test-worker.js'; +function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); +} + +promise_test(async t => { + const scope = 'resources/'; + const registration = + await service_worker_unregister_and_register(t, worker, scope); + await wait_for_state(t, registration.installing, 'activated'); + + // This will happen after all other tests + promise_test(t => { + return registration.unregister(); + }, 'restore global state'); + }, 'global setup'); + +promise_test(t => { + const page_url = 'resources/simple.html?headers'; + return with_iframe(page_url) + .then(function(frame) { + t.add_cleanup(() => { frame.remove(); }); + const headers = JSON.parse(frame.contentDocument.body.textContent); + const header_names = {}; + for (const [name, value] of headers) { + header_names[name] = true; + } + + assert_true( + header_names.hasOwnProperty('accept'), + 'request includes "Accept" header as inserted by Fetch' + ); + }); + }, 'Service Worker headers in the request of a fetch event'); + +promise_test(t => { + const page_url = 'resources/simple.html?string'; + return with_iframe(page_url) + .then(function(frame) { + t.add_cleanup(() => { frame.remove(); }); + assert_equals( + frame.contentDocument.body.textContent, + 'Test string', + 'Service Worker should respond to fetch with a test string'); + assert_equals( + frame.contentDocument.contentType, + 'text/plain', + 'The content type of the response created with a string should be text/plain'); + assert_equals( + frame.contentDocument.characterSet, + 'UTF-8', + 'The character set of the response created with a string should be UTF-8'); + }); + }, 'Service Worker responds to fetch event with string'); + +promise_test(t => { + const page_url = 'resources/simple.html?string'; + var frame; + return with_iframe(page_url) + .then(function(f) { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + return frame.contentWindow.fetch(page_url + "#foo") + }) + .then(function(response) { return response.text() }) + .then(function(text) { + assert_equals( + text, + 'Test string', + 'Service Worker should respond to fetch with a test string'); + }); + }, 'Service Worker responds to fetch event using request fragment with string'); + +promise_test(t => { + const page_url = 'resources/simple.html?blob'; + return with_iframe(page_url) + .then(frame => { + t.add_cleanup(() => { frame.remove(); }); + assert_equals( + frame.contentDocument.body.textContent, + 'Test blob', + 'Service Worker should respond to fetch with a test string'); + }); + }, 'Service Worker responds to fetch event with blob body'); + +promise_test(t => { + const page_url = 'resources/simple.html?referrer'; + return with_iframe(page_url) + .then(frame => { + t.add_cleanup(() => { frame.remove(); }); + assert_equals( + frame.contentDocument.body.textContent, + 'Referrer: ' + document.location.href, + 'Service Worker should respond to fetch with the referrer URL'); + }); + }, 'Service Worker responds to fetch event with the referrer URL'); + +promise_test(t => { + const page_url = 'resources/simple.html?clientId'; + var frame; + return with_iframe(page_url) + .then(function(f) { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + assert_equals( + frame.contentDocument.body.textContent, + 'Client ID Not Found', + 'Service Worker should respond to fetch with a client id'); + return frame.contentWindow.fetch('resources/other.html?clientId'); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + assert_equals( + response_text.substr(0, 15), + 'Client ID Found', + 'Service Worker should respond to fetch with an existing client id'); + }); + }, 'Service Worker responds to fetch event with an existing client id'); + +promise_test(t => { + const page_url = 'resources/simple.html?resultingClientId'; + const expected_found = 'Resulting Client ID Found'; + const expected_not_found = 'Resulting Client ID Not Found'; + return with_iframe(page_url) + .then(function(frame) { + t.add_cleanup(() => { frame.remove(); }); + assert_equals( + frame.contentDocument.body.textContent.substr(0, expected_found.length), + expected_found, + 'Service Worker should respond with an existing resulting client id for non-subresource requests'); + return frame.contentWindow.fetch('resources/other.html?resultingClientId'); + }) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + assert_equals( + response_text.substr(0), + expected_not_found, + 'Service Worker should respond with an empty resulting client id for subresource requests'); + }); + }, 'Service Worker responds to fetch event with the correct resulting client id'); + +promise_test(t => { + const page_url = 'resources/simple.html?ignore'; + return with_iframe(page_url) + .then(function(frame) { + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'Here\'s a simple html file.\n', + 'Response should come from fallback to native fetch'); + }); + }, 'Service Worker does not respond to fetch event'); + +promise_test(t => { + const page_url = 'resources/simple.html?null'; + return with_iframe(page_url) + .then(function(frame) { + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + '', + 'Response should be the empty string'); + }); + }, 'Service Worker responds to fetch event with null response body'); + +promise_test(t => { + const page_url = 'resources/simple.html?fetch'; + return with_iframe(page_url) + .then(function(frame) { + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'Here\'s an other html file.\n', + 'Response should come from fetched other file'); + }); + }, 'Service Worker fetches other file in fetch event'); + +// Creates a form and an iframe and does a form submission that navigates the +// frame to |action_url|. Returns the frame after navigation. +function submit_form(action_url) { + return new Promise(resolve => { + const frame = document.createElement('iframe'); + frame.name = 'post-frame'; + document.body.appendChild(frame); + const form = document.createElement('form'); + form.target = frame.name; + form.action = action_url; + form.method = 'post'; + const input1 = document.createElement('input'); + input1.type = 'text'; + input1.value = 'testValue1'; + input1.name = 'testName1' + form.appendChild(input1); + const input2 = document.createElement('input'); + input2.type = 'text'; + input2.value = 'testValue2'; + input2.name = 'testName2' + form.appendChild(input2); + document.body.appendChild(form); + frame.onload = function() { + form.remove(); + resolve(frame); + }; + form.submit(); + }); +} + +promise_test(t => { + const page_url = 'resources/simple.html?form-post'; + return submit_form(page_url) + .then(frame => { + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'POST:application/x-www-form-urlencoded:' + + 'testName1=testValue1&testName2=testValue2'); + }); + }, 'Service Worker responds to fetch event with POST form'); + +promise_test(t => { + // Add '?ignore' so the service worker falls back to network. + const page_url = 'resources/echo-content.py?ignore'; + return submit_form(page_url) + .then(frame => { + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'testName1=testValue1&testName2=testValue2'); + }); + }, 'Service Worker falls back to network in fetch event with POST form'); + +promise_test(t => { + const page_url = 'resources/simple.html?multiple-respond-with'; + return with_iframe(page_url) + .then(frame => { + t.add_cleanup(() => { frame.remove(); }); + assert_equals( + frame.contentDocument.body.textContent, + '(0)(1)[InvalidStateError](2)[InvalidStateError]', + 'Multiple calls of respondWith must throw InvalidStateErrors.'); + }); + }, 'Multiple calls of respondWith must throw InvalidStateErrors'); + +promise_test(t => { + const page_url = 'resources/simple.html?used-check'; + var first_frame; + return with_iframe(page_url) + .then(function(frame) { + assert_equals(frame.contentDocument.body.textContent, + 'Here\'s an other html file.\n', + 'Response should come from fetched other file'); + first_frame = frame; + t.add_cleanup(() => { first_frame.remove(); }); + return with_iframe(page_url); + }) + .then(function(frame) { + t.add_cleanup(() => { frame.remove(); }); + // When we access to the page_url in the second time, the content of the + // response is generated inside the ServiceWorker. The body contains + // the value of bodyUsed of the first response which is already + // consumed by FetchEvent.respondWith method. + assert_equals( + frame.contentDocument.body.textContent, + 'bodyUsed: true', + 'event.respondWith must set the used flag.'); + }); + }, 'Service Worker event.respondWith must set the used flag'); + +promise_test(t => { + const page_url = 'resources/simple.html?fragment-check'; + var fragment = '#/some/fragment'; + var first_frame; + return with_iframe(page_url + fragment) + .then(function(frame) { + t.add_cleanup(() => { frame.remove(); }); + assert_equals( + frame.contentDocument.body.textContent, + 'Fragment Found :' + fragment, + 'Service worker should expose URL fragments in request.'); + }); + }, 'Service Worker should expose FetchEvent URL fragments.'); + +promise_test(t => { + const page_url = 'resources/simple.html?cache'; + var frame; + var cacheTypes = [ + undefined, 'default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached' + ]; + return with_iframe(page_url) + .then(function(f) { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentWindow.document.body.textContent, 'default'); + var tests = cacheTypes.map(function(type) { + return new Promise(function(resolve, reject) { + var init = {cache: type}; + if (type === 'only-if-cached') { + // For privacy reasons, for the time being, only-if-cached + // requires the mode to be same-origin. + init.mode = 'same-origin'; + } + return frame.contentWindow.fetch(page_url + '=' + type, init) + .then(function(response) { return response.text(); }) + .then(function(response_text) { + var expected = (type === undefined) ? 'default' : type; + assert_equals(response_text, expected, + 'Service Worker should respond to fetch with the correct type'); + }) + .then(resolve) + .catch(reject); + }); + }); + return Promise.all(tests); + }) + .then(function() { + return new Promise(function(resolve, reject) { + frame.addEventListener('load', function onLoad() { + frame.removeEventListener('load', onLoad); + try { + assert_equals(frame.contentWindow.document.body.textContent, + 'no-cache'); + resolve(); + } catch (e) { + reject(e); + } + }); + frame.contentWindow.location.reload(); + }); + }); + }, 'Service Worker responds to fetch event with the correct cache types'); + +promise_test(t => { + const page_url = 'resources/simple.html?eventsource'; + var frame; + + function test_eventsource(opts) { + return new Promise(function(resolve, reject) { + var eventSource = new frame.contentWindow.EventSource(page_url, opts); + eventSource.addEventListener('message', function(msg) { + eventSource.close(); + try { + var data = JSON.parse(msg.data); + assert_equals(data.mode, 'cors', + 'EventSource should make CORS requests.'); + assert_equals(data.cache, 'no-store', + 'EventSource should bypass the http cache.'); + var expectedCredentials = opts.withCredentials ? 'include' + : 'same-origin'; + assert_equals(data.credentials, expectedCredentials, + 'EventSource should pass correct credentials mode.'); + resolve(); + } catch (e) { + reject(e); + } + }); + eventSource.addEventListener('error', function(e) { + eventSource.close(); + reject('The EventSource fired an error event.'); + }); + }); + } + + return with_iframe(page_url) + .then(function(f) { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + return test_eventsource({ withCredentials: false }); + }) + .then(function() { + return test_eventsource({ withCredentials: true }); + }); + }, 'Service Worker should intercept EventSource'); + +promise_test(t => { + const page_url = 'resources/simple.html?integrity'; + var frame; + var integrity_metadata = 'gs0nqru8KbsrIt5YToQqS9fYao4GQJXtcId610g7cCU='; + + return with_iframe(page_url) + .then(function(f) { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + // A request has associated integrity metadata (a string). + // Unless stated otherwise, it is the empty string. + assert_equals( + frame.contentDocument.body.textContent, ''); + + return frame.contentWindow.fetch(page_url, {'integrity': integrity_metadata}); + }) + .then(response => { + return response.text(); + }) + .then(response_text => { + assert_equals(response_text, integrity_metadata, 'integrity'); + }); + }, 'Service Worker responds to fetch event with the correct integrity_metadata'); + +// Test that the service worker can read FetchEvent#body when it is a string. +// It responds with request body it read. +promise_test(t => { + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = 'resources/simple.html?ignore-for-request-body-string'; + let frame; + + return with_iframe(page_url) + .then(f => { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + return frame.contentWindow.fetch('simple.html?request-body', { + method: 'POST', + body: 'i am the request body' + }); + }) + .then(response => { + return response.text(); + }) + .then(response_text => { + assert_equals(response_text, 'i am the request body'); + }); + }, 'FetchEvent#body is a string'); + +// Test that the service worker can read FetchEvent#body when it is made from +// a ReadableStream. It responds with request body it read. +promise_test(async t => { + const rs = new ReadableStream({start(c) { + c.enqueue('i a'); + c.enqueue('m the request'); + step_timeout(t.step_func(() => { + c.enqueue(' body'); + c.close(); + }, 10)); + }}); + + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = `resources/simple.html?ignore&id=${token()}`; + + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + const res = await frame.contentWindow.fetch('simple.html?request-body', { + method: 'POST', + body: rs.pipeThrough(new TextEncoderStream()), + duplex: 'half', + }); + assert_equals(await res.text(), 'i am the request body'); + }, 'FetchEvent#body is a ReadableStream'); + +// Test that the request body is sent to network upon network fallback, +// for a string body. +promise_test(t => { + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = 'resources/?ignore-for-request-body-fallback-string'; + let frame; + + return with_iframe(page_url) + .then(f => { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + // Add "?ignore" so the service worker falls back to echo-content.py. + const echo_url = '/fetch/api/resources/echo-content.py?ignore'; + return frame.contentWindow.fetch(echo_url, { + method: 'POST', + body: 'i am the request body' + }); + }) + .then(response => { + return response.text(); + }) + .then(response_text => { + assert_equals( + response_text, + 'i am the request body', + 'the network fallback request should include the request body'); + }); + }, 'FetchEvent#body is a string and is passed to network fallback'); + +// Test that the request body is sent to network upon network fallback, +// for a ReadableStream body. +promise_test(async t => { + const rs = new ReadableStream({start(c) { + c.enqueue('i a'); + c.enqueue('m the request'); + t.step_timeout(t.step_func(() => { + c.enqueue(' body'); + c.close(); + }, 10)); + }}); + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = 'resources/?ignore-for-request-body-fallback-string'; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + // Add "?ignore" so the service worker falls back to echo-content.py. + const echo_url = '/fetch/api/resources/echo-content.py?ignore'; + const w = frame.contentWindow; + await promise_rejects_js(t, w.TypeError, w.fetch(echo_url, { + method: 'POST', + body: rs + })); + }, 'FetchEvent#body is a none Uint8Array ReadableStream and is passed to a service worker'); + +// Test that the request body is sent to network upon network fallback even when +// the request body is used in the service worker, for a string body. +promise_test(async t => { + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = 'resources/?ignore-for-request-body-fallback-string'; + + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + // Add "?use-and-ignore" so the service worker falls back to echo-content.py. + const echo_url = '/fetch/api/resources/echo-content.py?use-and-ignore'; + const response = await frame.contentWindow.fetch(echo_url, { + method: 'POST', + body: 'i am the request body' + }); + const text = await response.text(); + assert_equals( + text, + 'i am the request body', + 'the network fallback request should include the request body'); + }, 'FetchEvent#body is a string, used and passed to network fallback'); + +// Test that the request body is sent to network upon network fallback even when +// the request body is used by clone() in the service worker, for a string body. +promise_test(async t => { + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = 'resources/?ignore-for-request-body-fallback-string'; + + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + // Add "?clone-and-ignore" so the service worker falls back to + // echo-content.py. + const echo_url = '/fetch/api/resources/echo-content.py?clone-and-ignore'; + const response = await frame.contentWindow.fetch(echo_url, { + method: 'POST', + body: 'i am the request body' + }); + const text = await response.text(); + assert_equals( + text, + 'i am the request body', + 'the network fallback request should include the request body'); + }, 'FetchEvent#body is a string, cloned and passed to network fallback'); + +// Test that the service worker can read FetchEvent#body when it is a blob. +// It responds with request body it read. +promise_test(t => { + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = 'resources/simple.html?ignore-for-request-body-blob'; + let frame; + + return with_iframe(page_url) + .then(f => { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + const blob = new Blob(['it\'s me the blob', ' ', 'and more blob!']); + return frame.contentWindow.fetch('simple.html?request-body', { + method: 'POST', + body: blob + }); + }) + .then(response => { + return response.text(); + }) + .then(response_text => { + assert_equals(response_text, 'it\'s me the blob and more blob!'); + }); + }, 'FetchEvent#body is a blob'); + +// Test that the request body is sent to network upon network fallback, +// for a blob body. +promise_test(t => { + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = 'resources/simple.html?ignore-for-request-body-fallback-blob'; + let frame; + + return with_iframe(page_url) + .then(f => { + frame = f; + t.add_cleanup(() => { frame.remove(); }); + const blob = new Blob(['it\'s me the blob', ' ', 'and more blob!']); + // Add "?ignore" so the service worker falls back to echo-content.py. + const echo_url = '/fetch/api/resources/echo-content.py?ignore'; + return frame.contentWindow.fetch(echo_url, { + method: 'POST', + body: blob + }); + }) + .then(response => { + return response.text(); + }) + .then(response_text => { + assert_equals( + response_text, + 'it\'s me the blob and more blob!', + 'the network fallback request should include the request body'); + }); + }, 'FetchEvent#body is a blob and is passed to network fallback'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?keepalive'; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, 'false'); + const response = await frame.contentWindow.fetch(page_url, {keepalive: true}); + const text = await response.text(); + assert_equals(text, 'true'); + }, 'Service Worker responds to fetch event with the correct keepalive value'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isReloadNavigation'; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isReloadNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.location.reload(); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isReloadNavigation = true'); + }, 'FetchEvent#request.isReloadNavigation is true (location.reload())'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isReloadNavigation'; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isReloadNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(0); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isReloadNavigation = true'); + }, 'FetchEvent#request.isReloadNavigation is true (history.go(0))'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isReloadNavigation'; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isReloadNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + const form = frame.contentDocument.createElement('form'); + form.method = 'POST'; + form.name = 'form'; + form.action = new Request(page_url).url; + frame.contentDocument.body.appendChild(form); + form.submit(); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = POST, isReloadNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.location.reload(); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = POST, isReloadNavigation = true'); + }, 'FetchEvent#request.isReloadNavigation is true (POST + location.reload())'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isReloadNavigation'; + const anotherUrl = new Request('resources/simple.html').url; + let frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isReloadNavigation = false'); + // Use step_timeout(0) to ensure the history entry is created for Blink + // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861. + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.src = anotherUrl; + }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(-1); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isReloadNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(0); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isReloadNavigation = true'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(1); + }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + }, 'FetchEvent#request.isReloadNavigation is true (with history traversal)'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isHistoryNavigation'; + const anotherUrl = new Request('resources/simple.html?ignore').url; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = false'); + // Use step_timeout(0) to ensure the history entry is created for Blink + // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861. + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.src = anotherUrl; + }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(-1); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = true'); + }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(-1))'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isHistoryNavigation'; + const anotherUrl = new Request('resources/simple.html?ignore').url; + const frame = await with_iframe(anotherUrl); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + // Use step_timeout(0) to ensure the history entry is created for Blink + // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861. + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.src = page_url; + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(-1); + }); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(1); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = true'); + }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(1))'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isHistoryNavigation'; + const anotherUrl = new Request('resources/simple.html?ignore').url; + const frame = await with_iframe(anotherUrl); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + // Use step_timeout(0) to ensure the history entry is created for Blink + // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861. + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.src = page_url; + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(-1); + }); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(1); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = true'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(0); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = false'); + }, 'FetchEvent#request.isHistoryNavigation is false (with history.go(0))'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isHistoryNavigation'; + const anotherUrl = new Request('resources/simple.html?ignore').url; + const frame = await with_iframe(anotherUrl); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + // Use step_timeout(0) to ensure the history entry is created for Blink + // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861. + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.src = page_url; + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(-1); + }); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(1); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = true'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.location.reload(); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = false'); + }, 'FetchEvent#request.isHistoryNavigation is false (with location.reload)'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isHistoryNavigation'; + const anotherUrl = new Request('resources/simple.html?ignore').url; + const oneAnotherUrl = new Request('resources/simple.html?ignore2').url; + + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = false'); + // Use step_timeout(0) to ensure the history entry is created for Blink + // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861. + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.src = anotherUrl; + }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.src = oneAnotherUrl; + }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(-2); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = true'); + }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(-2))'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isHistoryNavigation'; + const anotherUrl = new Request('resources/simple.html?ignore').url; + const oneAnotherUrl = new Request('resources/simple.html?ignore2').url; + const frame = await with_iframe(anotherUrl); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + // Use step_timeout(0) to ensure the history entry is created for Blink + // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861. + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.src = oneAnotherUrl; + }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.src = page_url; + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(-2); + }); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(2); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = true'); + }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(2))'); + +promise_test(async (t) => { + const page_url = 'resources/simple.html?isHistoryNavigation'; + const anotherUrl = new Request('resources/simple.html?ignore').url; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + assert_equals(frame.contentDocument.body.textContent, + 'method = GET, isHistoryNavigation = false'); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + const form = frame.contentDocument.createElement('form'); + form.method = 'POST'; + form.name = 'form'; + form.action = new Request(page_url).url; + frame.contentDocument.body.appendChild(form); + form.submit(); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = POST, isHistoryNavigation = false'); + // Use step_timeout(0) to ensure the history entry is created for Blink + // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861. + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.src = anotherUrl; + }); + assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n"); + await wait(0); + await new Promise((resolve) => { + frame.addEventListener('load', resolve); + frame.contentWindow.history.go(-1); + }); + assert_equals(frame.contentDocument.body.textContent, + 'method = POST, isHistoryNavigation = true'); + }, 'FetchEvent#request.isHistoryNavigation is true (POST + history.go(-1))'); + +// When service worker responds with a Response, no XHR upload progress +// events are delivered. +promise_test(async t => { + const page_url = 'resources/simple.html?ignore-for-request-body-string'; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + + const xhr = new frame.contentWindow.XMLHttpRequest(); + xhr.open('POST', 'simple.html?request-body'); + xhr.upload.addEventListener('progress', t.unreached_func('progress')); + xhr.upload.addEventListener('error', t.unreached_func('error')); + xhr.upload.addEventListener('abort', t.unreached_func('abort')); + xhr.upload.addEventListener('timeout', t.unreached_func('timeout')); + xhr.upload.addEventListener('load', t.unreached_func('load')); + xhr.upload.addEventListener('loadend', t.unreached_func('loadend')); + xhr.send('i am the request body'); + + await new Promise((resolve) => xhr.addEventListener('load', resolve)); + }, 'XHR upload progress events for response coming from SW'); + +// Upload progress events should be delivered for the network fallback case. +promise_test(async t => { + const page_url = 'resources/simple.html?ignore-for-request-body-string'; + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + + let progress = false; + let load = false; + let loadend = false; + + const xhr = new frame.contentWindow.XMLHttpRequest(); + xhr.open('POST', '/fetch/api/resources/echo-content.py?ignore'); + xhr.upload.addEventListener('progress', () => progress = true); + xhr.upload.addEventListener('error', t.unreached_func('error')); + xhr.upload.addEventListener('abort', t.unreached_func('abort')); + xhr.upload.addEventListener('timeout', t.unreached_func('timeout')); + xhr.upload.addEventListener('load', () => load = true); + xhr.upload.addEventListener('loadend', () => loadend = true); + xhr.send('i am the request body'); + + await new Promise((resolve) => xhr.addEventListener('load', resolve)); + assert_true(progress, 'progress'); + assert_true(load, 'load'); + assert_true(loadend, 'loadend'); + }, 'XHR upload progress events for network fallback'); + +promise_test(async t => { + // Set page_url to "?ignore" so the service worker falls back to network + // for the main resource request, and add a suffix to avoid colliding + // with other tests. + const page_url = 'resources/?ignore-for-request-body-fallback-string'; + + const frame = await with_iframe(page_url); + t.add_cleanup(() => { frame.remove(); }); + // Add "?clone-and-ignore" so the service worker falls back to + // echo-content.py. + const echo_url = '/fetch/api/resources/echo-content.py?status=421'; + const response = await frame.contentWindow.fetch(echo_url, { + method: 'POST', + body: 'text body' + }); + assert_equals(response.status, 421); + const text = await response.text(); + assert_equals( + text, + 'text body. Request was sent 1 times.', + 'the network fallback request should include the request body'); + }, 'Fetch with POST with text on sw 421 response should not be retried.'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-frame-resource.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-frame-resource.https.html new file mode 100644 index 0000000000..a33309f34f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-frame-resource.https.html @@ -0,0 +1,236 @@ +<!DOCTYPE html> +<title>Service Worker: Fetch for the frame loading.</title> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +var worker = 'resources/fetch-rewrite-worker.js'; +var path = base_path() + 'resources/fetch-access-control.py'; +var host_info = get_host_info(); + +function getLoadedObject(win, contentFunc, closeFunc) { + return new Promise(function(resolve) { + function done(contentString) { + var result = null; + // fetch-access-control.py returns a string like "report( <json> )". + // Eval the returned string with a report functionto get the json + // object. + try { + function report(obj) { result = obj }; + eval(contentString); + } catch(e) { + // just resolve null if we get unexpected page content + } + closeFunc(win); + resolve(result); + } + + // We can't catch the network error on window. So we use the timer. + var timeout = setTimeout(function() { + // Failure pages are considered cross-origin in some browsers. This + // means you cannot even .resolve() the window because the check for + // the .then property will throw. Instead, treat cross-origin + // failure pages as the empty string which will fail to parse as the + // expected json result. + var content = ''; + try { + content = contentFunc(win); + } catch(e) { + // use default empty string for cross-domain window + } + done(content); + }, 10000); + + win.onload = function() { + clearTimeout(timeout); + let content = ''; + try { + content = contentFunc(win); + } catch(e) { + // use default empty string for cross-domain window (see above) + } + done(content); + }; + }); +} + +function getLoadedFrameAsObject(frame) { + return getLoadedObject(frame, function(f) { + return f.contentDocument.body.textContent; + }, function(f) { + f.parentNode.removeChild(f); + }); +} + +function getLoadedWindowAsObject(win) { + return getLoadedObject(win, function(w) { + return w.document.body.textContent + }, function(w) { + w.close(); + }); +} + +promise_test(function(t) { + var scope = 'resources/fetch-frame-resource/frame-basic'; + var frame; + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { + frame = document.createElement('iframe'); + frame.src = + scope + '?url=' + + encodeURIComponent(host_info['HTTPS_ORIGIN'] + path); + document.body.appendChild(frame); + return getLoadedFrameAsObject(frame); + }) + .then(function(result) { + assert_equals( + result.jsonpResult, + 'success', + 'Basic type response could be loaded in the iframe.'); + frame.remove(); + }); + }, 'Basic type response could be loaded in the iframe.'); + +promise_test(function(t) { + var scope = 'resources/fetch-frame-resource/frame-cors'; + var frame; + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { + frame = document.createElement('iframe'); + frame.src = + scope + '?mode=cors&url=' + + encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path + + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ACACredentials=true'); + document.body.appendChild(frame); + return getLoadedFrameAsObject(frame); + }) + .then(function(result) { + assert_equals( + result.jsonpResult, + 'success', + 'CORS type response could be loaded in the iframe.'); + frame.remove(); + }); + }, 'CORS type response could be loaded in the iframe.'); + +promise_test(function(t) { + var scope = 'resources/fetch-frame-resource/frame-opaque'; + var frame; + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { + frame = document.createElement('iframe'); + frame.src = + scope + '?mode=no-cors&url=' + + encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path); + document.body.appendChild(frame); + return getLoadedFrameAsObject(frame); + }) + .then(function(result) { + assert_equals( + result, + null, + 'Opaque type response could not be loaded in the iframe.'); + frame.remove(); + }); + }, 'Opaque type response could not be loaded in the iframe.'); + +promise_test(function(t) { + var scope = 'resources/fetch-frame-resource/window-basic'; + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { + var win = window.open( + scope + '?url=' + + encodeURIComponent(host_info['HTTPS_ORIGIN'] + path)); + return getLoadedWindowAsObject(win); + }) + .then(function(result) { + assert_equals( + result.jsonpResult, + 'success', + 'Basic type response could be loaded in the new window.'); + }); + }, 'Basic type response could be loaded in the new window.'); + +promise_test(function(t) { + var scope = 'resources/fetch-frame-resource/window-cors'; + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { + var win = window.open( + scope + '?mode=cors&url=' + + encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path + + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ACACredentials=true')); + return getLoadedWindowAsObject(win); + }) + .then(function(result) { + assert_equals( + result.jsonpResult, + 'success', + 'CORS type response could be loaded in the new window.'); + }); + }, 'CORS type response could be loaded in the new window.'); + +promise_test(function(t) { + var scope = 'resources/fetch-frame-resource/window-opaque'; + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { + var win = window.open( + scope + '?mode=no-cors&url=' + + encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path)); + return getLoadedWindowAsObject(win); + }) + .then(function(result) { + assert_equals( + result, + null, + 'Opaque type response could not be loaded in the new window.'); + }); + }, 'Opaque type response could not be loaded in the new window.'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-header-visibility.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-header-visibility.https.html new file mode 100644 index 0000000000..1f4813c4f8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-header-visibility.https.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<title>Service Worker: Visibility of headers during fetch.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + var worker = 'resources/fetch-rewrite-worker.js'; + var path = base_path() + 'resources/fetch-access-control.py'; + var host_info = get_host_info(); + var frame; + + promise_test(function(t) { + var scope = 'resources/fetch-header-visibility-iframe.html'; + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { + frame = document.createElement('iframe'); + frame.src = scope; + document.body.appendChild(frame); + + // Resolve a promise when we recieve 2 success messages + return new Promise(function(resolve, reject) { + var remaining = 4; + function onMessage(e) { + if (e.data == 'PASS') { + remaining--; + if (remaining == 0) { + resolve(); + } else { + return; + } + } else { + reject(e.data); + } + + window.removeEventListener('message', onMessage); + } + window.addEventListener('message', onMessage); + }); + }) + .then(function(result) { + frame.remove(); + }); + }, 'Visibility of defaulted headers during interception'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html new file mode 100644 index 0000000000..0e8fa93b32 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<title>Service Worker: Mixed content of fetch()</title> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<body></body> +<script> +async_test(function(t) { + var host_info = get_host_info(); + window.addEventListener('message', t.step_func(on_message), false); + with_iframe( + host_info['HTTPS_ORIGIN'] + base_path() + + 'resources/fetch-mixed-content-iframe.html?target=inscope'); + function on_message(e) { + assert_equals(e.data.results, 'finish'); + t.done(); + } + }, 'Verify Mixed content of fetch() in a Service Worker'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html new file mode 100644 index 0000000000..391dc5d2c1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<title>Service Worker: Mixed content of fetch()</title> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<body></body> +<script> +async_test(function(t) { + var host_info = get_host_info(); + window.addEventListener('message', t.step_func(on_message), false); + with_iframe( + host_info['HTTPS_ORIGIN'] + base_path() + + 'resources/fetch-mixed-content-iframe.html?target=outscope'); + function on_message(e) { + assert_equals(e.data.results, 'finish'); + t.done(); + } + }, 'Verify Mixed content of fetch() in a Service Worker'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-base-url.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-base-url.https.html new file mode 100644 index 0000000000..467a66cee4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-base-url.https.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<title>Service Worker: CSS's base URL must be the response URL</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> +const SCOPE = 'resources/fetch-request-css-base-url-iframe.html'; +const SCRIPT = 'resources/fetch-request-css-base-url-worker.js'; +let worker; + +var signalMessage; +function getNextMessage() { + return new Promise(resolve => { signalMessage = resolve; }); +} + +promise_test(async (t) => { + const registration = await service_worker_unregister_and_register( + t, SCRIPT, SCOPE); + worker = registration.installing; + await wait_for_state(t, worker, 'activated'); +}, 'global setup'); + +// Creates a test concerning the base URL of a stylesheet. It loads a +// stylesheet from a controlled page. The stylesheet makes a subresource +// request for an image. The service worker messages back the details of the +// image request in order to test the base URL. +// +// The request URL for the stylesheet is under "resources/request-url-path/". +// The service worker may respond in a way such that the response URL is +// different to the request URL. +function base_url_test(params) { + promise_test(async (t) => { + let frame; + t.add_cleanup(() => { + if (frame) + frame.remove(); + }); + + // Ask the service worker to message this page once it gets the request + // for the image. + let channel = new MessageChannel(); + const sawPong = getNextMessage(); + channel.port1.onmessage = (event) => { + signalMessage(event.data); + }; + worker.postMessage({port:channel.port2},[channel.port2]); + + // It sends a pong back immediately. This ping/pong protocol helps deflake + // the test for browsers where message/fetch ordering isn't guaranteed. + assert_equals('pong', await sawPong); + + // Load the frame which will load the stylesheet that makes the image + // request. + const sawResult = getNextMessage(); + frame = await with_iframe(params.framePath); + const result = await sawResult; + + // Test the image request. + const base = new URL('.', document.location).href; + assert_equals(result.url, + base + params.expectImageRequestPath, + 'request'); + assert_equals(result.referrer, + base + params.expectImageRequestReferrer, + 'referrer'); + }, params.description); +} + +const cssFile = 'fetch-request-css-base-url-style.css'; + +base_url_test({ + framePath: SCOPE + '?fetch', + expectImageRequestPath: 'resources/sample.png', + expectImageRequestReferrer: `resources/${cssFile}?fetch`, + description: 'base URL when service worker does respondWith(fetch(responseUrl)).'}); + +base_url_test({ + framePath: SCOPE + '?newResponse', + expectImageRequestPath: 'resources/request-url-path/sample.png', + expectImageRequestReferrer: `resources/request-url-path/${cssFile}?newResponse`, + description: 'base URL when service worker does respondWith(new Response()).'}); + +// Cleanup step: this must be the last promise_test. +promise_test(async (t) => { + return service_worker_unregister(t, SCOPE); +}, 'cleanup global state'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html new file mode 100644 index 0000000000..d9c1c7f5df --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<title>Service Worker: Cross-origin CSS files fetched via SW.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +function getElementColorInFrame(frame, id) { + var element = frame.contentDocument.getElementById(id); + var style = frame.contentWindow.getComputedStyle(element, ''); + return style['color']; +} + +promise_test(async t => { + var SCOPE = + 'resources/fetch-request-css-cross-origin'; + var SCRIPT = + 'resources/fetch-request-css-cross-origin-worker.js'; + let registration = await service_worker_unregister_and_register( + t, SCRIPT, SCOPE); + promise_test(async t => { + await registration.unregister(); + }, 'cleanup global state'); + + await wait_for_state(t, registration.installing, 'activated'); +}, 'setup global state'); + +promise_test(async t => { + const EXPECTED_COLOR = 'rgb(0, 0, 255)'; + const PAGE = + 'resources/fetch-request-css-cross-origin-mime-check-iframe.html'; + + const f = await with_iframe(PAGE); + t.add_cleanup(() => {f.remove(); }); + assert_equals( + getElementColorInFrame(f, 'crossOriginCss'), + EXPECTED_COLOR, + 'The color must be overridden by cross origin CSS.'); + assert_equals( + getElementColorInFrame(f, 'crossOriginHtml'), + EXPECTED_COLOR, + 'The color must not be overridden by cross origin non CSS file.'); + assert_equals( + getElementColorInFrame(f, 'sameOriginCss'), + EXPECTED_COLOR, + 'The color must be overridden by same origin CSS.'); + assert_equals( + getElementColorInFrame(f, 'sameOriginHtml'), + EXPECTED_COLOR, + 'The color must be overridden by same origin non CSS file.'); + assert_equals( + getElementColorInFrame(f, 'synthetic'), + EXPECTED_COLOR, + 'The color must be overridden by synthetic CSS.'); +}, 'MIME checking of CSS resources fetched via service worker when Content-Type is not set.'); + +promise_test(async t => { + const PAGE = + 'resources/fetch-request-css-cross-origin-read-contents.html'; + + const f = await with_iframe(PAGE); + t.add_cleanup(() => {f.remove(); }); + assert_throws_dom('SecurityError', f.contentWindow.DOMException, () => { + f.contentDocument.styleSheets[0].cssRules[0].cssText; + }); + assert_equals( + f.contentDocument.styleSheets[1].cssRules[0].cssText, + '#crossOriginCss { color: blue; }', + 'cross-origin CORS approved response'); + assert_equals( + f.contentDocument.styleSheets[2].cssRules[0].cssText, + '#sameOriginCss { color: blue; }', + 'same-origin response'); + assert_equals( + f.contentDocument.styleSheets[3].cssRules[0].cssText, + '#synthetic { color: blue; }', + 'service worker generated response'); + }, 'Same-origin policy for access to CSS resources fetched via service worker'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-images.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-images.https.html new file mode 100644 index 0000000000..586dea2613 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-images.https.html @@ -0,0 +1,214 @@ +<!DOCTYPE html> +<title>Service Worker: FetchEvent for css image</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> +var SCOPE = 'resources/fetch-request-resources-iframe.https.html'; +var SCRIPT = 'resources/fetch-request-resources-worker.js'; +var host_info = get_host_info(); +var LOCAL_URL = + host_info['HTTPS_ORIGIN'] + base_path() + 'resources/sample?test'; +var REMOTE_URL = + host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/sample?test'; + +function css_image_test(expected_results, frame, url, type, + expected_mode, expected_credentials) { + expected_results[url] = { + url: url, + mode: expected_mode, + credentials: expected_credentials, + message: 'CSSImage load (url:' + url + ' type:' + type + ')' + }; + return frame.contentWindow.load_css_image(url, type); +} + +function css_image_set_test(expected_results, frame, url, type, + expected_mode, expected_credentials) { + expected_results[url] = { + url: url, + mode: expected_mode, + credentials: expected_credentials, + message: 'CSSImageSet load (url:' + url + ' type:' + type + ')' + }; + return frame.contentWindow.load_css_image_set(url, type); +} + +function waitForWorker(worker) { + return new Promise(function(resolve) { + var channel = new MessageChannel(); + channel.port1.addEventListener('message', function(msg) { + if (msg.data.ready) { + resolve(channel); + } + }); + channel.port1.start(); + worker.postMessage({port: channel.port2}, [channel.port2]); + }); +} + +function create_message_promise(channel, expected_results, worker, scope) { + return new Promise(function(resolve) { + channel.port1.addEventListener('message', function(msg) { + var result = msg.data; + if (!expected_results[result.url]) { + return; + } + resolve(result); + }); + }).then(function(result) { + var expected = expected_results[result.url]; + assert_equals( + result.mode, expected.mode, + 'mode of ' + expected.message + ' must be ' + + expected.mode + '.'); + assert_equals( + result.credentials, expected.credentials, + 'credentials of ' + expected.message + ' must be ' + + expected.credentials + '.'); + delete expected_results[result.url]; + }); +} + +promise_test(function(t) { + var scope = SCOPE + "?img=backgroundImage"; + var expected_results = {}; + var worker; + var frame; + return service_worker_unregister_and_register(t, SCRIPT, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + t.add_cleanup(function() { + f.remove(); + }); + frame = f; + return waitForWorker(worker); + }) + .then(function(channel) { + css_image_test(expected_results, frame, LOCAL_URL + Date.now(), + 'backgroundImage', 'no-cors', 'include'); + css_image_test(expected_results, frame, REMOTE_URL + Date.now(), + 'backgroundImage', 'no-cors', 'include'); + + return Promise.all([ + create_message_promise(channel, expected_results, worker, scope), + create_message_promise(channel, expected_results, worker, scope) + ]); + }); + }, 'Verify FetchEvent for css image (backgroundImage).'); + +promise_test(function(t) { + var scope = SCOPE + "?img=shapeOutside"; + var expected_results = {}; + var worker; + var frame; + return service_worker_unregister_and_register(t, SCRIPT, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + t.add_cleanup(function() { + f.remove(); + }); + frame = f; + return waitForWorker(worker); + }) + .then(function(channel) { + css_image_test(expected_results, frame, LOCAL_URL + Date.now(), + 'shapeOutside', 'cors', 'same-origin'); + css_image_test(expected_results, frame, REMOTE_URL + Date.now(), + 'shapeOutside', 'cors', 'same-origin'); + + return Promise.all([ + create_message_promise(channel, expected_results, worker, scope), + create_message_promise(channel, expected_results, worker, scope) + ]); + }); + }, 'Verify FetchEvent for css image (shapeOutside).'); + +promise_test(function(t) { + var scope = SCOPE + "?img_set=backgroundImage"; + var expected_results = {}; + var worker; + var frame; + return service_worker_unregister_and_register(t, SCRIPT, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + t.add_cleanup(function() { + f.remove();; + }); + frame = f; + return waitForWorker(worker); + }) + .then(function(channel) { + css_image_set_test(expected_results, frame, LOCAL_URL + Date.now(), + 'backgroundImage', 'no-cors', 'include'); + css_image_set_test(expected_results, frame, REMOTE_URL + Date.now(), + 'backgroundImage', 'no-cors', 'include'); + + return Promise.all([ + create_message_promise(channel, expected_results, worker, scope), + create_message_promise(channel, expected_results, worker, scope) + ]); + }); + }, 'Verify FetchEvent for css image-set (backgroundImage).'); + +promise_test(function(t) { + var scope = SCOPE + "?img_set=shapeOutside"; + var expected_results = {}; + var worker; + var frame; + return service_worker_unregister_and_register(t, SCRIPT, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + t.add_cleanup(function() { + f.remove(); + }); + frame = f; + return waitForWorker(worker); + }) + .then(function(channel) { + css_image_set_test(expected_results, frame, LOCAL_URL + Date.now(), + 'shapeOutside', 'cors', 'same-origin'); + css_image_set_test(expected_results, frame, REMOTE_URL + Date.now(), + 'shapeOutside', 'cors', 'same-origin'); + + return Promise.all([ + create_message_promise(channel, expected_results, worker, scope), + create_message_promise(channel, expected_results, worker, scope) + ]); + }); + }, 'Verify FetchEvent for css image-set (shapeOutside).'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-fallback.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-fallback.https.html new file mode 100644 index 0000000000..a29f31d127 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-fallback.https.html @@ -0,0 +1,282 @@ +<!DOCTYPE html> +<title>Service Worker: the fallback behavior of FetchEvent</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +function get_fetched_urls(worker) { + return new Promise(function(resolve) { + var channel = new MessageChannel(); + channel.port1.onmessage = function(msg) { resolve(msg); }; + worker.postMessage({port: channel.port2}, [channel.port2]); + }); +} + +function check_urls(worker, expected_requests) { + return get_fetched_urls(worker) + .then(function(msg) { + var requests = msg.data.requests; + assert_object_equals(requests, expected_requests); + }); +} + +var path = new URL(".", window.location).pathname; +var SCOPE = 'resources/fetch-request-fallback-iframe.html'; +var SCRIPT = 'resources/fetch-request-fallback-worker.js'; +var host_info = get_host_info(); +var BASE_URL = host_info['HTTPS_ORIGIN'] + + path + 'resources/fetch-access-control.py?'; +var BASE_PNG_URL = BASE_URL + 'PNGIMAGE&'; +var OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + + path + 'resources/fetch-access-control.py?'; +var OTHER_BASE_PNG_URL = OTHER_BASE_URL + 'PNGIMAGE&'; +var REDIRECT_URL = host_info['HTTPS_ORIGIN'] + + path + 'resources/redirect.py?Redirect='; +var register; + +promise_test(function(t) { + var registration; + var worker; + + register = service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(r) { + registration = r; + worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(function(frame) { + // This test should not be considered complete until after the service + // worker has been unregistered. Currently, `testharness.js` does not + // support asynchronous global "tear down" logic, so this must be + // expressed using a dedicated `promise_test`. Because the other + // sub-tests in this file are declared synchronously, this test will be + // the final test executed. + promise_test(function(t) { + t.add_cleanup(function() { + frame.remove(); + }); + return registration.unregister(); + }, 'restore global state'); + + return {frame: frame, worker: worker}; + }); + + return register; + }, 'initialize global state'); + +function promise_frame_test(body, desc) { + promise_test(function(test) { + return register.then(function(result) { + return body(test, result.frame, result.worker); + }); + }, desc); +} + +promise_frame_test(function(t, frame, worker) { + return check_urls( + worker, + [{ + url: host_info['HTTPS_ORIGIN'] + path + SCOPE, + mode: 'navigate' + }]); + }, 'The SW must intercept the request for a main resource.'); + +promise_frame_test(function(t, frame, worker) { + return frame.contentWindow.xhr(BASE_URL) + .then(function() { + return check_urls( + worker, + [{ url: BASE_URL, mode: 'cors' }]); + }); + }, 'The SW must intercept the request of same origin XHR.'); + +promise_frame_test(function(t, frame, worker) { + return promise_rejects_js( + t, + frame.contentWindow.Error, + frame.contentWindow.xhr(OTHER_BASE_URL), + 'SW fallbacked CORS-unsupported other origin XHR should fail.') + .then(function() { + return check_urls( + worker, + [{ url: OTHER_BASE_URL, mode: 'cors' }]); + }); + }, 'The SW must intercept the request of CORS-unsupported other origin XHR.'); + +promise_frame_test(function(t, frame, worker) { + return frame.contentWindow.xhr(OTHER_BASE_URL + 'ACAOrigin=*') + .then(function() { + return check_urls( + worker, + [{ url: OTHER_BASE_URL + 'ACAOrigin=*', mode: 'cors' }]); + }) + }, 'The SW must intercept the request of CORS-supported other origin XHR.'); + +promise_frame_test(function(t, frame, worker) { + return frame.contentWindow.xhr( + REDIRECT_URL + encodeURIComponent(BASE_URL)) + .then(function() { + return check_urls( + worker, + [{ + url: REDIRECT_URL + encodeURIComponent(BASE_URL), + mode: 'cors' + }]); + }); + }, 'The SW must intercept only the first request of redirected XHR.'); + +promise_frame_test(function(t, frame, worker) { + return promise_rejects_js( + t, + frame.contentWindow.Error, + frame.contentWindow.xhr( + REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL)), + 'SW fallbacked XHR which is redirected to CORS-unsupported ' + + 'other origin should fail.') + .then(function() { + return check_urls( + worker, + [{ + url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL), + mode: 'cors' + }]); + }); + }, 'The SW must intercept only the first request for XHR which is' + + ' redirected to CORS-unsupported other origin.'); + +promise_frame_test(function(t, frame, worker) { + return frame.contentWindow.xhr( + REDIRECT_URL + + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*')) + .then(function() { + return check_urls( + worker, + [{ + url: REDIRECT_URL + + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'), + mode: 'cors' + }]); + }); + }, 'The SW must intercept only the first request for XHR which is ' + + 'redirected to CORS-supported other origin.'); + +promise_frame_test(function(t, frame, worker) { + return frame.contentWindow.load_image(BASE_PNG_URL, '') + .then(function() { + return check_urls( + worker, + [{ url: BASE_PNG_URL, mode: 'no-cors' }]); + }); + }, 'The SW must intercept the request for image.'); + +promise_frame_test(function(t, frame, worker) { + return frame.contentWindow.load_image(OTHER_BASE_PNG_URL, '') + .then(function() { + return check_urls( + worker, + [{ url: OTHER_BASE_PNG_URL, mode: 'no-cors' }]); + }); + }, 'The SW must intercept the request for other origin image.'); + +promise_frame_test(function(t, frame, worker) { + return promise_rejects_js( + t, + frame.contentWindow.Error, + frame.contentWindow.load_image(OTHER_BASE_PNG_URL, 'anonymous'), + 'SW fallbacked CORS-unsupported other origin image request ' + + 'should fail.') + .then(function() { + return check_urls( + worker, + [{ url: OTHER_BASE_PNG_URL, mode: 'cors' }]); + }) + }, 'The SW must intercept the request for CORS-unsupported other ' + + 'origin image.'); + +promise_frame_test(function(t, frame, worker) { + return frame.contentWindow.load_image( + OTHER_BASE_PNG_URL + 'ACAOrigin=*', 'anonymous') + .then(function() { + return check_urls( + worker, + [{ url: OTHER_BASE_PNG_URL + 'ACAOrigin=*', mode: 'cors' }]); + }); + }, 'The SW must intercept the request for CORS-supported other ' + + 'origin image.'); + +promise_frame_test(function(t, frame, worker) { + return frame.contentWindow.load_image( + REDIRECT_URL + encodeURIComponent(BASE_PNG_URL), '') + .catch(function() { + assert_unreached( + 'SW fallbacked redirected image request should succeed.'); + }) + .then(function() { + return check_urls( + worker, + [{ + url: REDIRECT_URL + encodeURIComponent(BASE_PNG_URL), + mode: 'no-cors' + }]); + }); + }, 'The SW must intercept only the first request for redirected ' + + 'image resource.'); + +promise_frame_test(function(t, frame, worker) { + return frame.contentWindow.load_image( + REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL), '') + .catch(function() { + assert_unreached( + 'SW fallbacked image request which is redirected to ' + + 'other origin should succeed.'); + }) + .then(function() { + return check_urls( + worker, + [{ + url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL), + mode: 'no-cors' + }]); + }) + }, 'The SW must intercept only the first request for image ' + + 'resource which is redirected to other origin.'); + +promise_frame_test(function(t, frame, worker) { + return promise_rejects_js( + t, + frame.contentWindow.Error, + frame.contentWindow.load_image( + REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL), + 'anonymous'), + 'SW fallbacked image request which is redirected to ' + + 'CORS-unsupported other origin should fail.') + .then(function() { + return check_urls( + worker, + [{ + url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL), + mode: 'cors' + }]); + }); + }, 'The SW must intercept only the first request for image ' + + 'resource which is redirected to CORS-unsupported other origin.'); + +promise_frame_test(function(t, frame, worker) { + return frame.contentWindow.load_image( + REDIRECT_URL + + encodeURIComponent(OTHER_BASE_PNG_URL + 'ACAOrigin=*'), + 'anonymous') + .then(function() { + return check_urls( + worker, + [{ + url: REDIRECT_URL + + encodeURIComponent(OTHER_BASE_PNG_URL + 'ACAOrigin=*'), + mode: 'cors' + }]); + }); + }, 'The SW must intercept only the first request for image ' + + 'resource which is redirected to CORS-supported other origin.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html new file mode 100644 index 0000000000..03b7d35761 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<title>Service Worker: the headers of FetchEvent shouldn't contain freshness headers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> +promise_test(function(t) { + var SCOPE = 'resources/fetch-request-no-freshness-headers-iframe.html'; + var SCRIPT = 'resources/fetch-request-no-freshness-headers-worker.js'; + var worker; + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, SCOPE); + }); + + worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(function(frame) { + return new Promise(function(resolve) { + frame.onload = function() { + resolve(frame); + }; + frame.contentWindow.location.reload(); + }); + }) + .then(function(frame) { + return new Promise(function(resolve) { + var channel = new MessageChannel(); + channel.port1.onmessage = t.step_func(function(msg) { + frame.remove(); + resolve(msg); + }); + worker.postMessage( + {port: channel.port2}, [channel.port2]); + }); + }) + .then(function(msg) { + var freshness_headers = { + 'if-none-match': true, + 'if-modified-since': true + }; + msg.data.requests.forEach(function(request) { + request.headers.forEach(function(header) { + assert_false( + !!freshness_headers[header[0]], + header[0] + ' header must not be set in the ' + + 'FetchEvent\'s request. (url = ' + request.url + ')'); + }); + }) + }); + }, 'The headers of FetchEvent shouldn\'t contain freshness headers.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-redirect.https.html new file mode 100644 index 0000000000..5ce015b421 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-redirect.https.html @@ -0,0 +1,385 @@ +<!DOCTYPE html> +<title>Service Worker: FetchEvent for resources</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/media.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var test_scope = "" +function assert_resolves(promise, description) { + return promise.then( + () => test(() => {}, description + " - " + test_scope), + (e) => test(() => { throw e; }, description + " - " + test_scope) + ); +} + +function assert_rejects(promise, description) { + return promise.then( + () => test(() => { assert_unreached(); }, description + " - " + test_scope), + () => test(() => {}, description + " - " + test_scope) + ); +} + +function iframe_test(url, timeout_enabled) { + return new Promise(function(resolve, reject) { + var frame = document.createElement('iframe'); + frame.src = url; + if (timeout_enabled) { + // We can't catch the network error on iframe. So we use the timer for + // failure detection. + var timer = setTimeout(function() { + reject(new Error('iframe load timeout')); + frame.remove(); + }, 10000); + } + frame.onload = function() { + if (timeout_enabled) + clearTimeout(timer); + try { + if (frame.contentDocument.body.textContent == 'Hello world\n') + resolve(); + else + reject(new Error('content mismatch')); + } catch (e) { + // Chrome treats iframes that failed to load due to a network error as + // having a different origin, so accessing contentDocument throws an + // error. Other browsers might have different behavior. + reject(new Error(e)); + } + frame.remove(); + }; + document.body.appendChild(frame); + }); +} + +promise_test(function(t) { + test_scope = "default"; + + var SCOPE = 'resources/fetch-request-redirect-iframe.html'; + var SCRIPT = 'resources/fetch-rewrite-worker.js'; + var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect='; + var IMAGE_URL = base_path() + 'resources/square.png'; + var AUDIO_URL = getAudioURI("/media/sound_5"); + var XHR_URL = base_path() + 'resources/simple.txt'; + var HTML_URL = base_path() + 'resources/sample.html'; + + var REDIRECT_TO_IMAGE_URL = REDIRECT_URL + encodeURIComponent(IMAGE_URL); + var REDIRECT_TO_AUDIO_URL = REDIRECT_URL + encodeURIComponent(AUDIO_URL); + var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL); + var REDIRECT_TO_HTML_URL = REDIRECT_URL + encodeURIComponent(HTML_URL); + + var worker; + var frame; + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + t.add_cleanup(() => service_worker_unregister(t, SCOPE)); + + worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(async function(f) { + frame = f; + // XMLHttpRequest tests. + await assert_resolves(frame.contentWindow.xhr(XHR_URL), + 'Normal XHR should succeed.'); + await assert_resolves(frame.contentWindow.xhr(REDIRECT_TO_XHR_URL), + 'Redirected XHR should succeed.'); + await assert_resolves( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) + + '&redirect-mode=follow'), + 'Redirected XHR with Request.redirect=follow should succeed.'); + await assert_rejects( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) + + '&redirect-mode=error'), + 'Redirected XHR with Request.redirect=error should fail.'); + await assert_rejects( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) + + '&redirect-mode=manual'), + 'Redirected XHR with Request.redirect=manual should fail.'); + + // Image loading tests. + await assert_resolves(frame.contentWindow.load_image(IMAGE_URL), + 'Normal image resource should be loaded.'); + await assert_resolves( + frame.contentWindow.load_image(REDIRECT_TO_IMAGE_URL), + 'Redirected image resource should be loaded.'); + await assert_resolves( + frame.contentWindow.load_image( + './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) + + '&redirect-mode=follow'), + 'Loading redirected image with Request.redirect=follow should' + + ' succeed.'); + await assert_rejects( + frame.contentWindow.load_image( + './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) + + '&redirect-mode=error'), + 'Loading redirected image with Request.redirect=error should ' + + 'fail.'); + await assert_rejects( + frame.contentWindow.load_image( + './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) + + '&redirect-mode=manual'), + 'Loading redirected image with Request.redirect=manual should' + + ' fail.'); + + // Audio loading tests. + await assert_resolves(frame.contentWindow.load_audio(AUDIO_URL), + 'Normal audio resource should be loaded.'); + await assert_resolves( + frame.contentWindow.load_audio(REDIRECT_TO_AUDIO_URL), + 'Redirected audio resource should be loaded.'); + await assert_resolves( + frame.contentWindow.load_audio( + './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) + + '&redirect-mode=follow'), + 'Loading redirected audio with Request.redirect=follow should' + + ' succeed.'); + await assert_rejects( + frame.contentWindow.load_audio( + './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) + + '&redirect-mode=error'), + 'Loading redirected audio with Request.redirect=error should ' + + 'fail.'); + await assert_rejects( + frame.contentWindow.load_audio( + './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) + + '&redirect-mode=manual'), + 'Loading redirected audio with Request.redirect=manual should' + + ' fail.'); + + // Iframe tests. + await assert_resolves(iframe_test(HTML_URL), + 'Normal iframe loading should succeed.'); + await assert_resolves( + iframe_test(REDIRECT_TO_HTML_URL), + 'Normal redirected iframe loading should succeed.'); + await assert_rejects( + iframe_test(SCOPE + '?url=' + + encodeURIComponent(REDIRECT_TO_HTML_URL) + + '&redirect-mode=follow', + true /* timeout_enabled */), + 'Redirected iframe loading with Request.redirect=follow should'+ + ' fail.'); + await assert_rejects( + iframe_test(SCOPE + '?url=' + + encodeURIComponent(REDIRECT_TO_HTML_URL) + + '&redirect-mode=error', + true /* timeout_enabled */), + 'Redirected iframe loading with Request.redirect=error should '+ + 'fail.'); + await assert_resolves( + iframe_test(SCOPE + '?url=' + + encodeURIComponent(REDIRECT_TO_HTML_URL) + + '&redirect-mode=manual', + true /* timeout_enabled */), + 'Redirected iframe loading with Request.redirect=manual should'+ + ' succeed.'); + }) + .then(function() { + frame.remove(); + }); + }, 'Verify redirect mode of Fetch API and ServiceWorker FetchEvent.'); + +// test for reponse.redirected +promise_test(function(t) { + test_scope = "redirected"; + + var SCOPE = 'resources/fetch-request-redirect-iframe.html'; + var SCRIPT = 'resources/fetch-rewrite-worker.js'; + var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect='; + var XHR_URL = base_path() + 'resources/simple.txt'; + var IMAGE_URL = base_path() + 'resources/square.png'; + + var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL); + + var host_info = get_host_info(); + + var CROSS_ORIGIN_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_URL; + + var REDIRECT_TO_CROSS_ORIGIN = REDIRECT_URL + + encodeURIComponent(CROSS_ORIGIN_URL + '?ACAOrigin=*'); + + var worker; + var frame; + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + t.add_cleanup(() => service_worker_unregister(t, SCOPE)); + + worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(async function(f) { + frame = f; + // XMLHttpRequest tests. + await assert_resolves( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(XHR_URL) + + '&expected_redirected=false' + + '&expected_resolves=true'), + 'Normal XHR should be resolved and response should not be ' + + 'redirected.'); + await assert_resolves( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) + + '&expected_redirected=true' + + '&expected_resolves=true'), + 'Redirected XHR should be resolved and response should be ' + + 'redirected.'); + + // tests for request's mode = cors + await assert_resolves( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(XHR_URL) + + '&mode=cors' + + '&expected_redirected=false' + + '&expected_resolves=true'), + 'Normal XHR should be resolved and response should not be ' + + 'redirected even with CORS mode.'); + await assert_resolves( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) + + '&mode=cors' + + '&redirect-mode=follow' + + '&expected_redirected=true' + + '&expected_resolves=true'), + 'Redirected XHR should be resolved and response.redirected ' + + 'should be redirected with CORS mode.'); + + // tests for request's mode = no-cors + // The response.redirect should be false since we will not add + // redirected url list when redirect-mode is not follow. + await assert_rejects( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) + + '&mode=no-cors' + + '&redirect-mode=manual' + + '&expected_redirected=false' + + '&expected_resolves=false'), + 'Redirected XHR should be reject and response should be ' + + 'redirected with NO-CORS mode and redirect-mode=manual.'); + + // tests for redirecting to a cors + await assert_resolves( + frame.contentWindow.load_image( + './?url=' + encodeURIComponent(REDIRECT_TO_CROSS_ORIGIN) + + '&mode=no-cors' + + '&redirect-mode=follow' + + '&expected_redirected=false' + + '&expected_resolves=true'), + 'Redirected CORS image should be reject and response should ' + + 'not be redirected with NO-CORS mode.'); + }) + .then(function() { + frame.remove(); + }); + }, 'Verify redirected of Response(Fetch API) and ServiceWorker FetchEvent.'); + +// test for reponse.redirected after cached +promise_test(function(t) { + test_scope = "cache"; + + var SCOPE = 'resources/fetch-request-redirect-iframe.html'; + var SCRIPT = 'resources/fetch-rewrite-worker.js'; + var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect='; + var XHR_URL = base_path() + 'resources/simple.txt'; + var IMAGE_URL = base_path() + 'resources/square.png'; + + var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL); + + var host_info = get_host_info(); + + var CROSS_ORIGIN_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_URL; + + var REDIRECT_TO_CROSS_ORIGIN = REDIRECT_URL + + encodeURIComponent(CROSS_ORIGIN_URL + '?ACAOrigin=*'); + + var worker; + var frame; + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + t.add_cleanup(() => service_worker_unregister(t, SCOPE)); + + worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(async function(f) { + frame = f; + // XMLHttpRequest tests. + await assert_resolves( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(XHR_URL) + + '&expected_redirected=false' + + '&expected_resolves=true' + + '&cache'), + 'Normal XHR should be resolved and response should not be ' + + 'redirected.'); + await assert_resolves( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) + + '&expected_redirected=true' + + '&expected_resolves=true' + + '&cache'), + 'Redirected XHR should be resolved and response should be ' + + 'redirected.'); + + // tests for request's mode = cors + await assert_resolves( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(XHR_URL) + + '&mode=cors' + + '&expected_redirected=false' + + '&expected_resolves=true' + + '&cache'), + 'Normal XHR should be resolved and response should not be ' + + 'redirected even with CORS mode.'); + await assert_resolves( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) + + '&mode=cors' + + '&redirect-mode=follow' + + '&expected_redirected=true' + + '&expected_resolves=true' + + '&cache'), + 'Redirected XHR should be resolved and response.redirected ' + + 'should be redirected with CORS mode.'); + + // tests for request's mode = no-cors + // The response.redirect should be false since we will not add + // redirected url list when redirect-mode is not follow. + await assert_rejects( + frame.contentWindow.xhr( + './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) + + '&mode=no-cors' + + '&redirect-mode=manual' + + '&expected_redirected=false' + + '&expected_resolves=false' + + '&cache'), + 'Redirected XHR should be reject and response should be ' + + 'redirected with NO-CORS mode and redirect-mode=manual.'); + + // tests for redirecting to a cors + await assert_resolves( + frame.contentWindow.load_image( + './?url=' + encodeURIComponent(REDIRECT_TO_CROSS_ORIGIN) + + '&mode=no-cors' + + '&redirect-mode=follow' + + '&expected_redirected=false' + + '&expected_resolves=true' + + '&cache'), + 'Redirected CORS image should be reject and response should ' + + 'not be redirected with NO-CORS mode.'); + }) + .then(function() { + frame.remove(); + }); + }, 'Verify redirected of Response(Fetch API), Cache API and ServiceWorker ' + + 'FetchEvent.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-resources.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-resources.https.html new file mode 100644 index 0000000000..b4680c3ccd --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-resources.https.html @@ -0,0 +1,302 @@ +<!DOCTYPE html> +<title>Service Worker: FetchEvent for resources</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +let url_count = 0; +const expected_results = {}; + +function add_promise_to_test(url) +{ + const expected = expected_results[url]; + return new Promise((resolve) => { + expected.resolve = resolve; + }); +} + +function image_test(frame, url, cross_origin, expected_mode, + expected_credentials) { + const actual_url = url + (++url_count); + expected_results[actual_url] = { + cross_origin: cross_origin, + mode: expected_mode, + credentials: expected_credentials, + redirect: 'follow', + integrity: '', + destination: 'image', + message: `Image load (url:${actual_url} cross_origin:${cross_origin})` + }; + frame.contentWindow.load_image(actual_url, cross_origin); + return add_promise_to_test(actual_url); +} + +function script_test(frame, url, cross_origin, expected_mode, + expected_credentials) { + const actual_url = url + (++url_count); + expected_results[actual_url] = { + cross_origin: cross_origin, + mode: expected_mode, + credentials: expected_credentials, + redirect: 'follow', + integrity: '', + destination: 'script', + message: `Script load (url:${actual_url} cross_origin:${cross_origin})` + }; + frame.contentWindow.load_script(actual_url, cross_origin); + return add_promise_to_test(actual_url); +} + +function css_test(frame, url, cross_origin, expected_mode, + expected_credentials) { + const actual_url = url + (++url_count); + expected_results[actual_url] = { + cross_origin: cross_origin, + mode: expected_mode, + credentials: expected_credentials, + redirect: 'follow', + integrity: '', + destination: 'style', + message: `CSS load (url:${actual_url} cross_origin:${cross_origin})` + }; + frame.contentWindow.load_css(actual_url, cross_origin); + return add_promise_to_test(actual_url); +} + +function font_face_test(frame, url, expected_mode, expected_credentials) { + const actual_url = url + (++url_count); + expected_results[actual_url] = { + url: actual_url, + mode: expected_mode, + credentials: expected_credentials, + redirect: 'follow', + integrity: '', + destination: 'font', + message: `FontFace load (url: ${actual_url})` + }; + frame.contentWindow.load_font(actual_url); + return add_promise_to_test(actual_url); +} + +function script_integrity_test(frame, url, integrity, expected_integrity) { + const actual_url = url + (++url_count); + expected_results[actual_url] = { + url: actual_url, + mode: 'no-cors', + credentials: 'include', + redirect: 'follow', + integrity: expected_integrity, + destination: 'script', + message: `Script load (url:${actual_url})` + }; + frame.contentWindow.load_script_with_integrity(actual_url, integrity); + return add_promise_to_test(actual_url); +} + +function css_integrity_test(frame, url, integrity, expected_integrity) { + const actual_url = url + (++url_count); + expected_results[actual_url] = { + url: actual_url, + mode: 'no-cors', + credentials: 'include', + redirect: 'follow', + integrity: expected_integrity, + destination: 'style', + message: `CSS load (url:${actual_url})` + }; + frame.contentWindow.load_css_with_integrity(actual_url, integrity); + return add_promise_to_test(actual_url); +} + +function fetch_test(frame, url, mode, credentials, + expected_mode, expected_credentials) { + const actual_url = url + (++url_count); + expected_results[actual_url] = { + mode: expected_mode, + credentials: expected_credentials, + redirect: 'follow', + integrity: '', + destination: '', + message: `fetch (url:${actual_url} mode:${mode} ` + + `credentials:${credentials})` + }; + frame.contentWindow.fetch( + new Request(actual_url, {mode: mode, credentials: credentials})); + return add_promise_to_test(actual_url); +} + +function audio_test(frame, url, cross_origin, + expected_mode, expected_credentials) { + const actual_url = url + (++url_count); + expected_results[actual_url] = { + mode: expected_mode, + credentials: expected_credentials, + redirect: 'follow', + integrity: '', + destination: 'audio', + message: `Audio load (url:${actual_url} cross_origin:${cross_origin})` + }; + frame.contentWindow.load_audio(actual_url, cross_origin); + return add_promise_to_test(actual_url); +} + + +function video_test(frame, url, cross_origin, + expected_mode, expected_credentials) { + const actual_url = url + (++url_count); + expected_results[actual_url] = { + mode: expected_mode, + credentials: expected_credentials, + redirect: 'follow', + integrity: '', + destination: 'video', + message: `Video load (url:${actual_url} cross_origin:${cross_origin})` + }; + frame.contentWindow.load_video(actual_url, cross_origin); + return add_promise_to_test(actual_url); +} + +promise_test(async t => { + const SCOPE = 'resources/fetch-request-resources-iframe.https.html'; + const SCRIPT = 'resources/fetch-request-resources-worker.js'; + const host_info = get_host_info(); + const LOCAL_URL = + host_info['HTTPS_ORIGIN'] + base_path() + 'resources/sample?test'; + const REMOTE_URL = + host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/sample?test'; + + const registration = + await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + t.add_cleanup(() => registration.unregister()); + const worker = registration.installing; + await wait_for_state(t, worker, 'activated'); + + await new Promise((resolve, reject) => { + const channel = new MessageChannel(); + channel.port1.onmessage = t.step_func(msg => { + if (msg.data.ready) { + resolve(); + return; + } + const result = msg.data; + const expected = expected_results[result.url]; + if (!expected) { + return; + } + test(() => { + assert_equals( + result.mode, expected.mode, + `mode of must be ${expected.mode}.`); + assert_equals( + result.credentials, expected.credentials, + `credentials of ${expected.message} must be ` + + `${expected.credentials}.`); + assert_equals( + result.redirect, expected.redirect, + `redirect mode of ${expected.message} must be ` + + `${expected.redirect}.`); + assert_equals( + result.integrity, expected.integrity, + `integrity of ${expected.message} must be ` + + `${expected.integrity}.`); + assert_equals( + result.destination, expected.destination, + `destination of ${expected.message} must be ` + + `${expected.destination}.`); + }, expected.message); + expected.resolve(); + delete expected_results[result.url]; + }); + worker.postMessage({port: channel.port2}, [channel.port2]); + }); + + const f = await with_iframe(SCOPE); + t.add_cleanup(() => f.remove()); + + await image_test(f, LOCAL_URL, '', 'no-cors', 'include'); + await image_test(f, REMOTE_URL, '', 'no-cors', 'include'); + await css_test(f, LOCAL_URL, '', 'no-cors', 'include'); + await css_test(f, REMOTE_URL, '', 'no-cors', 'include'); + + await image_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin'); + await image_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include'); + await image_test(f, REMOTE_URL, '', 'no-cors', 'include'); + await image_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin'); + await image_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include'); + + await script_test(f, LOCAL_URL, '', 'no-cors', 'include'); + await script_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin'); + await script_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include'); + await script_test(f, REMOTE_URL, '', 'no-cors', 'include'); + await script_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin'); + await script_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include'); + + await css_test(f, LOCAL_URL, '', 'no-cors', 'include'); + await css_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin'); + await css_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include'); + await css_test(f, REMOTE_URL, '', 'no-cors', 'include'); + await css_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin'); + await css_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include'); + + await font_face_test(f, LOCAL_URL, 'cors', 'same-origin'); + await font_face_test(f, REMOTE_URL, 'cors', 'same-origin'); + + await script_integrity_test(f, LOCAL_URL, ' ', ' '); + await script_integrity_test( + f, LOCAL_URL, + 'This is not a valid integrity because it has no dashes', + 'This is not a valid integrity because it has no dashes'); + await script_integrity_test(f, LOCAL_URL, 'sha256-', 'sha256-'); + await script_integrity_test(f, LOCAL_URL, 'sha256-foo?123', 'sha256-foo?123'); + await script_integrity_test(f, LOCAL_URL, 'sha256-foo sha384-abc ', + 'sha256-foo sha384-abc '); + await script_integrity_test(f, LOCAL_URL, 'sha256-foo sha256-abc', + 'sha256-foo sha256-abc'); + + await css_integrity_test(f, LOCAL_URL, ' ', ' '); + await css_integrity_test( + f, LOCAL_URL, + 'This is not a valid integrity because it has no dashes', + 'This is not a valid integrity because it has no dashes'); + await css_integrity_test(f, LOCAL_URL, 'sha256-', 'sha256-'); + await css_integrity_test(f, LOCAL_URL, 'sha256-foo?123', 'sha256-foo?123'); + await css_integrity_test(f, LOCAL_URL, 'sha256-foo sha384-abc ', + 'sha256-foo sha384-abc '); + await css_integrity_test(f, LOCAL_URL, 'sha256-foo sha256-abc', + 'sha256-foo sha256-abc'); + + await fetch_test(f, LOCAL_URL, 'same-origin', 'omit', 'same-origin', 'omit'); + await fetch_test(f, LOCAL_URL, 'same-origin', 'same-origin', + 'same-origin', 'same-origin'); + await fetch_test(f, LOCAL_URL, 'same-origin', 'include', + 'same-origin', 'include'); + await fetch_test(f, LOCAL_URL, 'no-cors', 'omit', 'no-cors', 'omit'); + await fetch_test(f, LOCAL_URL, 'no-cors', 'same-origin', + 'no-cors', 'same-origin'); + await fetch_test(f, LOCAL_URL, 'no-cors', 'include', 'no-cors', 'include'); + await fetch_test(f, LOCAL_URL, 'cors', 'omit', 'cors', 'omit'); + await fetch_test(f, LOCAL_URL, 'cors', 'same-origin', 'cors', 'same-origin'); + await fetch_test(f, LOCAL_URL, 'cors', 'include', 'cors', 'include'); + await fetch_test(f, REMOTE_URL, 'no-cors', 'omit', 'no-cors', 'omit'); + await fetch_test(f, REMOTE_URL, 'no-cors', 'same-origin', 'no-cors', 'same-origin'); + await fetch_test(f, REMOTE_URL, 'no-cors', 'include', 'no-cors', 'include'); + await fetch_test(f, REMOTE_URL, 'cors', 'omit', 'cors', 'omit'); + await fetch_test(f, REMOTE_URL, 'cors', 'same-origin', 'cors', 'same-origin'); + await fetch_test(f, REMOTE_URL, 'cors', 'include', 'cors', 'include'); + + await audio_test(f, LOCAL_URL, '', 'no-cors', 'include'); + await audio_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin'); + await audio_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include'); + await audio_test(f, REMOTE_URL, '', 'no-cors', 'include'); + await audio_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin'); + await audio_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include'); + + await video_test(f, LOCAL_URL, '', 'no-cors', 'include'); + await video_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin'); + await video_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include'); + await video_test(f, REMOTE_URL, '', 'no-cors', 'include'); + await video_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin'); + await video_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include'); +}, 'Verify FetchEvent for resources.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js new file mode 100644 index 0000000000..e6c0213928 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js @@ -0,0 +1,19 @@ +// META: script=resources/test-helpers.sub.js + +"use strict"; + +promise_test(async t => { + const url = "resources/fetch-request-xhr-sync-error-worker.js"; + const scope = "resources/fetch-request-xhr-sync-iframe.html"; + + const registration = await service_worker_unregister_and_register(t, url, scope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-1.txt")); + assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-2.txt")); + assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-3.txt")); +}, "Verify synchronous XMLHttpRequest always throws a NetworkError for ReadableStream errors"); diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html new file mode 100644 index 0000000000..9f18096aa2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<title>Service Worker: Synchronous XHR on Worker is intercepted</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +promise_test((t) => { + const url = 'resources/fetch-request-xhr-sync-on-worker-worker.js'; + const scope = 'resources/fetch-request-xhr-sync-on-worker-scope/'; + const non_existent_file = 'non-existent-file.txt'; + + // In Chromium, the service worker scope matching for workers is based on + // the URL of the parent HTML. So this test creates an iframe which is + // controlled by the service worker first, and creates a worker from the + // iframe. + return service_worker_unregister_and_register(t, url, scope) + .then((registration) => { + t.add_cleanup(() => registration.unregister()); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { return with_iframe(scope + 'iframe_page'); }) + .then((frame) => { + t.add_cleanup(() => frame.remove()); + return frame.contentWindow.performSyncXHROnWorker(non_existent_file); + }) + .then((result) => { + assert_equals( + result.status, + 200, + 'HTTP response status code for intercepted request' + ); + assert_equals( + result.responseText, + 'Response from service worker', + 'HTTP response text for intercepted request' + ); + }); + }, 'Verify SyncXHR on Worker is intercepted'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html new file mode 100644 index 0000000000..ec27fb8983 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<title>Service Worker: Synchronous XHR is intercepted</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +promise_test(function(t) { + var url = 'resources/fetch-request-xhr-sync-worker.js'; + var scope = 'resources/fetch-request-xhr-sync-iframe.html'; + var non_existent_file = 'non-existent-file.txt'; + + return service_worker_unregister_and_register(t, url, scope) + .then(function(registration) { + t.add_cleanup(function() { + return registration.unregister(); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(frame) { + t.add_cleanup(function() { + frame.remove(); + }); + + return new Promise(function(resolve, reject) { + t.step_timeout(function() { + var xhr; + try { + xhr = frame.contentWindow.performSyncXHR(non_existent_file); + resolve(xhr); + } catch (err) { + reject(err); + } + }, 0); + }) + }) + .then(function(xhr) { + assert_equals( + xhr.status, + 200, + 'HTTP response status code for intercepted request' + ); + assert_equals( + xhr.responseText, + 'Response from service worker', + 'HTTP response text for intercepted request' + ); + }); + }, 'Verify SyncXHR is intercepted'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr.https.html new file mode 100644 index 0000000000..37a457393b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr.https.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<title>Service Worker: the body of FetchEvent using XMLHttpRequest</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe-sub"></script> +<script> +let frame; + +// Set up the service worker and the frame. +promise_test(t => { + const kScope = 'resources/fetch-request-xhr-iframe.https.html'; + const kScript = 'resources/fetch-request-xhr-worker.js'; + return service_worker_unregister_and_register(t, kScript, kScope) + .then(registration => { + promise_test(() => { + return registration.unregister(); + }, 'restore global state'); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { + return with_iframe(kScope); + }) + .then(f => { + frame = f; + add_completion_callback(() => { f.remove(); }); + }); + }, 'initialize global state'); + +// Run the tests. +promise_test(t => { + return frame.contentWindow.get_header_test(); + }, 'event.request has the expected headers for same-origin GET.'); + +promise_test(t => { + return frame.contentWindow.post_header_test(); + }, 'event.request has the expected headers for same-origin POST.'); + +promise_test(t => { + return frame.contentWindow.cross_origin_get_header_test(); + }, 'event.request has the expected headers for cross-origin GET.'); + +promise_test(t => { + return frame.contentWindow.cross_origin_post_header_test(); + }, 'event.request has the expected headers for cross-origin POST.'); + +promise_test(t => { + return frame.contentWindow.string_test(); + }, 'FetchEvent#request.body contains XHR request data (string)'); + +promise_test(t => { + return frame.contentWindow.blob_test(); + }, 'FetchEvent#request.body contains XHR request data (blob)'); + +promise_test(t => { + return frame.contentWindow.custom_method_test(); + }, 'FetchEvent#request.method is set to XHR method'); + +promise_test(t => { + return frame.contentWindow.options_method_test(); + }, 'XHR using OPTIONS method'); + +promise_test(t => { + return frame.contentWindow.form_data_test(); + }, 'XHR with form data'); + +promise_test(t => { + return frame.contentWindow.mode_credentials_test(); + }, 'XHR with mode/credentials set'); + +promise_test(t => { + return frame.contentWindow.data_url_test(); + }, 'XHR to data URL'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-response-taint.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-response-taint.https.html new file mode 100644 index 0000000000..8e190f4850 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-response-taint.https.html @@ -0,0 +1,223 @@ +<!DOCTYPE html> +<title>Service Worker: Tainting of responses fetched via SW.</title> +<!-- This test makes a large number of requests sequentially. --> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +var host_info = get_host_info(); +var BASE_ORIGIN = host_info.HTTPS_ORIGIN; +var OTHER_ORIGIN = host_info.HTTPS_REMOTE_ORIGIN; +var BASE_URL = BASE_ORIGIN + base_path() + + 'resources/fetch-access-control.py?'; +var OTHER_BASE_URL = OTHER_ORIGIN + base_path() + + 'resources/fetch-access-control.py?'; + +function frame_fetch(frame, url, mode, credentials) { + var foreignPromise = frame.contentWindow.fetch( + new Request(url, {mode: mode, credentials: credentials})) + + // Event loops should be shared between contexts of similar origin, not all + // browsers adhere to this expectation at the time of this writing. Incorrect + // behavior in this regard can interfere with test execution when the + // provided iframe is removed from the document. + // + // WPT maintains a test dedicated the expected treatment of event loops, so + // the following workaround is acceptable in this context. + return Promise.resolve(foreignPromise); +} + +var login_and_register; +promise_test(function(t) { + var SCOPE = 'resources/fetch-response-taint-iframe.html'; + var SCRIPT = 'resources/fetch-rewrite-worker.js'; + var registration; + + login_and_register = login_https(t, host_info.HTTPS_ORIGIN, host_info.HTTPS_REMOTE_ORIGIN) + .then(function() { + return service_worker_unregister_and_register(t, SCRIPT, SCOPE); + }) + .then(function(r) { + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(function(f) { + // This test should not be considered complete until after the + // service worker has been unregistered. Currently, `testharness.js` + // does not support asynchronous global "tear down" logic, so this + // must be expressed using a dedicated `promise_test`. Because the + // other sub-tests in this file are declared synchronously, this + // test will be the final test executed. + promise_test(function(t) { + f.remove(); + return registration.unregister(); + }, 'restore global state'); + + return f; + }); + return login_and_register; + }, 'initialize global state'); + +function ng_test(url, mode, credentials) { + promise_test(function(t) { + return login_and_register + .then(function(frame) { + var fetchRequest = frame_fetch(frame, url, mode, credentials); + return promise_rejects_js(t, frame.contentWindow.TypeError, fetchRequest); + }); + }, 'url:\"' + url + '\" mode:\"' + mode + + '\" credentials:\"' + credentials + '\" should fail.'); +} + +function ok_test(url, mode, credentials, expected_type, expected_username) { + promise_test(function() { + return login_and_register.then(function(frame) { + return frame_fetch(frame, url, mode, credentials) + }) + .then(function(res) { + assert_equals(res.type, expected_type, 'response type'); + return res.text(); + }) + .then(function(text) { + if (expected_type == 'opaque') { + assert_equals(text, ''); + } else { + return new Promise(function(resolve) { + var report = resolve; + // text must contain report() call. + eval(text); + }) + .then(function(result) { + assert_equals(result.username, expected_username); + }); + } + }); + }, 'fetching url:\"' + url + '\" mode:\"' + mode + + '\" credentials:\"' + credentials + '\" should ' + + 'succeed.'); +} + +function build_rewrite_url(origin, url, mode, credentials) { + return origin + '/?url=' + encodeURIComponent(url) + '&mode=' + mode + + '&credentials=' + credentials + '&'; +} + +function for_each_origin_mode_credentials(callback) { + [BASE_ORIGIN, OTHER_ORIGIN].forEach(function(origin) { + ['same-origin', 'no-cors', 'cors'].forEach(function(mode) { + ['omit', 'same-origin', 'include'].forEach(function(credentials) { + callback(origin, mode, credentials); + }); + }); + }); +} + +ok_test(BASE_URL, 'same-origin', 'omit', 'basic', 'undefined'); +ok_test(BASE_URL, 'same-origin', 'same-origin', 'basic', 'username2s'); +ok_test(BASE_URL, 'same-origin', 'include', 'basic', 'username2s'); +ok_test(BASE_URL, 'no-cors', 'omit', 'basic', 'undefined'); +ok_test(BASE_URL, 'no-cors', 'same-origin', 'basic', 'username2s'); +ok_test(BASE_URL, 'no-cors', 'include', 'basic', 'username2s'); +ok_test(BASE_URL, 'cors', 'omit', 'basic', 'undefined'); +ok_test(BASE_URL, 'cors', 'same-origin', 'basic', 'username2s'); +ok_test(BASE_URL, 'cors', 'include', 'basic', 'username2s'); +ng_test(OTHER_BASE_URL, 'same-origin', 'omit'); +ng_test(OTHER_BASE_URL, 'same-origin', 'same-origin'); +ng_test(OTHER_BASE_URL, 'same-origin', 'include'); +ok_test(OTHER_BASE_URL, 'no-cors', 'omit', 'opaque'); +ok_test(OTHER_BASE_URL, 'no-cors', 'same-origin', 'opaque'); +ok_test(OTHER_BASE_URL, 'no-cors', 'include', 'opaque'); +ng_test(OTHER_BASE_URL, 'cors', 'omit'); +ng_test(OTHER_BASE_URL, 'cors', 'same-origin'); +ng_test(OTHER_BASE_URL, 'cors', 'include'); +ok_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'omit', 'cors', 'undefined'); +ok_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'same-origin', 'cors', + 'undefined'); +ng_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'include'); +ok_test(OTHER_BASE_URL + 'ACAOrigin=' + BASE_ORIGIN + '&ACACredentials=true', + 'cors', 'include', 'cors', 'username1s') + +for_each_origin_mode_credentials(function(origin, mode, credentials) { + var url = build_rewrite_url( + origin, BASE_URL, 'same-origin', 'omit'); + // Fetch to the other origin with same-origin mode should fail. + if (origin == OTHER_ORIGIN && mode == 'same-origin') { + ng_test(url, mode, credentials); + } else { + // The response type from the SW should be basic + ok_test(url, mode, credentials, 'basic', 'undefined'); + } +}); + +for_each_origin_mode_credentials(function(origin, mode, credentials) { + var url = build_rewrite_url( + origin, BASE_URL, 'same-origin', 'same-origin'); + + // Fetch to the other origin with same-origin mode should fail. + if (origin == OTHER_ORIGIN && mode == 'same-origin') { + ng_test(url, mode, credentials); + } else { + // The response type from the SW should be basic. + ok_test(url, mode, credentials, 'basic', 'username2s'); + } +}); + +for_each_origin_mode_credentials(function(origin, mode, credentials) { + var url = build_rewrite_url( + origin, OTHER_BASE_URL, 'same-origin', 'omit'); + // The response from the SW should be an error. + ng_test(url, mode, credentials); +}); + +for_each_origin_mode_credentials(function(origin, mode, credentials) { + var url = build_rewrite_url( + origin, OTHER_BASE_URL, 'no-cors', 'omit'); + + // SW can respond only to no-cors requests. + if (mode != 'no-cors') { + ng_test(url, mode, credentials); + } else { + // The response type from the SW should be opaque. + ok_test(url, mode, credentials, 'opaque'); + } +}); + +for_each_origin_mode_credentials(function(origin, mode, credentials) { + var url = build_rewrite_url( + origin, OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'omit'); + + // Fetch to the other origin with same-origin mode should fail. + if (origin == OTHER_ORIGIN && mode == 'same-origin') { + ng_test(url, mode, credentials); + } else if (origin == BASE_ORIGIN && mode == 'same-origin') { + // Cors type response to a same-origin mode request should fail + ng_test(url, mode, credentials); + } else { + // The response from the SW should be cors. + ok_test(url, mode, credentials, 'cors', 'undefined'); + } +}); + +for_each_origin_mode_credentials(function(origin, mode, credentials) { + var url = build_rewrite_url( + origin, + OTHER_BASE_URL + 'ACAOrigin=' + BASE_ORIGIN + + '&ACACredentials=true', + 'cors', 'include'); + // Fetch to the other origin with same-origin mode should fail. + if (origin == OTHER_ORIGIN && mode == 'same-origin') { + ng_test(url, mode, credentials); + } else if (origin == BASE_ORIGIN && mode == 'same-origin') { + // Cors type response to a same-origin mode request should fail + ng_test(url, mode, credentials); + } else { + // The response from the SW should be cors. + ok_test(url, mode, credentials, 'cors', 'username1s'); + } +}); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-response-xhr.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-response-xhr.https.html new file mode 100644 index 0000000000..891eb02942 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-response-xhr.https.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<title>Service Worker: the response of FetchEvent using XMLHttpRequest</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(function(t) { + var SCOPE = 'resources/fetch-response-xhr-iframe.https.html'; + var SCRIPT = 'resources/fetch-response-xhr-worker.js'; + var host_info = get_host_info(); + + window.addEventListener('message', t.step_func(on_message), false); + function on_message(e) { + assert_equals(e.data.results, 'foo, bar'); + e.source.postMessage('ACK', host_info['HTTPS_ORIGIN']); + } + + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, SCOPE); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(function(frame) { + var channel; + + t.add_cleanup(function() { + frame.remove(); + }); + + channel = new MessageChannel(); + var onPortMsg = new Promise(function(resolve) { + channel.port1.onmessage = resolve; + }); + + frame.contentWindow.postMessage('START', + host_info['HTTPS_ORIGIN'], + [channel.port2]); + + return onPortMsg; + }) + .then(function(e) { + assert_equals(e.data.results, 'finish'); + }); + }, 'Verify the response of FetchEvent using XMLHttpRequest'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-waits-for-activate.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-waits-for-activate.https.html new file mode 100644 index 0000000000..7c888450f0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/fetch-waits-for-activate.https.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<title>Service Worker: Fetch Event Waits for Activate Event</title> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +const worker_url = 'resources/fetch-waits-for-activate-worker.js'; +const normalized_worker_url = normalizeURL(worker_url); +const worker_scope = 'resources/fetch-waits-for-activate/'; + +// Resolves with the Service Worker's registration once it's reached the +// "activating" state. (The Service Worker should remain "activating" until +// explicitly told advance to the "activated" state). +async function registerAndWaitForActivating(t) { + const registration = await service_worker_unregister_and_register( + t, worker_url, worker_scope); + t.add_cleanup(() => service_worker_unregister(t, worker_scope)); + + await wait_for_state(t, registration.installing, 'activating'); + + return registration; +} + +// Attempts to ensure that the "Handle Fetch" algorithm has reached the step +// +// "If activeWorker’s state is "activating", wait for activeWorker’s state to +// become "activated"." +// +// by waiting for some time to pass. +// +// WARNING: whether the algorithm has reached that step isn't directly +// observable, so this is best effort and can race. Note that this can only +// result in false positives (where the algorithm hasn't reached that step yet +// and any functional events haven't actually been handled by the Service +// Worker). +async function ensureFunctionalEventsAreWaiting(registration) { + await (new Promise(resolve => { setTimeout(resolve, 1000); })); + + assert_equals(registration.active.scriptURL, normalized_worker_url, + 'active worker should be present'); + assert_equals(registration.active.state, 'activating', + 'active worker should be in activating state'); +} + +promise_test(async t => { + const registration = await registerAndWaitForActivating(t); + + let frame = null; + t.add_cleanup(() => { + if (frame) { + frame.remove(); + } + }); + + // This should block until we message the worker to tell it to complete + // the activate event. + const frameLoadPromise = with_iframe(worker_scope).then(function(f) { + frame = f; + }); + + await ensureFunctionalEventsAreWaiting(registration); + assert_equals(frame, null, 'frame should not be loaded'); + + registration.active.postMessage('ACTIVATE'); + + await frameLoadPromise; + assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL, + normalized_worker_url, + 'frame should now be loaded and controlled'); + assert_equals(registration.active.state, 'activated', + 'active worker should be in activated state'); +}, 'Navigation fetch events should wait for the activate event to complete.'); + +promise_test(async t => { + const frame = await with_iframe(worker_scope); + t.add_cleanup(() => { frame.remove(); }); + + const registration = await registerAndWaitForActivating(t); + + // Make the Service Worker control the frame so the frame can perform an + // intercepted fetch. + await (new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller.scriptURL, + normalized_worker_url, 'frame should be controlled'); + resolve(); + }; + + registration.active.postMessage('CLAIM'); + })); + + const fetch_url = `${worker_scope}non/existent/path`; + const expected_fetch_result = 'Hello world'; + let fetch_promise_settled = false; + + // This should block until we message the worker to tell it to complete + // the activate event. + const fetchPromise = frame.contentWindow.fetch(fetch_url, { + method: 'POST', + body: expected_fetch_result, + }).then(response => { + fetch_promise_settled = true; + return response; + }); + + await ensureFunctionalEventsAreWaiting(registration); + assert_false(fetch_promise_settled, + "fetch()-ing a Service Worker-controlled scope shouldn't have " + + "settled yet"); + + registration.active.postMessage('ACTIVATE'); + + const response = await fetchPromise; + assert_equals(await response.text(), expected_fetch_result, + "Service Worker should have responded to request to" + + fetch_url) + assert_equals(registration.active.state, 'activated', + 'active worker should be in activated state'); +}, 'Subresource fetch events should wait for the activate event to complete.'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/getregistration.https.html b/testing/web-platform/tests/service-workers/service-worker/getregistration.https.html new file mode 100644 index 0000000000..634c2efa12 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/getregistration.https.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +async_test(function(t) { + var documentURL = 'no-such-worker'; + navigator.serviceWorker.getRegistration(documentURL) + .then(function(value) { + assert_equals(value, undefined, + 'getRegistration should resolve with undefined'); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'getRegistration'); + +promise_test(function(t) { + var scope = 'resources/scope/getregistration/normal'; + var registration; + return service_worker_unregister_and_register(t, 'resources/empty-worker.js', + scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + return navigator.serviceWorker.getRegistration(scope); + }) + .then(function(value) { + assert_equals( + value, registration, + 'getRegistration should resolve to the same registration object'); + }); + }, 'Register then getRegistration'); + +promise_test(function(t) { + var scope = 'resources/scope/getregistration/url-with-fragment'; + var documentURL = scope + '#ref'; + var registration; + return service_worker_unregister_and_register(t, 'resources/empty-worker.js', + scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + return navigator.serviceWorker.getRegistration(documentURL); + }) + .then(function(value) { + assert_equals( + value, registration, + 'getRegistration should resolve to the same registration object'); + }); + }, 'Register then getRegistration with a URL having a fragment'); + +async_test(function(t) { + var documentURL = 'http://example.com/'; + navigator.serviceWorker.getRegistration(documentURL) + .then(function() { + assert_unreached( + 'getRegistration with an out of origin URL should fail'); + }, function(reason) { + assert_equals( + reason.name, 'SecurityError', + 'getRegistration with an out of origin URL should fail'); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'getRegistration with a cross origin URL'); + +async_test(function(t) { + var scope = 'resources/scope/getregistration/register-unregister'; + service_worker_unregister_and_register(t, 'resources/empty-worker.js', + scope) + .then(function(registration) { + return registration.unregister(); + }) + .then(function() { + return navigator.serviceWorker.getRegistration(scope); + }) + .then(function(value) { + assert_equals(value, undefined, + 'getRegistration should resolve with undefined'); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Register then Unregister then getRegistration'); + + +promise_test(async function(t) { + const scope = 'resources/scope/getregistration/register-unregister'; + const registration = await service_worker_unregister_and_register( + t, 'resources/empty-worker.js', scope + ); + + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + const frameNav = frame.contentWindow.navigator; + await registration.unregister(); + const value = await frameNav.serviceWorker.getRegistration(scope); + + assert_equals(value, undefined, 'getRegistration should resolve with undefined'); +}, 'Register then Unregister then getRegistration in controlled iframe'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/getregistrations.https.html b/testing/web-platform/tests/service-workers/service-worker/getregistrations.https.html new file mode 100644 index 0000000000..3a9b9a2331 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/getregistrations.https.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<title>Service Worker: getRegistrations()</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script> +// Purge the existing registrations for the origin. +// getRegistrations() is used in order to avoid adding additional complexity +// e.g. adding an internal function. +promise_test(async () => { + const registrations = await navigator.serviceWorker.getRegistrations(); + await Promise.all(registrations.map(r => r.unregister())); + const value = await navigator.serviceWorker.getRegistrations(); + assert_array_equals( + value, [], + 'getRegistrations should resolve with an empty array.'); +}, 'registrations are not returned following unregister'); + +promise_test(async t => { + const scope = 'resources/scope/getregistrations/normal'; + const script = 'resources/empty-worker.js'; + const registrations = [ + await service_worker_unregister_and_register(t, script, scope)]; + t.add_cleanup(() => registrations[0].unregister()); + const value = await navigator.serviceWorker.getRegistrations(); + assert_array_equals(value, registrations, + 'getRegistrations should resolve with an array of registrations'); +}, 'Register then getRegistrations'); + +promise_test(async t => { + const scope1 = 'resources/scope/getregistrations/scope1'; + const scope2 = 'resources/scope/getregistrations/scope2'; + const scope3 = 'resources/scope/getregistrations/scope12'; + + const script = 'resources/empty-worker.js'; + t.add_cleanup(() => service_worker_unregister(t, scope1)); + t.add_cleanup(() => service_worker_unregister(t, scope2)); + t.add_cleanup(() => service_worker_unregister(t, scope3)); + + const registrations = [ + await service_worker_unregister_and_register(t, script, scope1), + await service_worker_unregister_and_register(t, script, scope2), + await service_worker_unregister_and_register(t, script, scope3), + ]; + + const value = await navigator.serviceWorker.getRegistrations(); + assert_array_equals(value, registrations); +}, 'Register multiple times then getRegistrations'); + +promise_test(async t => { + const scope = 'resources/scope/getregistrations/register-unregister'; + const script = 'resources/empty-worker.js'; + const registration = await service_worker_unregister_and_register(t, script, scope); + await registration.unregister(); + const value = await navigator.serviceWorker.getRegistrations(); + assert_array_equals( + value, [], 'getRegistrations should resolve with an empty array.'); +}, 'Register then Unregister then getRegistrations'); + +promise_test(async t => { + const scope = 'resources/scope/getregistrations/register-unregister-controlled'; + const script = 'resources/empty-worker.js'; + const registration = await service_worker_unregister_and_register(t, script, scope); + await wait_for_state(t, registration.installing, 'activated'); + + // Create a frame controlled by the service worker and unregister the + // worker. + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + await registration.unregister(); + + const value = await navigator.serviceWorker.getRegistrations(); + assert_array_equals( + value, [], + 'getRegistrations should resolve with an empty array.'); + assert_equals(registration.installing, null); + assert_equals(registration.waiting, null); + assert_equals(registration.active.state, 'activated'); +}, 'Register then Unregister with controlled frame then getRegistrations'); + +promise_test(async t => { + const host_info = get_host_info(); + // Rewrite the url to point to remote origin. + const frame_same_origin_url = new URL("resources/frame-for-getregistrations.html", window.location); + const frame_url = host_info['HTTPS_REMOTE_ORIGIN'] + frame_same_origin_url.pathname; + const scope = 'resources/scope-for-getregistrations'; + const script = 'resources/empty-worker.js'; + + // Loads an iframe and waits for 'ready' message from it to resolve promise. + // Caller is responsible for removing frame. + function with_iframe_ready(url) { + return new Promise(resolve => { + const frame = document.createElement('iframe'); + frame.src = url; + window.addEventListener('message', function onMessage(e) { + window.removeEventListener('message', onMessage); + if (e.data == 'ready') { + resolve(frame); + } + }); + document.body.appendChild(frame); + }); + } + + // We need this special frame loading function because the frame is going + // to register it's own service worker and there is the possibility that that + // register() finishes after the register() for the same domain later in the + // test. So we have to wait until the cross origin register() is done, and not + // just until the frame loads. + const frame = await with_iframe_ready(frame_url); + t.add_cleanup(async () => { + // Wait until the cross-origin worker is unregistered. + let resolve; + const channel = new MessageChannel(); + channel.port1.onmessage = e => { + if (e.data == 'unregistered') + resolve(); + }; + frame.contentWindow.postMessage('unregister', '*', [channel.port2]); + await new Promise(r => { resolve = r; }); + + frame.remove(); + }); + + const registrations = [ + await service_worker_unregister_and_register(t, script, scope)]; + t.add_cleanup(() => registrations[0].unregister()); + const value = await navigator.serviceWorker.getRegistrations(); + assert_array_equals( + value, registrations, + 'getRegistrations should only return same origin registrations.'); +}, 'getRegistrations promise resolves only with same origin registrations.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/global-serviceworker.https.any.js b/testing/web-platform/tests/service-workers/service-worker/global-serviceworker.https.any.js new file mode 100644 index 0000000000..19d77847c4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/global-serviceworker.https.any.js @@ -0,0 +1,53 @@ +// META: title=serviceWorker on service worker global +// META: global=serviceworker + +test(() => { + assert_equals(registration.installing, null, 'registration.installing'); + assert_equals(registration.waiting, null, 'registration.waiting'); + assert_equals(registration.active, null, 'registration.active'); + assert_true('serviceWorker' in self, 'self.serviceWorker exists'); + assert_equals(serviceWorker.state, 'parsed', 'serviceWorker.state'); + assert_readonly(self, 'serviceWorker', `self.serviceWorker is read only`); +}, 'First run'); + +// Cache this for later tests. +const initialServiceWorker = self.serviceWorker; + +async_test((t) => { + assert_true('serviceWorker' in self, 'self.serviceWorker exists'); + serviceWorker.postMessage({ messageTest: true }); + + // The rest of the test runs once this receives the above message. + addEventListener('message', t.step_func((event) => { + // Ignore unrelated messages. + if (!event.data.messageTest) return; + assert_equals(event.source, serviceWorker, 'event.source'); + t.done(); + })); +}, 'Can post message to self during startup'); + +// The test is registered now so there isn't a race condition when collecting tests, but the asserts +// don't happen until the 'install' event fires. +async_test((t) => { + addEventListener('install', t.step_func_done(() => { + assert_true('serviceWorker' in self, 'self.serviceWorker exists'); + assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`); + assert_equals(registration.installing, serviceWorker, 'registration.installing'); + assert_equals(registration.waiting, null, 'registration.waiting'); + assert_equals(registration.active, null, 'registration.active'); + assert_equals(serviceWorker.state, 'installing', 'serviceWorker.state'); + })); +}, 'During install'); + +// The test is registered now so there isn't a race condition when collecting tests, but the asserts +// don't happen until the 'activate' event fires. +async_test((t) => { + addEventListener('activate', t.step_func_done(() => { + assert_true('serviceWorker' in self, 'self.serviceWorker exists'); + assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`); + assert_equals(registration.installing, null, 'registration.installing'); + assert_equals(registration.waiting, null, 'registration.waiting'); + assert_equals(registration.active, serviceWorker, 'registration.active'); + assert_equals(serviceWorker.state, 'activating', 'serviceWorker.state'); + })); +}, 'During activate'); diff --git a/testing/web-platform/tests/service-workers/service-worker/historical.https.any.js b/testing/web-platform/tests/service-workers/service-worker/historical.https.any.js new file mode 100644 index 0000000000..20b3ddfbf7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/historical.https.any.js @@ -0,0 +1,5 @@ +// META: global=serviceworker + +test((t) => { + assert_false('targetClientId' in FetchEvent.prototype) +}, 'targetClientId should not be on FetchEvent'); diff --git a/testing/web-platform/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html b/testing/web-platform/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html new file mode 100644 index 0000000000..5626237dcc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<title>register on a secure page after redirect from an non-secure url</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +'use strict'; + +var host_info = get_host_info(); + +// Loads a non-secure url in a new window, which redirects to |target_url|. +// That page then registers a service worker, and messages back with the result. +// Returns a promise that resolves with the result. +function redirect_and_register(target_url) { + var redirect_url = host_info.HTTP_REMOTE_ORIGIN + base_path() + + 'resources/redirect.py?Redirect='; + var child = window.open(redirect_url + encodeURIComponent(target_url)); + return new Promise(resolve => { + window.addEventListener('message', e => resolve(e.data)); + }) + .then(function(result) { + child.close(); + return result; + }); +} + +promise_test(function(t) { + var target_url = window.location.origin + base_path() + + 'resources/http-to-https-redirect-and-register-iframe.html'; + + return redirect_and_register(target_url) + .then(result => { + assert_equals(result, 'OK'); + }); + }, 'register on a secure page after redirect from an non-secure url'); + +promise_test(function(t) { + var target_url = host_info.HTTP_REMOTE_ORIGIN + base_path() + + 'resources/http-to-https-redirect-and-register-iframe.html'; + + return redirect_and_register(target_url) + .then(result => { + assert_equals(result, 'FAIL: navigator.serviceWorker is undefined'); + }); + }, 'register on a non-secure page after redirect from an non-secure url'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html b/testing/web-platform/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html new file mode 100644 index 0000000000..e63f6b348a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +'use strict'; + +let expected = ['immutable', 'immutable', 'immutable', 'immutable', 'immutable']; + +promise_test(t => + navigator.serviceWorker.register('resources/immutable-prototype-serviceworker.js', {scope: './resources/'}) + .then(registration => { + let worker = registration.installing || registration.waiting || registration.active; + let channel = new MessageChannel() + worker.postMessage(channel.port2, [channel.port2]); + let resolve; + let promise = new Promise(r => resolve = r); + channel.port1.onmessage = resolve; + return promise.then(result => assert_array_equals(expected, result.data)); + }), +'worker prototype chain should be immutable'); +</script> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-cross-origin.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-cross-origin.https.html new file mode 100644 index 0000000000..773708a9fb --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-cross-origin.https.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Tests for importScripts: cross-origin</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +promise_test(async t => { + const scope = 'resources/import-scripts-cross-origin'; + await service_worker_unregister(t, scope); + let reg = await navigator.serviceWorker.register( + 'resources/import-scripts-cross-origin-worker.sub.js', { scope: scope }); + t.add_cleanup(_ => reg.unregister()); + assert_not_equals(reg.installing, null, 'worker is installing'); + }, 'importScripts() supports cross-origin requests'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-data-url.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-data-url.https.html new file mode 100644 index 0000000000..f0922193dd --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-data-url.https.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Tests for importScripts: data: URL</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +promise_test(async t => { + const scope = 'resources/import-scripts-data-url'; + await service_worker_unregister(t, scope); + let reg = await navigator.serviceWorker.register( + 'resources/import-scripts-data-url-worker.js', { scope: scope }); + t.add_cleanup(_ => reg.unregister()); + assert_not_equals(reg.installing, null, 'worker is installing'); + }, 'importScripts() supports data URLs'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-mime-types.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-mime-types.https.html new file mode 100644 index 0000000000..1679831d0f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-mime-types.https.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Tests for importScripts: MIME types</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +/** + * Test that a Service Worker's importScript() only accepts valid MIME types. + */ +let serviceWorker = null; + +promise_test(async t => { + const scope = 'resources/import-scripts-mime-types'; + const registration = await service_worker_unregister_and_register(t, + 'resources/import-scripts-mime-types-worker.js', scope); + + add_completion_callback(() => { registration.unregister(); }); + + await wait_for_state(t, registration.installing, 'activated'); + + serviceWorker = registration.active; +}, 'Global setup'); + +promise_test(async t => { + await fetch_tests_from_worker(serviceWorker); +}, 'Fetch importScripts tests from service worker') +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-redirect.https.html new file mode 100644 index 0000000000..07ea49439e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-redirect.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Tests for importScripts: redirect</title> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +promise_test(async t => { + const scope = 'resources/import-scripts-redirect'; + await service_worker_unregister(t, scope); + let reg = await navigator.serviceWorker.register( + 'resources/import-scripts-redirect-worker.js', { scope: scope }); + assert_not_equals(reg.installing, null, 'worker is installing'); + await reg.unregister(); + }, 'importScripts() supports redirects'); + +promise_test(async t => { + const scope = 'resources/import-scripts-redirect'; + await service_worker_unregister(t, scope); + let reg = await navigator.serviceWorker.register( + 'resources/import-scripts-redirect-worker.js', { scope: scope }); + assert_not_equals(reg.installing, null, 'before update'); + await wait_for_state(t, reg.installing, 'activated'); + await Promise.all([ + wait_for_update(t, reg), + reg.update() + ]); + assert_not_equals(reg.installing, null, 'after update'); + await reg.unregister(); + }, + "an imported script redirects, and the body changes during the update check"); + +promise_test(async t => { + const key = token(); + const scope = 'resources/import-scripts-redirect'; + await service_worker_unregister(t, scope); + let reg = await navigator.serviceWorker.register( + `resources/import-scripts-redirect-on-second-time-worker.js?Key=${key}`, + { scope }); + t.add_cleanup(() => reg.unregister()); + + assert_not_equals(reg.installing, null, 'before update'); + await wait_for_state(t, reg.installing, 'activated'); + await Promise.all([ + wait_for_update(t, reg), + reg.update() + ]); + assert_not_equals(reg.installing, null, 'after update'); + }, + "an imported script doesn't redirect initially, then redirects during " + + "the update check and the body changes"); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-resource-map.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-resource-map.https.html new file mode 100644 index 0000000000..4742bd0126 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-resource-map.https.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<meta charset="utf-8" /> +<title>Tests for importScripts: script resource map</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> + <script> + // This test registers a worker that imports a script multiple times. The + // script should be stored on the first import and thereafter that stored + // script should be loaded. The worker asserts that the stored script was + // loaded; if the assert fails then registration fails. + + promise_test(async t => { + const SCOPE = "resources/import-scripts-resource-map"; + const SCRIPT = "resources/import-scripts-resource-map-worker.js"; + await service_worker_unregister(t, SCOPE); + const registration = await navigator.serviceWorker.register(SCRIPT, { + scope: SCOPE + }); + await registration.unregister(); + }, "import the same script URL multiple times"); + + promise_test(async t => { + const SCOPE = "resources/import-scripts-diff-resource-map"; + const SCRIPT = "resources/import-scripts-diff-resource-map-worker.js"; + await service_worker_unregister(t, SCOPE); + const registration = await navigator.serviceWorker.register(SCRIPT, { + scope: SCOPE + }); + await registration.unregister(); + }, "call importScripts() with multiple arguments"); + </script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-updated-flag.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-updated-flag.https.html new file mode 100644 index 0000000000..09b4496aa0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-updated-flag.https.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Tests for importScripts: import scripts updated flag</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +// This test registers a worker that calls importScripts at various stages of +// service worker lifetime. The sub-tests trigger subsequent `importScript` +// invocations via the `message` event. + +var register; + +function post_and_wait_for_reply(worker, message) { + return new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { resolve(e.data); }; + worker.postMessage(message); + }); +} + +promise_test(function(t) { + const scope = 'resources/import-scripts-updated-flag'; + let registration; + + register = service_worker_unregister_and_register( + t, 'resources/import-scripts-updated-flag-worker.js', scope) + .then(r => { + registration = r; + add_completion_callback(() => { registration.unregister(); }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { + // This test should not be considered complete until after the + // service worker has been unregistered. Currently, `testharness.js` + // does not support asynchronous global "tear down" logic, so this + // must be expressed using a dedicated `promise_test`. Because the + // other sub-tests in this file are declared synchronously, this test + // will be the final test executed. + promise_test(function(t) { + return registration.unregister(); + }); + + return registration.active; + }); + + return register; + }, 'initialize global state'); + +promise_test(t => { + return register + .then(function(worker) { + return post_and_wait_for_reply(worker, 'root-and-message'); + }) + .then(result => { + assert_equals(result.error, null); + assert_equals(result.value, 'root-and-message'); + }); + }, 'import script previously imported at worker evaluation time'); + +promise_test(t => { + return register + .then(function(worker) { + return post_and_wait_for_reply(worker, 'install-and-message'); + }) + .then(result => { + assert_equals(result.error, null); + assert_equals(result.value, 'install-and-message'); + }); + }, 'import script previously imported at worker install time'); + +promise_test(t => { + return register + .then(function(worker) { + return post_and_wait_for_reply(worker, 'message'); + }) + .then(result => { + assert_equals(result.error, 'NetworkError'); + assert_equals(result.value, null); + }); + }, 'import script not previously imported'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/indexeddb.https.html b/testing/web-platform/tests/service-workers/service-worker/indexeddb.https.html new file mode 100644 index 0000000000..be9be4968f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/indexeddb.https.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<title>Service Worker: Indexed DB</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +function readDB() { + return new Promise(function(resolve, reject) { + var openRequest = indexedDB.open('db'); + + openRequest.onerror = reject; + openRequest.onsuccess = function() { + var db = openRequest.result; + var tx = db.transaction('store'); + var store = tx.objectStore('store'); + var getRequest = store.get('key'); + + getRequest.onerror = function() { + db.close(); + reject(getRequest.error); + }; + getRequest.onsuccess = function() { + db.close(); + resolve(getRequest.result); + }; + }; + }); +} + +function send(worker, action) { + return new Promise(function(resolve, reject) { + var messageChannel = new MessageChannel(); + messageChannel.port1.onmessage = function(event) { + if (event.data.type === 'error') { + reject(event.data.reason); + } + + resolve(); + }; + + worker.postMessage( + {action: action, port: messageChannel.port2}, + [messageChannel.port2]); + }); +} + +promise_test(function(t) { + var scope = 'resources/blank.html'; + + return service_worker_unregister_and_register( + t, 'resources/indexeddb-worker.js', scope) + .then(function(registration) { + var worker = registration.installing; + + promise_test(function() { + return registration.unregister(); + }, 'clean up: registration'); + + return send(worker, 'create') + .then(function() { + promise_test(function() { + return new Promise(function(resolve, reject) { + var delete_request = indexedDB.deleteDatabase('db'); + + delete_request.onsuccess = resolve; + delete_request.onerror = reject; + }); + }, 'clean up: database'); + }) + .then(readDB) + .then(function(value) { + assert_equals( + value, 'value', + 'The get() result should match what the worker put().'); + }); + }); + }, 'Verify Indexed DB operation in a Service Worker'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/install-event-type.https.html b/testing/web-platform/tests/service-workers/service-worker/install-event-type.https.html new file mode 100644 index 0000000000..7e74af85c3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/install-event-type.https.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +function wait_for_install_event(worker) { + return new Promise(function(resolve) { + worker.addEventListener('statechange', function(event) { + if (worker.state == 'installed') + resolve(true); + else if (worker.state == 'redundant') + resolve(false); + }); + }); +} + +promise_test(function(t) { + var script = 'resources/install-event-type-worker.js'; + var scope = 'resources/install-event-type'; + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + return wait_for_install_event(registration.installing); + }) + .then(function(did_install) { + assert_true(did_install, 'The worker was installed'); + }) + }, 'install event type'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/installing.https.html b/testing/web-platform/tests/service-workers/service-worker/installing.https.html new file mode 100644 index 0000000000..0f257b6aba --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/installing.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<title>ServiceWorker: navigator.serviceWorker.installing</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +const SCRIPT = 'resources/empty-worker.js'; +const SCOPE = 'resources/blank.html'; + +// "installing" is set +promise_test(async t => { + + t.add_cleanup(async() => { + if (frame) + frame.remove(); + if (registration) + await registration.unregister(); + }); + + await service_worker_unregister(t, SCOPE); + const frame = await with_iframe(SCOPE); + const registration = + await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE}); + const container = frame.contentWindow.navigator.serviceWorker; + assert_equals(container.controller, null, 'controller'); + assert_equals(registration.active, null, 'registration.active'); + assert_equals(registration.waiting, null, 'registration.waiting'); + assert_equals(registration.installing.scriptURL, normalizeURL(SCRIPT), + 'registration.installing.scriptURL'); + // FIXME: Add a test for a frame created after installation. + // Should the existing frame ("frame") block activation? +}, 'installing is set'); + +// Tests that The ServiceWorker objects returned from installing attribute getter +// that represent the same service worker are the same objects. +promise_test(async t => { + const registration1 = + await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + const registration2 = await navigator.serviceWorker.getRegistration(SCOPE); + assert_equals(registration1.installing, registration2.installing, + 'ServiceWorkerRegistration.installing should return the ' + + 'same object'); + await registration1.unregister(); +}, 'The ServiceWorker objects returned from installing attribute getter that ' + + 'represent the same service worker are the same objects'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/interface-requirements-sw.https.html b/testing/web-platform/tests/service-workers/service-worker/interface-requirements-sw.https.html new file mode 100644 index 0000000000..eef868c889 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/interface-requirements-sw.https.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<title>Service Worker Global Scope Interfaces</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +// interface-requirements-worker.sub.js checks additional interface +// requirements, on top of the basic IDL that is validated in +// service-workers/idlharness.any.js +service_worker_test( + 'resources/interface-requirements-worker.sub.js', + 'Interfaces and attributes in ServiceWorkerGlobalScope'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/invalid-blobtype.https.html b/testing/web-platform/tests/service-workers/service-worker/invalid-blobtype.https.html new file mode 100644 index 0000000000..1c5920fb03 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/invalid-blobtype.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<title>Service Worker: respondWith with header value containing a null byte</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(function(t) { + var SCOPE = 'resources/invalid-blobtype-iframe.https.html'; + var SCRIPT = 'resources/invalid-blobtype-worker.js'; + var host_info = get_host_info(); + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, SCOPE); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(function(frame) { + t.add_cleanup(function() { + frame.remove(); + }); + + var channel = new MessageChannel(); + var onMsg = new Promise(function(resolve) { + channel.port1.onmessage = resolve; + }); + + frame.contentWindow.postMessage({}, + host_info['HTTPS_ORIGIN'], + [channel.port2]); + return onMsg; + }) + .then(function(e) { + assert_equals(e.data.results, 'finish'); + }); + }, 'Verify the response of FetchEvent using XMLHttpRequest'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/invalid-header.https.html b/testing/web-platform/tests/service-workers/service-worker/invalid-header.https.html new file mode 100644 index 0000000000..1bc9769790 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/invalid-header.https.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<title>Service Worker: respondWith with header value containing a null byte</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(function(t) { + var SCOPE = 'resources/invalid-header-iframe.https.html'; + var SCRIPT = 'resources/invalid-header-worker.js'; + var host_info = get_host_info(); + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, SCOPE); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(function(frame) { + t.add_cleanup(function() { + frame.remove(); + }); + + var channel = new MessageChannel(); + var onMsg = new Promise(function(resolve) { + channel.port1.onmessage = resolve; + }); + frame.contentWindow.postMessage({}, + host_info['HTTPS_ORIGIN'], + [channel.port2]); + return onMsg; + }) + .then(function(e) { + assert_equals(e.data.results, 'finish'); + }); + }, 'Verify the response of FetchEvent using XMLHttpRequest'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/iso-latin1-header.https.html b/testing/web-platform/tests/service-workers/service-worker/iso-latin1-header.https.html new file mode 100644 index 0000000000..c27a5f48a5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/iso-latin1-header.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<title>Service Worker: respondWith with header value containing an ISO Latin 1 (ISO-8859-1 Character Set) string</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(function(t) { + var SCOPE = 'resources/iso-latin1-header-iframe.html'; + var SCRIPT = 'resources/iso-latin1-header-worker.js'; + var host_info = get_host_info(); + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, SCOPE); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(function(frame) { + var channel = new MessageChannel(); + t.add_cleanup(function() { + frame.remove(); + }); + + var onMsg = new Promise(function(resolve) { + channel.port1.onmessage = resolve; + }); + + frame.contentWindow.postMessage({}, + host_info['HTTPS_ORIGIN'], + [channel.port2]); + return onMsg; + }) + .then(function(e) { + assert_equals(e.data.results, 'finish'); + }); + }, 'Verify the response of FetchEvent using XMLHttpRequest'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/local-url-inherit-controller.https.html b/testing/web-platform/tests/service-workers/service-worker/local-url-inherit-controller.https.html new file mode 100644 index 0000000000..6702abcadb --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/local-url-inherit-controller.https.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<title>Service Worker: local URL windows and workers inherit controller</title> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +const SCRIPT = 'resources/local-url-inherit-controller-worker.js'; +const SCOPE = 'resources/local-url-inherit-controller-frame.html'; + +async function doAsyncTest(t, opts) { + let name = `${opts.scheme}-${opts.child}-${opts.check}`; + let scope = SCOPE + '?name=' + name; + let reg = await service_worker_unregister_and_register(t, SCRIPT, scope); + add_completion_callback(_ => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + let frame = await with_iframe(scope); + add_completion_callback(_ => frame.remove()); + assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null, + 'frame should be controlled'); + + let result = await frame.contentWindow.checkChildController(opts); + result = result.data; + + let expect = 'unexpected'; + if (opts.check === 'controller') { + expect = opts.expect === 'inherit' + ? frame.contentWindow.navigator.serviceWorker.controller.scriptURL + : null; + } else if (opts.check === 'fetch') { + // The service worker FetchEvent handler will provide an "intercepted" + // body. If the local URL ends up with an opaque origin and is not + // intercepted then it will get an opaque Response. In that case it + // should see an empty string body. + expect = opts.expect === 'intercept' ? 'intercepted' : ''; + } + + assert_equals(result, expect, + `${opts.scheme} URL ${opts.child} should ${opts.expect} ${opts.check}`); +} + +promise_test(function(t) { + return doAsyncTest(t, { + scheme: 'blob', + child: 'iframe', + check: 'controller', + expect: 'inherit', + }); +}, 'Same-origin blob URL iframe should inherit service worker controller.'); + +promise_test(function(t) { + return doAsyncTest(t, { + scheme: 'blob', + child: 'iframe', + check: 'fetch', + expect: 'intercept', + }); +}, 'Same-origin blob URL iframe should intercept fetch().'); + +promise_test(function(t) { + return doAsyncTest(t, { + scheme: 'blob', + child: 'worker', + check: 'controller', + expect: 'inherit', + }); +}, 'Same-origin blob URL worker should inherit service worker controller.'); + +promise_test(function(t) { + return doAsyncTest(t, { + scheme: 'blob', + child: 'worker', + check: 'fetch', + expect: 'intercept', + }); +}, 'Same-origin blob URL worker should intercept fetch().'); + +promise_test(function(t) { + return doAsyncTest(t, { + scheme: 'data', + child: 'iframe', + check: 'fetch', + expect: 'not intercept', + }); +}, 'Data URL iframe should not intercept fetch().'); + +promise_test(function(t) { + // Data URLs should result in an opaque origin and should probably not + // have access to a cross-origin service worker. See: + // + // https://github.com/w3c/ServiceWorker/issues/1262 + // + return doAsyncTest(t, { + scheme: 'data', + child: 'worker', + check: 'controller', + expect: 'not inherit', + }); +}, 'Data URL worker should not inherit service worker controller.'); + +promise_test(function(t) { + return doAsyncTest(t, { + scheme: 'data', + child: 'worker', + check: 'fetch', + expect: 'not intercept', + }); +}, 'Data URL worker should not intercept fetch().'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/mime-sniffing.https.html b/testing/web-platform/tests/service-workers/service-worker/mime-sniffing.https.html new file mode 100644 index 0000000000..8175bcdf87 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/mime-sniffing.https.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<title>Service Worker: MIME sniffing</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(t => { + const SCOPE = 'resources/blank.html?mime-sniffing'; + const SCRIPT = 'resources/mime-sniffing-worker.js'; + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(registration => { + add_completion_callback(() => registration.unregister()); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(_ => with_iframe(SCOPE)) + .then(frame => { + add_completion_callback(() => frame.remove()); + assert_equals(frame.contentWindow.document.body.innerText, 'test'); + const h1 = frame.contentWindow.document.getElementById('testid'); + assert_equals(h1.innerText,'test'); + }); + }, 'The response from service worker should be correctly MIME siniffed.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/current.https.html b/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/current.https.html new file mode 100644 index 0000000000..82a48d4099 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/current.https.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<title>Current page used as a test helper</title> diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/test-sw.js b/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/test-sw.js new file mode 100644 index 0000000000..e673292f2c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/test-sw.js @@ -0,0 +1 @@ +// Service worker for current/
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html b/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html new file mode 100644 index 0000000000..4585f15b0f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<title>Incumbent page used as a test helper</title> + +<iframe src="../current/current.https.html" id="c"></iframe> +<iframe src="../relevant/relevant.https.html" id="r"></iframe> + +<script> +'use strict'; + +const current = document.querySelector('#c').contentWindow; +const relevant = document.querySelector('#r').contentWindow; + +window.testRegister = options => { + return current.navigator.serviceWorker.register.call(relevant.navigator.serviceWorker, 'test-sw.js', options); +}; + +window.testGetRegistration = () => { + return current.navigator.serviceWorker.getRegistration.call(relevant.navigator.serviceWorker, 'test-sw.js'); +}; +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js b/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js new file mode 100644 index 0000000000..e2a0e93b58 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js @@ -0,0 +1 @@ +// Service worker for incumbent/
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html b/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html new file mode 100644 index 0000000000..44f42eda49 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<title>Relevant page used as a test helper</title> diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js b/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js new file mode 100644 index 0000000000..ff44cdf086 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js @@ -0,0 +1 @@ +// Service worker for relevant/
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/test-sw.js b/testing/web-platform/tests/service-workers/service-worker/multi-globals/test-sw.js new file mode 100644 index 0000000000..ce3c940ece --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/test-sw.js @@ -0,0 +1 @@ +// Service worker for /
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/url-parsing.https.html b/testing/web-platform/tests/service-workers/service-worker/multi-globals/url-parsing.https.html new file mode 100644 index 0000000000..b9dfe36343 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/url-parsing.https.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<title>register()/getRegistration() URL parsing, with multiple globals in play</title> +<link rel="help" href="https://w3c.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-register-method"> +<link rel="help" href="https://w3c.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-getregistration-method"> +<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me"> +<script src="/resources/testharness.js"></script> +<script src="../resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> + +<!-- This is the entry global --> + +<iframe src="incumbent/incumbent.https.html"></iframe> + +<script> +'use strict'; + +const loadPromise = new Promise(resolve => { + window.addEventListener('load', () => resolve()); +}); + +promise_test(t => { + let registration; + + return loadPromise.then(() => { + return frames[0].testRegister(); + }).then(r => { + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }).then(_ => { + assert_equals(registration.active.scriptURL, normalizeURL('relevant/test-sw.js'), 'the script URL should be parsed against the relevant global'); + assert_equals(registration.scope, normalizeURL('relevant/'), 'the default scope URL should be parsed against the parsed script URL'); + + return registration.unregister(); + }); +}, 'register should use the relevant global of the object it was called on to resolve the script URL and the default scope URL'); + +promise_test(t => { + let registration; + + return loadPromise.then(() => { + return frames[0].testRegister({ scope: 'scope' }); + }).then(r => { + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }).then(_ => { + assert_equals(registration.active.scriptURL, normalizeURL('relevant/test-sw.js'), 'the script URL should be parsed against the relevant global'); + assert_equals(registration.scope, normalizeURL('relevant/scope'), 'the given scope URL should be parsed against the relevant global'); + + return registration.unregister(); + }); +}, 'register should use the relevant global of the object it was called on to resolve the script URL and the given scope URL'); + +promise_test(t => { + let registration; + + return loadPromise.then(() => { + return navigator.serviceWorker.register(normalizeURL('relevant/test-sw.js')); + }).then(r => { + registration = r; + return frames[0].testGetRegistration(); + }) + .then(gottenRegistration => { + assert_not_equals(registration, null, 'the registration should not be null'); + assert_not_equals(gottenRegistration, null, 'the registration from the other frame should not be null'); + assert_equals(gottenRegistration.scope, registration.scope, + 'the retrieved registration\'s scope should be equal to the original\'s scope'); + + return registration.unregister(); + }); +}, 'getRegistration should use the relevant global of the object it was called on to resolve the script URL'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/multipart-image.https.html b/testing/web-platform/tests/service-workers/service-worker/multipart-image.https.html new file mode 100644 index 0000000000..00c20d25f9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multipart-image.https.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<title>Tests for cross-origin multipart image returned by service worker</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> + +<script> +// This tests loading a multipart image via service worker. The service worker responds with +// an opaque or a non-opaque response. The content of opaque response should not be readable. + +const script = 'resources/multipart-image-worker.js'; +const scope = 'resources/multipart-image-iframe.html'; +let frame; + +function check_image_data(data) { + assert_equals(data[0], 255); + assert_equals(data[1], 0); + assert_equals(data[2], 0); + assert_equals(data[3], 255); +} + +promise_test(t => { + return service_worker_unregister_and_register(t, script, scope) + .then(registration => { + promise_test(() => { + if (frame) { + frame.remove(); + } + return registration.unregister(); + }, 'restore global state'); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => with_iframe(scope)) + .then(f => { + frame = f; + }); + }, 'initialize global state'); + +promise_test(t => { + return frame.contentWindow.load_multipart_image('same-origin-multipart-image') + .then(img => frame.contentWindow.get_image_data(img)) + .then(img_data => { + check_image_data(img_data.data); + }); + }, 'same-origin multipart image via SW should be readable'); + +promise_test(t => { + return frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-cors-approved') + .then(img => frame.contentWindow.get_image_data(img)) + .then(img_data => { + check_image_data(img_data.data); + }); + }, 'cross-origin multipart image via SW with approved CORS should be readable'); + +promise_test(t => { + return frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-no-cors') + .then(img => { + assert_throws_dom('SecurityError', frame.contentWindow.DOMException, + () => frame.contentWindow.get_image_data(img)); + }); + }, 'cross-origin multipart image with no-cors via SW should not be readable'); + +promise_test(t => { + const promise = frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-cors-rejected'); + return promise_rejects_dom(t, 'NetworkError', frame.contentWindow.DOMException, promise); + }, 'cross-origin multipart image via SW with rejected CORS should fail to load'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/multiple-register.https.html b/testing/web-platform/tests/service-workers/service-worker/multiple-register.https.html new file mode 100644 index 0000000000..752e132fc1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multiple-register.https.html @@ -0,0 +1,117 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +var worker_url = 'resources/empty-worker.js'; + +async_test(function(t) { + var scope = 'resources/scope/subsequent-register-from-same-window'; + var registration; + + service_worker_unregister_and_register(t, worker_url, scope) + .then(function(r) { + registration = r; + return wait_for_state(t, r.installing, 'activated'); + }) + .then(function() { + return navigator.serviceWorker.register(worker_url, { scope: scope }); + }) + .then(function(new_registration) { + assert_equals(new_registration, registration, + 'register should resolve to the same registration'); + assert_equals(new_registration.active, registration.active, + 'register should resolve to the same worker'); + assert_equals(new_registration.active.state, 'activated', + 'the worker should be in state "activated"'); + return registration.unregister(); + }) + .then(function() { t.done(); }) + .catch(unreached_rejection(t)); +}, 'Subsequent registrations resolve to the same registration object'); + +async_test(function(t) { + var scope = 'resources/scope/subsequent-register-from-different-iframe'; + var frame; + var registration; + + service_worker_unregister_and_register(t, worker_url, scope) + .then(function(r) { + registration = r; + return wait_for_state(t, r.installing, 'activated'); + }) + .then(function() { return with_iframe('resources/404.py'); }) + .then(function(f) { + frame = f; + return frame.contentWindow.navigator.serviceWorker.register( + 'empty-worker.js', + { scope: 'scope/subsequent-register-from-different-iframe' }); + }) + .then(function(new_registration) { + assert_not_equals( + registration, new_registration, + 'register should resolve to a different registration'); + assert_equals( + registration.scope, new_registration.scope, + 'registrations should have the same scope'); + + assert_equals( + registration.installing, null, + 'installing worker should be null'); + assert_equals( + new_registration.installing, null, + 'installing worker should be null'); + assert_equals( + registration.waiting, null, + 'waiting worker should be null') + assert_equals( + new_registration.waiting, null, + 'waiting worker should be null') + + assert_not_equals( + registration.active, new_registration.active, + 'registration should have a different active worker'); + assert_equals( + registration.active.scriptURL, + new_registration.active.scriptURL, + 'active workers should have the same script URL'); + assert_equals( + registration.active.state, + new_registration.active.state, + 'active workers should be in the same state'); + + frame.remove(); + return registration.unregister(); + }) + .then(function() { t.done(); }) + .catch(unreached_rejection(t)); +}, 'Subsequent registrations from a different iframe resolve to the ' + + 'different registration object but they refer to the same ' + + 'registration and workers'); + +async_test(function(t) { + var scope = 'resources/scope/concurrent-register'; + + service_worker_unregister(t, scope) + .then(function() { + var promises = []; + for (var i = 0; i < 10; ++i) { + promises.push(navigator.serviceWorker.register(worker_url, + { scope: scope })); + } + return Promise.all(promises); + }) + .then(function(registrations) { + registrations.forEach(function(registration) { + assert_equals(registration, registrations[0], + 'register should resolve to the same registration'); + }); + return registrations[0].unregister(); + }) + .then(function() { t.done(); }) + .catch(unreached_rejection(t)); +}, 'Concurrent registrations resolve to the same registration object'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/multiple-update.https.html b/testing/web-platform/tests/service-workers/service-worker/multiple-update.https.html new file mode 100644 index 0000000000..6a83f73a05 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/multiple-update.https.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<!-- In Bug 1217367, we will try to merge update events for same registration + if possible. This testcase is used to make sure the optimization algorithm + doesn't go wrong. --> +<title>Service Worker: Trigger multiple updates</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(function(t) { + var script = 'resources/update-nocookie-worker.py'; + var scope = 'resources/scope/update'; + var expected_url = normalizeURL(script); + var registration; + + return service_worker_unregister_and_register(t, expected_url, scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + // Test single update works before triggering multiple update events + return Promise.all([registration.update(), + wait_for_update(t, registration)]); + }) + .then(function() { + assert_equals(registration.installing.scriptURL, expected_url, + 'new installing should be set after update resolves.'); + assert_equals(registration.waiting, null, + 'waiting should still be null after update resolves.'); + assert_equals(registration.active.scriptURL, expected_url, + 'active should still exist after update found.'); + return wait_for_state(t, registration.installing, 'installed'); + }) + .then(function() { + assert_equals(registration.installing, null, + 'installing should be null after installing.'); + if (registration.waiting) { + assert_equals(registration.waiting.scriptURL, expected_url, + 'waiting should be set after installing.'); + assert_equals(registration.active.scriptURL, expected_url, + 'active should still exist after installing.'); + return wait_for_state(t, registration.waiting, 'activated'); + } + }) + .then(function() { + // Test triggering multiple update events at the same time. + var promiseList = []; + const burstUpdateCount = 10; + for (var i = 0; i < burstUpdateCount; i++) { + promiseList.push(registration.update()); + } + promiseList.push(wait_for_update(t, registration)); + return Promise.all(promiseList); + }) + .then(function() { + assert_equals(registration.installing.scriptURL, expected_url, + 'new installing should be set after update resolves.'); + assert_equals(registration.waiting, null, + 'waiting should still be null after update resolves.'); + assert_equals(registration.active.scriptURL, expected_url, + 'active should still exist after update found.'); + return wait_for_state(t, registration.installing, 'installed'); + }) + .then(function() { + assert_equals(registration.installing, null, + 'installing should be null after installing.'); + if (registration.waiting) { + assert_equals(registration.waiting.scriptURL, expected_url, + 'waiting should be set after installing.'); + assert_equals(registration.active.scriptURL, expected_url, + 'active should still exist after installing.'); + return wait_for_state(t, registration.waiting, 'activated'); + } + }) + .then(function() { + // Test update still works after handling update event burst. + return Promise.all([registration.update(), + wait_for_update(t, registration)]); + }) + .then(function() { + assert_equals(registration.installing.scriptURL, expected_url, + 'new installing should be set after update resolves.'); + assert_equals(registration.waiting, null, + 'waiting should be null after activated.'); + assert_equals(registration.active.scriptURL, expected_url, + 'active should still exist after update found.'); + }); + }, 'Trigger multiple updates.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigate-window.https.html b/testing/web-platform/tests/service-workers/service-worker/navigate-window.https.html new file mode 100644 index 0000000000..46d32a48a0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigate-window.https.html @@ -0,0 +1,151 @@ +<!DOCTYPE html> +<title>Service Worker: Navigate a Window</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +var host_info = get_host_info(); +var BASE_URL = host_info['HTTPS_ORIGIN'] + base_path(); + +function wait_for_message(msg) { + return new Promise(function(resolve, reject) { + window.addEventListener('message', function onMsg(evt) { + if (evt.data.type === msg) { + resolve(); + } + }); + }); +} + +function with_window(url) { + var win = window.open(url); + return wait_for_message('LOADED').then(_ => win); +} + +function navigate_window(win, url) { + win.location = url; + return wait_for_message('LOADED').then(_ => win); +} + +function reload_window(win) { + win.location.reload(); + return wait_for_message('LOADED').then(_ => win); +} + +function go_back(win) { + win.history.back(); + return wait_for_message('PAGESHOW').then(_ => win); +} + +function go_forward(win) { + win.history.forward(); + return wait_for_message('PAGESHOW').then(_ => win); +} + +function get_clients(win, sw, opts) { + return new Promise((resolve, reject) => { + win.navigator.serviceWorker.addEventListener('message', function onMsg(evt) { + win.navigator.serviceWorker.removeEventListener('message', onMsg); + if (evt.data.type === 'success') { + resolve(evt.data.detail); + } else { + reject(evt.data.detail); + } + }); + sw.postMessage({ type: 'GET_CLIENTS', opts: (opts || {}) }); + }); +} + +function compare_urls(a, b) { + return a.url < b.url ? -1 : b.url < a.url ? 1 : 0; +} + +function validate_window(win, url, opts) { + return win.navigator.serviceWorker.getRegistration(url) + .then(reg => { + // In order to compare service worker instances we need to + // make sure the DOM object is owned by the same global; the + // opened window in this case. + assert_equals(win.navigator.serviceWorker.controller, reg.active, + 'window should be controlled by service worker'); + return get_clients(win, reg.active, opts); + }) + .then(resultList => { + // We should always see our controlled window. + var expected = [ + { url: url, frameType: 'auxiliary' } + ]; + // If we are including uncontrolled windows, then we might see the + // test window itself and the test harness. + if (opts.includeUncontrolled) { + expected.push({ url: BASE_URL + 'navigate-window.https.html', + frameType: 'auxiliary' }); + expected.push({ + url: host_info['HTTPS_ORIGIN'] + '/testharness_runner.html', + frameType: 'top-level' }); + } + + assert_equals(resultList.length, expected.length, + 'expected number of clients'); + + expected.sort(compare_urls); + resultList.sort(compare_urls); + + for (var i = 0; i < resultList.length; ++i) { + assert_equals(resultList[i].url, expected[i].url, + 'client should have expected url'); + assert_equals(resultList[i].frameType, expected[i].frameType, + 'client should have expected frame type'); + } + return win; + }) +} + +promise_test(function(t) { + var worker = BASE_URL + 'resources/navigate-window-worker.js'; + var scope = BASE_URL + 'resources/loaded.html?navigate-window-controlled'; + var url1 = scope + '&q=1'; + var url2 = scope + '&q=2'; + return service_worker_unregister_and_register(t, worker, scope) + .then(reg => wait_for_state(t, reg.installing, 'activated') ) + .then(___ => with_window(url1)) + .then(win => validate_window(win, url1, { includeUncontrolled: false })) + .then(win => navigate_window(win, url2)) + .then(win => validate_window(win, url2, { includeUncontrolled: false })) + .then(win => go_back(win)) + .then(win => validate_window(win, url1, { includeUncontrolled: false })) + .then(win => go_forward(win)) + .then(win => validate_window(win, url2, { includeUncontrolled: false })) + .then(win => reload_window(win)) + .then(win => validate_window(win, url2, { includeUncontrolled: false })) + .then(win => win.close()) + .catch(unreached_rejection(t)) + .then(___ => service_worker_unregister(t, scope)) + }, 'Clients.matchAll() should not show an old window as controlled after ' + + 'it navigates.'); + +promise_test(function(t) { + var worker = BASE_URL + 'resources/navigate-window-worker.js'; + var scope = BASE_URL + 'resources/loaded.html?navigate-window-uncontrolled'; + var url1 = scope + '&q=1'; + var url2 = scope + '&q=2'; + return service_worker_unregister_and_register(t, worker, scope) + .then(reg => wait_for_state(t, reg.installing, 'activated') ) + .then(___ => with_window(url1)) + .then(win => validate_window(win, url1, { includeUncontrolled: true })) + .then(win => navigate_window(win, url2)) + .then(win => validate_window(win, url2, { includeUncontrolled: true })) + .then(win => go_back(win)) + .then(win => validate_window(win, url1, { includeUncontrolled: true })) + .then(win => go_forward(win)) + .then(win => validate_window(win, url2, { includeUncontrolled: true })) + .then(win => reload_window(win)) + .then(win => validate_window(win, url2, { includeUncontrolled: true })) + .then(win => win.close()) + .catch(unreached_rejection(t)) + .then(___ => service_worker_unregister(t, scope)) + }, 'Clients.matchAll() should not show an old window after it navigates.'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-headers.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-headers.https.html new file mode 100644 index 0000000000..a4b52035e2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-headers.https.html @@ -0,0 +1,819 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<meta name="timeout" content="long"> +<title>Service Worker: Navigation Post Request Origin Header</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<body> +<script> +'use strict'; + +const script = new URL('./resources/fetch-rewrite-worker.js', self.location); +const base = './resources/navigation-headers-server.py'; +const scope = base + '?with-sw'; +let registration; + +async function post_and_get_headers(t, form_host, method, swaction, + redirect_hosts=[]) { + if (swaction === 'navpreload') { + assert_true('navigationPreload' in registration, + 'navigation preload must be supported'); + } + let target_string; + if (swaction === 'no-sw') { + target_string = base + '?no-sw'; + } else if (swaction === 'fallback') { + target_string = `${scope}&ignore`; + } else { + target_string = `${scope}&${swaction}`; + } + let target = new URL(target_string, self.location); + + for (let i = redirect_hosts.length - 1; i >= 0; --i) { + const redirect_url = new URL('./resources/redirect.py', self.location); + redirect_url.hostname = redirect_hosts[i]; + redirect_url.search = `?Status=307&Redirect=${encodeURIComponent(target)}`; + target = redirect_url; + } + + let popup_url_path; + if (method === 'GET') { + popup_url_path = './resources/location-setter.html'; + } else if (method === 'POST') { + popup_url_path = './resources/form-poster.html'; + } + + const popup_url = new URL(popup_url_path, self.location); + popup_url.hostname = form_host; + popup_url.search = `?target=${encodeURIComponent(target.href)}`; + + const message_promise = new Promise(resolve => { + self.addEventListener('message', evt => { + resolve(evt.data); + }); + }); + + const frame = await with_iframe(popup_url); + t.add_cleanup(() => frame.remove()); + + return await message_promise; +} + +const SAME_ORIGIN = new URL(self.location.origin); +const SAME_SITE = new URL(get_host_info().HTTPS_REMOTE_ORIGIN); +const CROSS_SITE = new URL(get_host_info().HTTPS_NOTSAMESITE_ORIGIN); + +promise_test(async t => { + registration = await service_worker_unregister_and_register(t, script, scope); + await wait_for_state(t, registration.installing, 'activated'); + if (registration.navigationPreload) + await registration.navigationPreload.enable(); +}, 'Setup service worker'); + +// +// Origin and referer headers +// + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'no-sw'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'GET Navigation, same-origin with no service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'no-sw'); + assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with no service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'passthrough'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'GET Navigation, same-origin with passthrough service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'passthrough'); + assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with passthrough service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'fallback'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'GET Navigation, same-origin with fallback service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'fallback'); + assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with fallback service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'navpreload'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'GET Navigation, same-origin with navpreload service worker sets correct ' + + 'origin and referer headers.'); + +// There is no POST test for navpreload since the feature only supports GET +// requests. + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'change-request'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, script.href, 'referer header'); +}, 'GET Navigation, same-origin with service worker that changes the ' + + 'request sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'change-request'); + assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header'); + assert_equals(result.referer, script.href, 'referer header'); +}, 'POST Navigation, same-origin with service worker that changes the ' + + 'request sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET', + 'no-sw'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, SAME_SITE.href, 'referer header'); +}, 'GET Navigation, same-site with no service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST', + 'no-sw'); + assert_equals(result.origin, SAME_SITE.origin, 'origin header'); + assert_equals(result.referer, SAME_SITE.href, 'referer header'); +}, 'POST Navigation, same-site with no service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET', + 'passthrough'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, SAME_SITE.href, 'referer header'); +}, 'GET Navigation, same-site with passthrough service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST', + 'passthrough'); + assert_equals(result.origin, SAME_SITE.origin, 'origin header'); + assert_equals(result.referer, SAME_SITE.href, 'referer header'); +}, 'POST Navigation, same-site with passthrough service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET', + 'fallback'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, SAME_SITE.href, 'referer header'); +}, 'GET Navigation, same-site with fallback service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST', + 'fallback'); + assert_equals(result.origin, SAME_SITE.origin, 'origin header'); + assert_equals(result.referer, SAME_SITE.href, 'referer header'); +}, 'POST Navigation, same-site with fallback service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET', + 'navpreload'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, SAME_SITE.href, 'referer header'); +}, 'GET Navigation, same-site with navpreload service worker sets correct ' + + 'origin and referer headers.'); + +// There is no POST test for navpreload since the feature only supports GET +// requests. + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET', + 'change-request'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, script.href, 'referer header'); +}, 'GET Navigation, same-site with service worker that changes the ' + + 'request sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST', + 'change-request'); + assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header'); + assert_equals(result.referer, script.href, 'referer header'); +}, 'POST Navigation, same-site with service worker that changes the ' + + 'request sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET', + 'no-sw'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, CROSS_SITE.href, 'referer header'); +}, 'GET Navigation, cross-site with no service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST', + 'no-sw'); + assert_equals(result.origin, CROSS_SITE.origin, 'origin header'); + assert_equals(result.referer, CROSS_SITE.href, 'referer header'); +}, 'POST Navigation, cross-site with no service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET', + 'passthrough'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, CROSS_SITE.href, 'referer header'); +}, 'GET Navigation, cross-site with passthrough service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST', + 'passthrough'); + assert_equals(result.origin, CROSS_SITE.origin, 'origin header'); + assert_equals(result.referer, CROSS_SITE.href, 'referer header'); +}, 'POST Navigation, cross-site with passthrough service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET', + 'fallback'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, CROSS_SITE.href, 'referer header'); +}, 'GET Navigation, cross-site with fallback service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST', + 'fallback'); + assert_equals(result.origin, CROSS_SITE.origin, 'origin header'); + assert_equals(result.referer, CROSS_SITE.href, 'referer header'); +}, 'POST Navigation, cross-site with fallback service worker sets correct ' + + 'origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET', + 'navpreload'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, CROSS_SITE.href, 'referer header'); +}, 'GET Navigation, cross-site with navpreload service worker sets correct ' + + 'origin and referer headers.'); + +// There is no POST test for navpreload since the feature only supports GET +// requests. + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET', + 'change-request'); + assert_equals(result.origin, 'not set', 'origin header'); + assert_equals(result.referer, script.href, 'referer header'); +}, 'GET Navigation, cross-site with service worker that changes the ' + + 'request sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST', + 'change-request'); + assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header'); + assert_equals(result.referer, script.href, 'referer header'); +}, 'POST Navigation, cross-site with service worker that changes the ' + + 'request sets correct origin and referer headers.'); + +// +// Origin and referer header tests using redirects +// + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'no-sw', [SAME_SITE.hostname]); + assert_equals(result.origin, 'null', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with same-site redirect and no service worker ' + + 'sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'passthrough', [SAME_SITE.hostname]); + assert_equals(result.origin, 'null', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with same-site redirect and passthrough service ' + + 'worker sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'fallback', [SAME_SITE.hostname]); + assert_equals(result.origin, 'null', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with same-site redirect and fallback service ' + + 'worker sets correct origin and referer headers.'); + +// There is no navpreload case because it does not work with POST requests. + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'change-request', [SAME_SITE.hostname]); + assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header'); + assert_equals(result.referer, script.href, 'referer header'); +}, 'POST Navigation, same-origin with same-site redirect and change-request service ' + + 'worker sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'no-sw', [CROSS_SITE.hostname]); + assert_equals(result.origin, 'null', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with cross-site redirect and no service worker ' + + 'sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'passthrough', [CROSS_SITE.hostname]); + assert_equals(result.origin, 'null', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with cross-site redirect and passthrough service ' + + 'worker sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'fallback', [CROSS_SITE.hostname]); + assert_equals(result.origin, 'null', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with cross-site redirect and fallback service ' + + 'worker sets correct origin and referer headers.'); + +// There is no navpreload case because it does not work with POST requests. + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'change-request', [CROSS_SITE.hostname]); + assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header'); + assert_equals(result.referer, script.href, 'referer header'); +}, 'POST Navigation, same-origin with cross-site redirect and change-request service ' + + 'worker sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'no-sw', [CROSS_SITE.hostname, + SAME_ORIGIN.hostname]); + assert_equals(result.origin, 'null', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' + + 'and no service worker sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'passthrough', [CROSS_SITE.hostname, + SAME_ORIGIN.hostname]); + assert_equals(result.origin, 'null', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' + + 'and passthrough service worker sets correct origin and referer headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'fallback', [CROSS_SITE.hostname, + SAME_ORIGIN.hostname]); + assert_equals(result.origin, 'null', 'origin header'); + assert_equals(result.referer, SAME_ORIGIN.href, 'referer header'); +}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' + + 'and fallback service worker sets correct origin and referer headers.'); + +// There is no navpreload case because it does not work with POST requests. + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'change-request', [CROSS_SITE.hostname, + SAME_ORIGIN.hostname]); + assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header'); + assert_equals(result.referer, script.href, 'referer header'); +}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' + + 'and change-request service worker sets correct origin and referer headers.'); + +// +// Sec-Fetch-* Headers (separated since not all browsers implement them) +// + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'no-sw'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with no service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'no-sw'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'POST Navigation, same-origin with no service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'passthrough'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with passthrough service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'passthrough'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'POST Navigation, same-origin with passthrough service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'fallback'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with fallback service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'fallback'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'POST Navigation, same-origin with fallback service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'navpreload'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with navpreload service worker sets correct ' + + 'sec-fetch headers.'); + +// There is no POST test for navpreload since the feature only supports GET +// requests. + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'change-request'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with service worker that changes the ' + + 'request sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST', + 'change-request'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'POST Navigation, same-origin with service worker that changes the ' + + 'request sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET', + 'no-sw'); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-site with no service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST', + 'no-sw'); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'POST Navigation, same-site with no service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET', + 'passthrough'); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, same-site with passthrough service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST', + 'passthrough'); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'POST Navigation, same-site with passthrough service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET', + 'fallback'); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-site with fallback service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST', + 'fallback'); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'POST Navigation, same-site with fallback service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET', + 'navpreload'); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-site with navpreload service worker sets correct ' + + 'sec-fetch headers.'); + +// There is no POST test for navpreload since the feature only supports GET +// requests. + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET', + 'change-request'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, same-site with service worker that changes the ' + + 'request sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST', + 'change-request'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'POST Navigation, same-site with service worker that changes the ' + + 'request sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET', + 'no-sw'); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, cross-site with no service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST', + 'no-sw'); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'POST Navigation, cross-site with no service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET', + 'passthrough'); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, cross-site with passthrough service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST', + 'passthrough'); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'POST Navigation, cross-site with passthrough service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET', + 'fallback'); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, cross-site with fallback service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST', + 'fallback'); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'POST Navigation, cross-site with fallback service worker sets correct ' + + 'sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET', + 'navpreload'); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, cross-site with navpreload service worker sets correct ' + + 'sec-fetch headers.'); + +// There is no POST test for navpreload since the feature only supports GET +// requests. + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET', + 'change-request'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, cross-site with service worker that changes the ' + + 'request sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST', + 'change-request'); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'POST Navigation, cross-site with service worker that changes the ' + + 'request sets correct sec-fetch headers.'); + +// +// Sec-Fetch-* header tests using redirects +// + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'no-sw', [SAME_SITE.hostname]); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with same-site redirect and no service worker ' + + 'sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'passthrough', [SAME_SITE.hostname]); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with same-site redirect and passthrough service ' + + 'worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'fallback', [SAME_SITE.hostname]); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with same-site redirect and fallback service ' + + 'worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'navpreload', [SAME_SITE.hostname]); + assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with same-site redirect and navpreload service ' + + 'worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'change-request', [SAME_SITE.hostname]); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with same-site redirect and change-request service ' + + 'worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'no-sw', [CROSS_SITE.hostname]); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with cross-site redirect and no service worker ' + + 'sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'passthrough', [CROSS_SITE.hostname]); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with cross-site redirect and passthrough service ' + + 'worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'fallback', [CROSS_SITE.hostname]); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with cross-site redirect and fallback service ' + + 'worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'navpreload', [CROSS_SITE.hostname]); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with cross-site redirect and navpreload service ' + + 'worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'change-request', [CROSS_SITE.hostname]); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with cross-site redirect and change-request service ' + + 'worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'no-sw', [CROSS_SITE.hostname, + SAME_ORIGIN.hostname]); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' + + 'and no service worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'passthrough', [CROSS_SITE.hostname, + SAME_ORIGIN.hostname]); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' + + 'and passthrough service worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'fallback', [CROSS_SITE.hostname, + SAME_ORIGIN.hostname]); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' + + 'and fallback service worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'navpreload', [CROSS_SITE.hostname, + SAME_ORIGIN.hostname]); + assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' + + 'and navpreload service worker sets correct sec-fetch headers.'); + +promise_test(async t => { + const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET', + 'change-request', [CROSS_SITE.hostname, + SAME_ORIGIN.hostname]); + assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header'); + assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header'); + assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header'); +}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' + + 'and change-request service worker sets correct sec-fetch headers.'); + +promise_test(async t => { + await registration.unregister(); +}, 'Cleanup service worker'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html new file mode 100644 index 0000000000..ec74282ac3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Navigation Preload with chunked encoding</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<script> +promise_test(t => { + var script = 'resources/broken-chunked-encoding-worker.js'; + var scope = 'resources/broken-chunked-encoding-scope.asis'; + return service_worker_unregister_and_register(t, script, scope) + .then(registration => { + add_completion_callback(_ => registration.unregister()); + var worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(_ => with_iframe(scope)) + .then(frame => { + assert_equals( + frame.contentDocument.body.textContent, + 'PASS: preloadResponse resolved'); + }); + }, 'FetchEvent#preloadResponse resolves even if the body is sent with broken chunked encoding.'); + +promise_test(t => { + var script = 'resources/broken-chunked-encoding-worker.js'; + var scope = 'resources/chunked-encoding-scope.py?use_broken_body'; + return service_worker_unregister_and_register(t, script, scope) + .then(registration => { + add_completion_callback(_ => registration.unregister()); + var worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(_ => with_iframe(scope)) + .then(frame => { + assert_equals( + frame.contentDocument.body.textContent, + 'PASS: preloadResponse resolved'); + }); + }, 'FetchEvent#preloadResponse resolves even if the body is sent with broken chunked encoding with some delays'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html new file mode 100644 index 0000000000..830ce32cea --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Navigation Preload with chunked encoding</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<script> +promise_test(t => { + var script = 'resources/chunked-encoding-worker.js'; + var scope = 'resources/chunked-encoding-scope.py'; + return service_worker_unregister_and_register(t, script, scope) + .then(registration => { + add_completion_callback(_ => registration.unregister()); + var worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(_ => with_iframe(scope)) + .then(frame => { + assert_equals( + frame.contentDocument.body.textContent, + '0123456789'); + }); + }, 'Navigation Preload must work with chunked encoding.'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html new file mode 100644 index 0000000000..7e8aacdd36 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Navigation Preload empty response body</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<script> +promise_test(t => { + var script = 'resources/empty-preload-response-body-worker.js'; + var scope = 'resources/empty-preload-response-body-scope.html'; + return service_worker_unregister_and_register(t, script, scope) + .then(registration => { + add_completion_callback(_ => registration.unregister()); + var worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(_ => with_iframe(scope)) + .then(frame => { + assert_equals( + frame.contentDocument.body.textContent, + '[]'); + }); + }, 'Navigation Preload empty response body.'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/get-state.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/get-state.https.html new file mode 100644 index 0000000000..08e2f4976c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/get-state.https.html @@ -0,0 +1,217 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>NavigationPreloadManager.getState</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<script src="resources/helpers.js"></script> +<body> +<script> +function post_and_wait_for_reply(worker, message) { + return new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { resolve(e.data); }; + worker.postMessage(message); + }); +} + +promise_test(t => { + const scope = '../resources/get-state'; + const script = '../resources/empty-worker.js'; + var np; + + return service_worker_unregister_and_register(t, script, scope) + .then(r => { + np = r.navigationPreload; + add_completion_callback(() => r.unregister()); + return wait_for_state(t, r.installing, 'activated'); + }) + .then(() => np.getState()) + .then(state => { + expect_navigation_preload_state(state, false, 'true', 'default state'); + return np.enable(); + }) + .then(result => { + assert_equals(result, undefined, + 'enable() should resolve to undefined'); + return np.getState(); + }) + .then(state => { + expect_navigation_preload_state(state, true, 'true', + 'state after enable()'); + return np.disable(); + }) + .then(result => { + assert_equals(result, undefined, + 'disable() should resolve to undefined'); + return np.getState(); + }) + .then(state => { + expect_navigation_preload_state(state, false, 'true', + 'state after disable()'); + return np.setHeaderValue('dreams that cannot be'); + }) + .then(result => { + assert_equals(result, undefined, + 'setHeaderValue() should resolve to undefined'); + return np.getState(); + }) + .then(state => { + expect_navigation_preload_state(state, false, 'dreams that cannot be', + 'state after setHeaderValue()'); + return np.setHeaderValue('').then(() => np.getState()); + }) + .then(state => { + expect_navigation_preload_state(state, false, '', + 'after setHeaderValue to empty string'); + return np.setHeaderValue(null).then(() => np.getState()); + }) + .then(state => { + expect_navigation_preload_state(state, false, 'null', + 'after setHeaderValue to null'); + return promise_rejects_js(t, + TypeError, + np.setHeaderValue('what\uDC00\uD800this'), + 'setHeaderValue() should throw if passed surrogates'); + }) + .then(() => { + return promise_rejects_js(t, + TypeError, + np.setHeaderValue('zer\0o'), + 'setHeaderValue() should throw if passed \\0'); + }) + .then(() => { + return promise_rejects_js(t, + TypeError, + np.setHeaderValue('\rcarriage'), + 'setHeaderValue() should throw if passed \\r'); + }) + .then(() => { + return promise_rejects_js(t, + TypeError, + np.setHeaderValue('newline\n'), + 'setHeaderValue() should throw if passed \\n'); + }) + .then(() => { + return promise_rejects_js(t, + TypeError, + np.setHeaderValue(), + 'setHeaderValue() should throw if passed undefined'); + }) + .then(() => np.enable().then(() => np.getState())) + .then(state => { + expect_navigation_preload_state(state, true, 'null', + 'enable() should not change header'); + }); + }, 'getState'); + +// This test sends commands to a worker to call enable()/disable()/getState(). +// It checks the results from the worker and verifies that they match the +// navigation preload state accessible from the page. +promise_test(t => { + const scope = 'resources/get-state-worker'; + const script = 'resources/get-state-worker.js'; + var worker; + var registration; + + return service_worker_unregister_and_register(t, script, scope) + .then(r => { + registration = r; + add_completion_callback(() => registration.unregister()); + worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(() => { + // Call getState(). + return post_and_wait_for_reply(worker, 'getState'); + }) + .then(data => { + return Promise.all([data, registration.navigationPreload.getState()]); + }) + .then(states => { + expect_navigation_preload_state(states[0], false, 'true', + 'default state (from worker)'); + expect_navigation_preload_state(states[1], false, 'true', + 'default state (from page)'); + // Call enable() and then getState(). + return post_and_wait_for_reply(worker, 'enable'); + }) + .then(data => { + assert_equals(data, undefined, 'enable() should resolve to undefined'); + return Promise.all([ + post_and_wait_for_reply(worker, 'getState'), + registration.navigationPreload.getState() + ]); + }) + .then(states => { + expect_navigation_preload_state(states[0], true, 'true', + 'state after enable() (from worker)'); + expect_navigation_preload_state(states[1], true, 'true', + 'state after enable() (from page)'); + // Call disable() and then getState(). + return post_and_wait_for_reply(worker, 'disable'); + }) + .then(data => { + assert_equals(data, undefined, + '.disable() should resolve to undefined'); + return Promise.all([ + post_and_wait_for_reply(worker, 'getState'), + registration.navigationPreload.getState() + ]); + }) + .then(states => { + expect_navigation_preload_state(states[0], false, 'true', + 'state after disable() (from worker)'); + expect_navigation_preload_state(states[1], false, 'true', + 'state after disable() (from page)'); + return post_and_wait_for_reply(worker, 'setHeaderValue'); + }) + .then(data => { + assert_equals(data, undefined, + '.setHeaderValue() should resolve to undefined'); + return Promise.all([ + post_and_wait_for_reply(worker, 'getState'), + registration.navigationPreload.getState()]); + }) + .then(states => { + expect_navigation_preload_state( + states[0], false, 'insightful', + 'state after setHeaderValue() (from worker)'); + expect_navigation_preload_state( + states[1], false, 'insightful', + 'state after setHeaderValue() (from page)'); + }); + }, 'getState from a worker'); + +// This tests navigation preload API when there is no active worker. It calls +// the API from the main page and then from the worker itself. +promise_test(t => { + const scope = 'resources/wait-for-activate-worker'; + const script = 'resources/wait-for-activate-worker.js'; + var registration; + var np; + return service_worker_unregister_and_register(t, script, scope) + .then(r => { + registration = r; + np = registration.navigationPreload; + add_completion_callback(() => registration.unregister()); + return Promise.all([ + promise_rejects_dom( + t, 'InvalidStateError', np.enable(), + 'enable should reject if there is no active worker'), + promise_rejects_dom( + t, 'InvalidStateError', np.disable(), + 'disable should reject if there is no active worker'), + promise_rejects_dom( + t, 'InvalidStateError', np.setHeaderValue('umm'), + 'setHeaderValue should reject if there is no active worker')]); + }) + .then(() => np.getState()) + .then(state => { + expect_navigation_preload_state(state, false, 'true', + 'state before activation'); + return post_and_wait_for_reply(registration.installing, 'ping'); + }) + .then(result => assert_equals(result, 'PASS')); + }, 'no active worker'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html new file mode 100644 index 0000000000..392e5c14dc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>ServiceWorker: navigator.serviceWorker.navigationPreload</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<script src="resources/helpers.js"></script> +<script> +promise_test(async t => { + const SCRIPT = '../resources/empty-worker.js'; + const SCOPE = '../resources/navigationpreload'; + const registration = + await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + const navigationPreload = registration.navigationPreload; + assert_true(navigationPreload instanceof NavigationPreloadManager, + 'ServiceWorkerRegistration.navigationPreload'); + await registration.unregister(); +}, "The navigationPreload attribute must return service worker " + + "registration's NavigationPreloadManager object."); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/redirect.https.html new file mode 100644 index 0000000000..5970f053e3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/redirect.https.html @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Navigation Preload redirect response</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<script> + +function check_opaqueredirect(response_info, scope) { + assert_equals(response_info.type, 'opaqueredirect'); + assert_equals(response_info.url, '' + new URL(scope, location)); + assert_equals(response_info.status, 0); + assert_equals(response_info.ok, false); + assert_equals(response_info.statusText, ''); + assert_equals(response_info.headers.length, 0); +} + +function redirect_response_test(t, scope, expected_body, expected_urls) { + var script = 'resources/redirect-worker.js'; + var registration; + var message_resolvers = []; + function wait_for_message(count) { + var promises = []; + message_resolvers = []; + for (var i = 0; i < count; ++i) { + promises.push(new Promise(resolve => message_resolvers.push(resolve))); + } + return promises; + } + function on_message(e) { + var resolve = message_resolvers.shift(); + if (resolve) + resolve(e.data); + } + return service_worker_unregister_and_register(t, script, scope) + .then(reg => { + registration = reg; + add_completion_callback(_ => registration.unregister()); + var worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(_ => with_iframe(scope + '&base')) + .then(frame => { + assert_equals(frame.contentDocument.body.textContent, 'OK'); + frame.contentWindow.navigator.serviceWorker.onmessage = on_message; + return Promise.all(wait_for_message(expected_urls.length) + .concat(with_iframe(scope))); + }) + .then(results => { + var frame = results[expected_urls.length]; + assert_equals(frame.contentDocument.body.textContent, expected_body); + for (var i = 0; i < expected_urls.length; ++i) { + check_opaqueredirect(results[i], expected_urls[i]); + } + frame.remove(); + return registration.unregister(); + }); +} + +promise_test(t => { + return redirect_response_test( + t, + 'resources/redirect-scope.py?type=normal', + 'redirected\n', + ['resources/redirect-scope.py?type=normal']); + }, 'Navigation Preload redirect response.'); + +promise_test(t => { + return redirect_response_test( + t, + 'resources/redirect-scope.py?type=no-location', + '', + ['resources/redirect-scope.py?type=no-location']); + }, 'Navigation Preload no-location redirect response.'); + +promise_test(t => { + return redirect_response_test( + t, + 'resources/redirect-scope.py?type=no-location-with-body', + 'BODY', + ['resources/redirect-scope.py?type=no-location-with-body']); + }, 'Navigation Preload no-location redirect response with body.'); + +promise_test(t => { + return redirect_response_test( + t, + 'resources/redirect-scope.py?type=redirect-to-scope', + 'redirected\n', + ['resources/redirect-scope.py?type=redirect-to-scope', + 'resources/redirect-scope.py?type=redirect-to-scope2', + 'resources/redirect-scope.py?type=redirect-to-scope3',]); + }, 'Navigation Preload redirect to the same scope.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/request-headers.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/request-headers.https.html new file mode 100644 index 0000000000..0964201021 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/request-headers.https.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Navigation Preload request headers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<script> +promise_test(t => { + var script = 'resources/request-headers-worker.js'; + var scope = 'resources/request-headers-scope.py'; + return service_worker_unregister_and_register(t, script, scope) + .then(registration => { + add_completion_callback(_ => registration.unregister()); + var worker = registration.installing; + return wait_for_state(t, worker, 'activated'); + }) + .then(_ => with_iframe(scope)) + .then(frame => { + var headers = JSON.parse(frame.contentDocument.body.textContent); + assert_true( + 'SERVICE-WORKER-NAVIGATION-PRELOAD' in headers, + 'The Navigation Preload request must specify a ' + + '"Service-Worker-Navigation-Preload" header.'); + assert_array_equals( + headers['SERVICE-WORKER-NAVIGATION-PRELOAD'], + ['hello'], + 'The Navigation Preload request must specify the correct value ' + + 'for the "Service-Worker-Navigation-Preload" header.'); + assert_true( + 'UPGRADE-INSECURE-REQUESTS' in headers, + 'The Navigation Preload request must specify an ' + + '"Upgrade-Insecure-Requests" header.'); + assert_array_equals( + headers['UPGRADE-INSECURE-REQUESTS'], + ['1'], + 'The Navigation Preload request must specify the correct value ' + + 'for the "Upgrade-Insecure-Requests" header.'); + }); + }, 'Navigation Preload request headers.'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html new file mode 100644 index 0000000000..468a1f51e8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Navigation Preload Resource Timing</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<script> + +function check_timing_entry(entry, url, decodedBodySize, encodedBodySize) { + assert_equals(entry.name, url, 'The entry name of '+ url); + + assert_equals( + entry.entryType, 'resource', + 'The entryType of preload response timing entry must be "resource' + + '" :' + url); + assert_equals( + entry.initiatorType, 'navigation', + 'The initiatorType of preload response timing entry must be ' + + '"navigation":' + url); + + // If the server returns the redirect response, |decodedBodySize| is null and + // |entry.decodedBodySize| should be 0. Otherwise |entry.decodedBodySize| must + // same as |decodedBodySize| + assert_equals( + entry.decodedBodySize, Number(decodedBodySize), + 'decodedBodySize must same as the decoded size in the server:' + url); + + // If the server returns the redirect response, |encodedBodySize| is null and + // |entry.encodedBodySize| should be 0. Otherwise |entry.encodedBodySize| must + // same as |encodedBodySize| + assert_equals( + entry.encodedBodySize, Number(encodedBodySize), + 'encodedBodySize must same as the encoded size in the server:' + url); + + assert_greater_than( + entry.transferSize, entry.decodedBodySize, + 'transferSize must greater then encodedBodySize.'); + + assert_greater_than(entry.startTime, 0, 'startTime of ' + url); + assert_greater_than_equal(entry.fetchStart, entry.startTime, + 'fetchStart >= startTime of ' + url); + assert_greater_than_equal(entry.domainLookupStart, entry.fetchStart, + 'domainLookupStart >= fetchStart of ' + url); + assert_greater_than_equal(entry.domainLookupEnd, entry.domainLookupStart, + 'domainLookupEnd >= domainLookupStart of ' + url); + assert_greater_than_equal(entry.connectStart, entry.domainLookupEnd, + 'connectStart >= domainLookupEnd of ' + url); + assert_greater_than_equal(entry.connectEnd, entry.connectStart, + 'connectEnd >= connectStart of ' + url); + assert_greater_than_equal(entry.requestStart, entry.connectEnd, + 'requestStart >= connectEnd of ' + url); + assert_greater_than_equal(entry.responseStart, entry.requestStart, + 'domainLookupStart >= requestStart of ' + url); + assert_greater_than_equal(entry.responseEnd, entry.responseStart, + 'responseEnd >= responseStart of ' + url); + assert_greater_than(entry.duration, 0, 'duration of ' + url); +} + +promise_test(t => { + var script = 'resources/resource-timing-worker.js'; + var scope = 'resources/resource-timing-scope.py'; + var registration; + var frames = []; + return service_worker_unregister_and_register(t, script, scope) + .then(reg => { + registration = reg; + add_completion_callback(_ => registration.unregister()); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(_ => with_iframe(scope + '?type=normal')) + .then(frame => { + frames.push(frame); + return with_iframe(scope + '?type=redirect'); + }) + .then(frame => { + frames.push(frame); + frames.forEach(frame => { + var result = JSON.parse(frame.contentDocument.body.textContent); + assert_equals( + result.timingEntries.length, 1, + 'performance.getEntriesByName() must returns one ' + + 'PerformanceResourceTiming entry for the navigation preload.'); + var entry = result.timingEntries[0]; + check_timing_entry(entry, frame.src, result.decodedBodySize, + result.encodedBodySize); + frame.remove(); + }); + return registration.unregister(); + }); + }, 'Navigation Preload Resource Timing.'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis new file mode 100644 index 0000000000..2a719536fb --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Content-type: text/html; charset=UTF-8 +Transfer-encoding: chunked + +hello +world diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js new file mode 100644 index 0000000000..7a453e4055 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js @@ -0,0 +1,11 @@ +self.addEventListener('activate', event => { + event.waitUntil( + self.registration.navigationPreload.enable()); + }); + +self.addEventListener('fetch', event => { + event.respondWith(event.preloadResponse + .then( + _ => new Response('PASS: preloadResponse resolved'), + _ => new Response('FAIL: preloadResponse rejected'))); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py new file mode 100644 index 0000000000..659c4d8cdf --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py @@ -0,0 +1,19 @@ +import time + +def main(request, response): + use_broken_body = b'use_broken_body' in request.GET + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header(b"Content-type", b"text/html; charset=UTF-8") + response.writer.write_header(b"Transfer-encoding", b"chunked") + response.writer.end_headers() + + for idx in range(10): + if use_broken_body: + response.writer.write(u"%s\n%s\n" % (len(str(idx)), idx)) + else: + response.writer.write(u"%s\r\n%s\r\n" % (len(str(idx)), idx)) + time.sleep(0.001) + + response.writer.write(u"0\r\n\r\n") diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js new file mode 100644 index 0000000000..f30e5ed274 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js @@ -0,0 +1,8 @@ +self.addEventListener('activate', event => { + event.waitUntil( + self.registration.navigationPreload.enable()); + }); + +self.addEventListener('fetch', event => { + event.respondWith(event.preloadResponse); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/cookie.py b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/cookie.py new file mode 100644 index 0000000000..30a1dd498a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/cookie.py @@ -0,0 +1,20 @@ +def main(request, response): + """ + Returns a response with a Set-Cookie header based on the query params. + The body will be "1" if the cookie is present in the request and `drop` parameter is "0", + otherwise the body will be "0". + """ + same_site = request.GET.first(b"same-site") + cookie_name = request.GET.first(b"cookie-name") + drop = request.GET.first(b"drop") + cookie_in_request = b"0" + cookie = b"%s=1; Secure; SameSite=%s" % (cookie_name, same_site) + + if drop == b"1": + cookie += b"; Max-Age=0" + + if request.cookies.get(cookie_name): + cookie_in_request = request.cookies[cookie_name].value + + headers = [(b'Content-Type', b'text/html'), (b'Set-Cookie', cookie)] + return (200, headers, cookie_in_request) diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js new file mode 100644 index 0000000000..48c14b7916 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js @@ -0,0 +1,15 @@ +self.addEventListener('activate', event => { + event.waitUntil( + self.registration.navigationPreload.enable()); + }); + +self.addEventListener('fetch', event => { + event.respondWith( + event.preloadResponse + .then(res => res.text()) + .then(text => { + return new Response( + '<body>[' + text + ']</body>', + {headers: [['content-type', 'text/html']]}); + })); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js new file mode 100644 index 0000000000..a14ffb4faa --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js @@ -0,0 +1,21 @@ +// This worker listens for commands from the page and messages back +// the result. + +function handle(message) { + const np = self.registration.navigationPreload; + switch (message) { + case 'getState': + return np.getState(); + case 'enable': + return np.enable(); + case 'disable': + return np.disable(); + case 'setHeaderValue': + return np.setHeaderValue('insightful'); + } + return Promise.reject('bad message'); +} + +self.addEventListener('message', e => { + e.waitUntil(handle(e.data).then(result => e.source.postMessage(result))); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/helpers.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/helpers.js new file mode 100644 index 0000000000..86f0c0916e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/helpers.js @@ -0,0 +1,5 @@ +function expect_navigation_preload_state(state, enabled, header, desc) { + assert_equals(Object.keys(state).length, 2, desc + ': # of keys'); + assert_equals(state.enabled, enabled, desc + ': enabled'); + assert_equals(state.headerValue, header, desc + ': header'); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js new file mode 100644 index 0000000000..6e1ab23290 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', event => { + event.respondWith(event.preloadResponse); +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html new file mode 100644 index 0000000000..f9bfce5e89 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<body>redirected</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py new file mode 100644 index 0000000000..84a97e594b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py @@ -0,0 +1,38 @@ +def main(request, response): + if b"base" in request.GET: + return [(b"Content-Type", b"text/html")], b"OK" + type = request.GET.first(b"type") + + if type == b"normal": + response.status = 302 + response.headers.append(b"Location", b"redirect-redirected.html") + response.headers.append(b"Custom-Header", b"hello") + return b"" + + if type == b"no-location": + response.status = 302 + response.headers.append(b"Content-Type", b"text/html") + response.headers.append(b"Custom-Header", b"hello") + return b"" + + if type == b"no-location-with-body": + response.status = 302 + response.headers.append(b"Content-Type", b"text/html") + response.headers.append(b"Custom-Header", b"hello") + return b"<body>BODY</body>" + + if type == b"redirect-to-scope": + response.status = 302 + response.headers.append(b"Location", + b"redirect-scope.py?type=redirect-to-scope2") + return b"" + if type == b"redirect-to-scope2": + response.status = 302 + response.headers.append(b"Location", + b"redirect-scope.py?type=redirect-to-scope3") + return b"" + if type == b"redirect-to-scope3": + response.status = 302 + response.headers.append(b"Location", b"redirect-redirected.html") + response.headers.append(b"Custom-Header", b"hello") + return b"" diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js new file mode 100644 index 0000000000..1b55f2ef0d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js @@ -0,0 +1,35 @@ +self.addEventListener('activate', event => { + event.waitUntil( + self.registration.navigationPreload.enable()); + }); + +function get_response_info(r) { + var info = { + type: r.type, + url: r.url, + status: r.status, + ok: r.ok, + statusText: r.statusText, + headers: [] + }; + r.headers.forEach((value, name) => { info.headers.push([value, name]); }); + return info; +} + +function post_to_page(data) { + return self.clients.matchAll() + .then(clients => clients.forEach(client => client.postMessage(data))); +} + +self.addEventListener('fetch', event => { + event.respondWith( + event.preloadResponse + .then( + res => { + if (res.url.includes("base")) { + return res; + } + return post_to_page(get_response_info(res)).then(_ => res); + }, + err => new Response(err.toString()))); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py new file mode 100644 index 0000000000..5bab5b01f3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py @@ -0,0 +1,14 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + normalized = dict() + + for key, values in dict(request.headers).items(): + values = [isomorphic_decode(value) for value in values] + normalized[isomorphic_decode(key.upper())] = values + + response.headers.append(b"Content-Type", b"text/html") + + return json.dumps(normalized) diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js new file mode 100644 index 0000000000..1006cf2791 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js @@ -0,0 +1,10 @@ +self.addEventListener('activate', event => { + event.waitUntil( + Promise.all[ + self.registration.navigationPreload.enable(), + self.registration.navigationPreload.setHeaderValue('hello')]); + }); + +self.addEventListener('fetch', event => { + event.respondWith(event.preloadResponse); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py new file mode 100644 index 0000000000..856f9dbc2a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py @@ -0,0 +1,19 @@ +import zlib + +def main(request, response): + type = request.GET.first(b"type") + + if type == "normal": + content = b"This is Navigation Preload Resource Timing test." + output = zlib.compress(content, 9) + headers = [(b"Content-type", b"text/plain"), + (b"Content-Encoding", b"deflate"), + (b"X-Decoded-Body-Size", len(content)), + (b"X-Encoded-Body-Size", len(output)), + (b"Content-Length", len(output))] + return headers, output + + if type == b"redirect": + response.status = 302 + response.headers.append(b"Location", b"redirect-redirected.html") + return b"" diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js new file mode 100644 index 0000000000..fac0d8de2a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js @@ -0,0 +1,37 @@ +async function wait_for_performance_entries(url) { + let entries = performance.getEntriesByName(url); + if (entries.length > 0) { + return entries; + } + return new Promise((resolve) => { + new PerformanceObserver((list) => { + const entries = list.getEntriesByName(url); + if (entries.length > 0) { + resolve(entries); + } + }).observe({ entryTypes: ['resource'] }); + }); +} + +self.addEventListener('activate', event => { + event.waitUntil(self.registration.navigationPreload.enable()); + }); + +self.addEventListener('fetch', event => { + let headers; + event.respondWith( + event.preloadResponse + .then(response => { + headers = response.headers; + return response.text() + }) + .then(_ => wait_for_performance_entries(event.request.url)) + .then(entries => + new Response( + JSON.stringify({ + decodedBodySize: headers.get('X-Decoded-Body-Size'), + encodedBodySize: headers.get('X-Encoded-Body-Size'), + timingEntries: entries + }), + {headers: {'Content-Type': 'text/html'}}))); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html new file mode 100644 index 0000000000..a28b61261e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<body>samesite</body> +<script> +onmessage = (e) => { + if (e.data === "GetBody") { + parent.postMessage("samesite", '*'); + } +} +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html new file mode 100644 index 0000000000..51fdc9ec74 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Navigation Preload Same Site SW registrator</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/test-helpers.sub.js"></script> +<script> + +/** + * This is a helper file to register/unregister service worker in a same-site + * iframe. + **/ + +async function messageToParent(msg) { + parent.postMessage(msg, '*'); +} + +onmessage = async (e) => { + // t is a , but the helper function needs a test object. + let t = { + step_func: (func) => func, + }; + if (e.data === "Register") { + let reg = await service_worker_unregister_and_register(t, "samesite-worker.js", "."); + let worker = reg.installing; + await wait_for_state(t, worker, 'activated'); + await messageToParent("SW Registered"); + } else if (e.data == "Unregister") { + await service_worker_unregister(t, "."); + await messageToParent("SW Unregistered"); + } +} + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js new file mode 100644 index 0000000000..f30e5ed274 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js @@ -0,0 +1,8 @@ +self.addEventListener('activate', event => { + event.waitUntil( + self.registration.navigationPreload.enable()); + }); + +self.addEventListener('fetch', event => { + event.respondWith(event.preloadResponse); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js new file mode 100644 index 0000000000..87791d2e48 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js @@ -0,0 +1,40 @@ +// This worker remains in the installing phase so that the +// navigation preload API can be tested when there is no +// active worker. +importScripts('/resources/testharness.js'); +importScripts('helpers.js'); + +function expect_rejection(promise) { + return promise.then( + () => { return Promise.reject('unexpected fulfillment'); }, + err => { assert_equals('InvalidStateError', err.name); }); +} + +function test_before_activation() { + const np = self.registration.navigationPreload; + return expect_rejection(np.enable()) + .then(() => expect_rejection(np.disable())) + .then(() => expect_rejection(np.setHeaderValue('hi'))) + .then(() => np.getState()) + .then(state => expect_navigation_preload_state( + state, false, 'true', 'state should be the default')) + .then(() => 'PASS') + .catch(err => 'FAIL: ' + err); +} + +var resolve_done_promise; +var done_promise = new Promise(resolve => { resolve_done_promise = resolve; }); + +// Run the test once the page messages this worker. +self.addEventListener('message', e => { + e.waitUntil(test_before_activation() + .then(result => { + e.source.postMessage(result); + resolve_done_promise(); + })); + }); + +// Don't become the active worker until the test is done. +self.addEventListener('install', e => { + e.waitUntil(done_promise); + }); diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html new file mode 100644 index 0000000000..a860d95456 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<title>Navigation Preload: SameSite cookies</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<body> +<script> +const scope = 'resources/cookie.py'; +const script = 'resources/navigation-preload-worker.js'; + +async function drop_cookie(t, same_site, cookie) { + const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=1'); + t.add_cleanup(() => frame.remove()); +} + +async function same_site_cookies_test(t, same_site, cookie) { + // Remove the cookie before the first visit. + await drop_cookie(t, same_site, cookie); + + { + const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=0'); + t.add_cleanup(() => frame.remove()); + // The body will be 0 because this is the first visit. + assert_equals(frame.contentDocument.body.textContent, '0', 'first visit'); + } + + { + const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=0'); + t.add_cleanup(() => frame.remove()); + // The body will be 1 because this is the second visit. + assert_equals(frame.contentDocument.body.textContent, '1', 'second visit'); + } + + // Remove the cookie after the test. + t.add_cleanup(() => drop_cookie(t, same_site, cookie)); +} + +promise_test(async t => { + const registration = + await service_worker_unregister_and_register(t, script, scope); + promise_test(t => registration.unregister(), 'Unregister a service worker.'); + + await wait_for_state(t, registration.installing, 'activated'); + await registration.navigationPreload.enable(); +}, 'Set up a service worker for navigation preload tests.'); + +promise_test(async t => { + await same_site_cookies_test(t, 'None', 'cookie-key-none'); +}, 'Navigation Preload for same site cookies (None).'); + +promise_test(async t => { + await same_site_cookies_test(t, 'Strict', 'cookie-key-strict'); +}, 'Navigation Preload for same site cookies (Strict).'); + +promise_test(async t => { + await same_site_cookies_test(t, 'Lax', 'cookie-key-lax'); +}, 'Navigation Preload for same site cookies (Lax).'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html new file mode 100644 index 0000000000..633da9926a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Navigation Preload for same site iframe</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="../resources/test-helpers.sub.js"></script> +<body></body> +<script> + +const SAME_SITE = get_host_info().HTTPS_REMOTE_ORIGIN; +const RESOURCES_DIR = "/service-workers/service-worker/navigation-preload/resources/"; + +/** + * This test is used for testing the NavigationPreload works in a same site iframe. + * The test scenario is + * 1. Create a same site iframe to register service worker and wait for it be activated + * 2. Create a same site iframe which be intercepted by the service worker. + * 3. Once the iframe is loaded, service worker should set the page through the preload response. + * And checking if the iframe's body content is expected. + * 4. Unregister the service worker. + * 5. remove created iframes. + */ + +promise_test(async (t) => { + let resolver; + let checkValue = false; + window.onmessage = (e) => { + if (checkValue) { + assert_equals(e.data, "samesite"); + checkValue = false; + } + resolver(); + }; + + let helperIframe = document.createElement("iframe"); + helperIframe.src = SAME_SITE + RESOURCES_DIR + "samesite-sw-helper.html"; + document.body.appendChild(helperIframe); + + await new Promise(resolve => { + resolver = resolve; + helperIframe.onload = async () => { + helperIframe.contentWindow.postMessage("Register", '*'); + } + }); + + let sameSiteIframe = document.createElement("iframe"); + sameSiteIframe.src = SAME_SITE + RESOURCES_DIR + "samesite-iframe.html"; + document.body.appendChild(sameSiteIframe); + await new Promise(resolve => { + resolver = resolve; + sameSiteIframe.onload = async() => { + checkValue = true; + sameSiteIframe.contentWindow.postMessage("GetBody", '*') + } + }); + + await new Promise(resolve => { + resolver = resolve; + helperIframe.contentWindow.postMessage("Unregister", '*') + }); + + helperIframe.remove(); + sameSiteIframe.remove(); + }); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-body.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-body.https.html new file mode 100644 index 0000000000..0441c610b1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-body.https.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<title>Service Worker: Navigation redirection must clear body</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<meta charset="utf-8"> +<body> +<form id="test-form" method="POST" style="display: none;"> + <input type="submit" id="submit-button" /> +</form> +<script> +promise_test(function(t) { + var scope = 'resources/navigation-redirect-body.py'; + var script = 'resources/navigation-redirect-body-worker.js'; + var registration; + var frame = document.createElement('frame'); + var form = document.getElementById('test-form'); + var submit_button = document.getElementById('submit-button'); + + frame.src = 'about:blank'; + frame.name = 'target_frame'; + frame.id = 'frame'; + document.body.appendChild(frame); + t.add_cleanup(function() { document.body.removeChild(frame); }); + + form.action = scope; + form.target = 'target_frame'; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(r) { + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + var frame_load_promise = new Promise(function(resolve) { + frame.addEventListener('load', function() { + resolve(frame.contentWindow.document.body.innerText); + }, false); + }); + submit_button.click(); + return frame_load_promise; + }) + .then(function(text) { + var request_uri = decodeURIComponent(text); + assert_equals( + request_uri, + '/service-workers/service-worker/resources/navigation-redirect-body.py?redirect'); + return registration.unregister(); + }); + }, 'Navigation redirection must clear body'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-resolution.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-resolution.https.html new file mode 100644 index 0000000000..59e1cafec3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-resolution.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<title>Service Worker: Navigation Redirect Resolution</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +function make_absolute(url) { + return new URL(url, location).toString(); +} + +const script = 'resources/fetch-rewrite-worker.js'; + +function redirect_result_test(scope, expected_url, description) { + promise_test(async t => { + const registration = await service_worker_unregister_and_register( + t, script, scope); + t.add_cleanup(() => { + return service_worker_unregister(t, scope); + }) + await wait_for_state(t, registration.installing, 'activated'); + + // The navigation to |scope| will be resolved by a fetch to |redirect_url| + // which returns a relative Location header. If it is resolved relative to + // |scope|, the result will be navigate-redirect-resolution/blank.html. If + // relative to |redirect_url|, it will be resources/blank.html. The latter + // is correct. + const iframe = await with_iframe(scope); + t.add_cleanup(() => { iframe.remove(); }); + assert_equals(iframe.contentWindow.location.href, + make_absolute(expected_url)); + }, description); +} + +// |redirect_url| serves a relative redirect to resources/blank.html. +const redirect_url = 'resources/redirect.py?Redirect=blank.html'; + +// |scope_base| does not exist but will be replaced with a fetch of +// |redirect_url| by fetch-rewrite-worker.js. +const scope_base = 'resources/subdir/navigation-redirect-resolution?' + + 'redirect-mode=manual&url=' + + encodeURIComponent(make_absolute(redirect_url)); + +// When the Service Worker forwards the result of |redirect_url| as an +// opaqueredirect response, the redirect uses the response's URL list as the +// base URL, not the request. +redirect_result_test(scope_base, 'resources/blank.html', + 'test relative opaqueredirect'); + +// The response's base URL should be preserved across CacheStorage and clone. +redirect_result_test(scope_base + '&cache=1', 'resources/blank.html', + 'test relative opaqueredirect with CacheStorage'); +redirect_result_test(scope_base + '&clone=1', 'resources/blank.html', + 'test relative opaqueredirect with clone'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-to-http.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-to-http.https.html new file mode 100644 index 0000000000..d4d2788c58 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-to-http.https.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<title>Service Worker: Service Worker can receive HTTP opaqueredirect response.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<meta charset="utf-8"> +<body></body> +<script> +async_test(function(t) { + var frame_src = get_host_info()['HTTPS_ORIGIN'] + base_path() + + 'resources/navigation-redirect-to-http-iframe.html'; + function on_message(e) { + assert_equals(e.data.results, 'OK'); + t.done(); + } + + window.addEventListener('message', t.step_func(on_message), false); + + with_iframe(frame_src) + .then(function(frame) { + t.add_cleanup(function() { frame.remove(); }); + }); + }, 'Verify Service Worker can receive HTTP opaqueredirect response.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect.https.html new file mode 100644 index 0000000000..a87de56931 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect.https.html @@ -0,0 +1,846 @@ +<!DOCTYPE html> +<title>Service Worker: Navigation redirection</title> +<meta name="timeout" content="long"> +<!-- default variant tests document.location and intercepted URLs --> +<meta name="variant" content="?default"> +<!-- client variant tests the Clients API (resultingClientId and Client.url) --> +<meta name="variant" content="?client"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +const host_info = get_host_info(); + +// This test registers three Service Workers at SCOPE1, SCOPE2 and +// OTHER_ORIGIN_SCOPE. And checks the redirected page's URL and the requests +// which are intercepted by Service Worker while loading redirect page. +const BASE_URL = host_info['HTTPS_ORIGIN'] + base_path(); +const OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path(); + +const SCOPE1 = BASE_URL + 'resources/navigation-redirect-scope1.py?'; +const SCOPE2 = BASE_URL + 'resources/navigation-redirect-scope2.py?'; +const OUT_SCOPE = BASE_URL + 'resources/navigation-redirect-out-scope.py?'; +const SCRIPT = 'resources/redirect-worker.js'; + +const OTHER_ORIGIN_IFRAME_URL = + OTHER_BASE_URL + 'resources/navigation-redirect-other-origin.html'; +const OTHER_ORIGIN_SCOPE = + OTHER_BASE_URL + 'resources/navigation-redirect-scope1.py?'; +const OTHER_ORIGIN_OUT_SCOPE = + OTHER_BASE_URL + 'resources/navigation-redirect-out-scope.py?'; + +let registrations; +let workers; +let other_origin_frame; +let message_resolvers = {}; +let next_message_id = 0; + +promise_test(async t => { + // In this frame we register a service worker at OTHER_ORIGIN_SCOPE. + // And will use this frame to communicate with the worker. + other_origin_frame = await with_iframe(OTHER_ORIGIN_IFRAME_URL); + + // Register same-origin service workers. + registrations = await Promise.all([ + service_worker_unregister_and_register(t, SCRIPT, SCOPE1), + service_worker_unregister_and_register(t, SCRIPT, SCOPE2)]); + + // Wait for all workers to activate. + workers = registrations.map(get_effective_worker); + return Promise.all([ + wait_for_state(t, workers[0], 'activated'), + wait_for_state(t, workers[1], 'activated'), + // This promise will resolve when |wait_for_worker_promise| + // in OTHER_ORIGIN_IFRAME_URL resolves. + send_to_iframe(other_origin_frame, {command: 'wait_for_worker'})]); +}, 'initialize global state'); + +function get_effective_worker(registration) { + if (registration.active) + return registration.active; + if (registration.waiting) + return registration.waiting; + if (registration.installing) + return registration.installing; +} + +async function check_all_intercepted_urls(expected_urls) { + const urls = []; + urls.push(await get_intercepted_urls(workers[0])); + urls.push(await get_intercepted_urls(workers[1])); + // Gets the request URLs which are intercepted by OTHER_ORIGIN_SCOPE's + // SW. This promise will resolve when get_request_infos() in + // OTHER_ORIGIN_IFRAME_URL resolves. + const request_infos = await send_to_iframe(other_origin_frame, + {command: 'get_request_infos'}); + urls.push(request_infos.map(info => { return info.url; })); + + assert_object_equals(urls, expected_urls, 'Intercepted URLs should match.'); +} + +// Checks |clients| returned from a worker. Only the client matching +// |expected_final_client_tag| should be found. Returns true if a client was +// found. Note that the final client is not necessarily found by this worker, +// if the client is cross-origin. +// +// |clients| is an object like: +// {x: {found: true, id: id1, url: url1}, b: {found: false}} +function check_clients(clients, + expected_id, + expected_url, + expected_final_client_tag, + worker_name) { + let found = false; + Object.keys(clients).forEach(key => { + const info = clients[key]; + if (info.found) { + assert_true(!!expected_final_client_tag, + `${worker_name} client tag exists`); + assert_equals(key, expected_final_client_tag, + `${worker_name} client tag matches`); + assert_equals(info.id, expected_id, `${worker_name} client id`); + assert_equals(info.url, expected_url, `${worker_name} client url`); + found = true; + } + }); + return found; +} + +function check_resulting_client_ids(infos, expected_infos, actual_ids, worker) { + assert_equals(infos.length, expected_infos.length, + `request length for ${worker}`); + for (var i = 0; i < infos.length; i++) { + const tag = expected_infos[i].resultingClientIdTag; + const url = expected_infos[i].url; + const actual_id = infos[i].resultingClientId; + const expected_id = actual_ids[tag]; + assert_equals(typeof(actual_id), 'string', + `resultingClientId for ${url} request to ${worker}`); + if (expected_id) { + assert_equals(actual_id, expected_id, + `resultingClientId for ${url} request to ${worker}`); + } else { + actual_ids[tag] = actual_id; + } + } +} + +// Creates an iframe and navigates to |url|, which is expected to start a chain +// of redirects. +// - |expected_last_url| is the expected window.location after the +// navigation. +// +// - |expected_request_infos| is the expected requests that the service workers +// were dispatched fetch events for. The format is: +// [ +// [ +// // Requests received by workers[0]. +// {url: url1, resultingClientIdTag: 'a'}, +// {url: url2, resultingClientIdTag: 'a'} +// ], +// [ +// // Requests received by workers[1]. +// {url: url3, resultingClientIdTag: 'a'} +// ], +// [ +// // Requests received by the cross-origin worker. +// {url: url4, resultingClientIdTag: 'x'} +// {url: url5, resultingClientIdTag: 'x'} +// ] +// ] +// Here, |url| is |event.request.url| and |resultingClientIdTag| represents +// |event.resultingClientId|. Since the actual client ids are not known +// beforehand, the expectation isn't the literal expected value, but all equal +// tags must map to the same actual id. +// +// - |expected_final_client_tag| is the resultingClientIdTag that is +// expected to map to the created client's id. This is null if there +// is no such tag, which can happen when the final request was a cross-origin +// redirect to out-scope, so no worker received a fetch event whose +// resultingClientId is the id of the resulting client. +// +// In the example above: +// - workers[0] receives two requests with the same resultingClientId. +// - workers[1] receives one request also with that resultingClientId. +// - The cross-origin worker receives two requests with the same +// resultingClientId which differs from the previous one. +// - Assuming |expected_final_client_tag| is 'x', then the created +// client has the id seen by the cross-origin worker above. +function redirect_test(url, + expected_last_url, + expected_request_infos, + expected_final_client_tag, + test_name) { + promise_test(async t => { + const frame = await with_iframe(url); + t.add_cleanup(() => { frame.remove(); }); + + // Switch on variant. + if (document.location.search == '?client') { + return client_variant_test(url, expected_last_url, expected_request_infos, + expected_final_client_tag, test_name); + } + + return default_variant_test(url, expected_last_url, expected_request_infos, + frame, test_name); + }, test_name); +} + +// The default variant tests the request interception chain and +// resulting document.location. +async function default_variant_test(url, + expected_last_url, + expected_request_infos, + frame, + test_name) { + const expected_intercepted_urls = expected_request_infos.map( + requests_for_worker => { + return requests_for_worker.map(info => { + return info.url; + }); + }); + await check_all_intercepted_urls(expected_intercepted_urls); + const last_url = await send_to_iframe(frame, 'getLocation'); + assert_equals(last_url, expected_last_url, 'Last URL should match.'); +} + +// The "client" variant tests the Clients API using resultingClientId. +async function client_variant_test(url, + expected_last_url, + expected_request_infos, + expected_final_client_tag, + test_name) { + // Request infos is an array like: + // [ + // [{url: url1, resultingClientIdTag: tag1}], + // [{url: url2, resultingClientIdTag: tag2}], + // [{url: url3: resultingClientIdTag: tag3}] + // ] + const requestInfos = await get_all_request_infos(); + + // We check the actual infos against the expected ones, and learn the + // actual ids as we go. + const actual_ids = {}; + check_resulting_client_ids(requestInfos[0], + expected_request_infos[0], + actual_ids, + 'worker0'); + check_resulting_client_ids(requestInfos[1], + expected_request_infos[1], + actual_ids, + 'worker1'); + check_resulting_client_ids(requestInfos[2], + expected_request_infos[2], + actual_ids, + 'crossOriginWorker'); + + // Now |actual_ids| maps tag to actual id: + // {x: id1, b: id2, c: id3} + // Ask each worker to try to resolve the actual ids to clients. + // Only |expected_final_client_tag| should resolve to a client. + const client_infos = await get_all_clients(actual_ids); + + // Client infos is an object like: + // { + // worker0: {x: {found: true, id: id1, url: url1}, b: {found: false}}, + // worker1: {x: {found: true, id: id1, url: url1}}, + // crossOriginWorker: {x: {found: false}}, {b: {found: false}} + // } + // + // Now check each client info. check_clients() verifies each info: only + // |expected_final_client_tag| should ever be found and the found client + // should have the expected url and id. A wrinkle is that not all workers + // will find the client, if they are cross-origin to the client. This + // means check_clients() trivially passes if no clients are found. So + // additionally check that at least one worker found the client (|found|), + // if that was expected (|expect_found|). + let found = false; + const expect_found = !!expected_final_client_tag; + const expected_id = actual_ids[expected_final_client_tag]; + found = check_clients(client_infos.worker0, + expected_id, + expected_last_url, + expected_final_client_tag, + 'worker0'); + found = check_clients(client_infos.worker1, + expected_id, + expected_last_url, + expected_final_client_tag, + 'worker1') || found; + found = check_clients(client_infos.crossOriginWorker, + expected_id, + expected_last_url, + expected_final_client_tag, + 'crossOriginWorker') || found; + assert_equals(found, expect_found, 'client found'); + + if (!expect_found) { + // TODO(falken): Ask the other origin frame if it has a client of the + // expected URL. + } +} + +window.addEventListener('message', on_message, false); + +function on_message(e) { + if (e.origin != host_info['HTTPS_REMOTE_ORIGIN'] && + e.origin != host_info['HTTPS_ORIGIN'] ) { + console.error('invalid origin: ' + e.origin); + return; + } + var resolve = message_resolvers[e.data.id]; + delete message_resolvers[e.data.id]; + resolve(e.data.result); +} + +function send_to_iframe(frame, message) { + var message_id = next_message_id++; + return new Promise(resolve => { + message_resolvers[message_id] = resolve; + frame.contentWindow.postMessage( + {id: message_id, message}, + '*'); + }); +} + +async function get_all_clients(actual_ids) { + const client_infos = {}; + client_infos['worker0'] = await get_clients(workers[0], actual_ids); + client_infos['worker1'] = await get_clients(workers[1], actual_ids); + client_infos['crossOriginWorker'] = + await send_to_iframe(other_origin_frame, + {command: 'get_clients', actual_ids}); + return client_infos; +} + +function get_clients(worker, actual_ids) { + return new Promise(resolve => { + var channel = new MessageChannel(); + channel.port1.onmessage = (msg) => { + resolve(msg.data.clients); + }; + worker.postMessage({command: 'getClients', actual_ids, port: channel.port2}, + [channel.port2]); + }); +} + +// Returns an array of the URLs that |worker| received fetch events for: +// [url1, url2] +async function get_intercepted_urls(worker) { + const infos = await get_request_infos(worker); + return infos.map(info => { return info.url; }); +} + +// Returns the requests that |worker| received fetch events for. The return +// value is an array of format: +// [ +// {url: url1, resultingClientId: id}, +// {url: url2, resultingClientId: id} +// ] +function get_request_infos(worker) { + return new Promise(resolve => { + var channel = new MessageChannel(); + channel.port1.onmessage = (msg) => { + resolve(msg.data.requestInfos); + }; + worker.postMessage({command: 'getRequestInfos', port: channel.port2}, + [channel.port2]); + }); +} + +// Returns an array of the requests the workers received fetch events for: +// [ +// // Requests from workers[0]. +// [ +// {url: url1, resultingClientIdTag: tag1}, +// {url: url2, resultingClientIdTag: tag1} +// ], +// +// // Requests from workers[1]. +// [{url: url3, resultingClientIdTag: tag2}], +// +// // Requests from the cross-origin worker. +// [] +// ] +async function get_all_request_infos() { + const request_infos = []; + request_infos.push(await get_request_infos(workers[0])); + request_infos.push(await get_request_infos(workers[1])); + request_infos.push(await send_to_iframe(other_origin_frame, + {command: 'get_request_infos'})); + return request_infos; +} + +let url; +let url1; +let url2; + +// Normal redirect (from out-scope to in-scope). +url = SCOPE1; +redirect_test( + OUT_SCOPE + 'url=' + encodeURIComponent(url), + url, + [[{url, resultingClientIdTag: 'x'}], [], []], + 'x', + 'Normal redirect to same-origin scope.'); + + +url = SCOPE1 + '#ref'; +redirect_test( + OUT_SCOPE + 'url=' + encodeURIComponent(SCOPE1) + '#ref', + url, + [[{url, resultingClientIdTag: 'x'}], [], []], + 'x', + 'Normal redirect to same-origin scope with a hash fragment.'); + +url = SCOPE1 + '#ref2'; +redirect_test( + OUT_SCOPE + 'url=' + encodeURIComponent(url) + '#ref', + url, + [[{url, resultingClientIdTag: 'x'}], [], []], + 'x', + 'Normal redirect to same-origin scope with different hash fragments.'); + +url = OTHER_ORIGIN_SCOPE; +redirect_test( + OUT_SCOPE + 'url=' + encodeURIComponent(url), + url, + [[], [], [{url, resultingClientIdTag: 'x'}]], + 'x', + 'Normal redirect to other-origin scope.'); + +// SW fallbacked redirect. SW doesn't handle the fetch request. +url = SCOPE1 + 'url=' + encodeURIComponent(OUT_SCOPE); +redirect_test( + url, + OUT_SCOPE, + [[{url, resultingClientIdTag: 'x'}], [], []], + 'x', + 'SW-fallbacked redirect to same-origin out-scope.'); + +url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1); +url2 = SCOPE1; +redirect_test( + url1, + url2, + [ + [ + {url: url1, resultingClientIdTag: 'x'}, + {url: url2, resultingClientIdTag: 'x'} + ], + [], + [] + ], + 'x', + 'SW-fallbacked redirect to same-origin same-scope.'); + +url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1) + '#ref'; +url2 = SCOPE1 + '#ref'; +redirect_test( + url1, + url2, + [ + [ + {url: url1, resultingClientIdTag: 'x'}, + {url: url2, resultingClientIdTag: 'x'} + ], + [], + [] + ], + 'x', + 'SW-fallbacked redirect to same-origin same-scope with a hash fragment.'); + +url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1 + '#ref2') + '#ref'; +url2 = SCOPE1 + '#ref2'; +redirect_test( + url1, + url2, + [ + [ + {url: url1, resultingClientIdTag: 'x'}, + {url: url2, resultingClientIdTag: 'x'} + ], + [], + [] + ], + 'x', + 'SW-fallbacked redirect to same-origin same-scope with different hash ' + + 'fragments.'); + +url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE2); +url2 = SCOPE2; +redirect_test( + url1, + url2, + [ + [{url: url1, resultingClientIdTag: 'x'}], + [{url: url2, resultingClientIdTag: 'x'}], + [] + ], + 'x', + 'SW-fallbacked redirect to same-origin other-scope.'); + +url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE); +url2 = OTHER_ORIGIN_OUT_SCOPE; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'a'}], [], []], + null, + 'SW-fallbacked redirect to other-origin out-scope.'); + +url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE); +url2 = OTHER_ORIGIN_SCOPE; +redirect_test( + url1, + url2, + [ + [{url: url1, resultingClientIdTag: 'a'}], + [], + [{url: url2, resultingClientIdTag: 'x'}] + ], + 'x', + 'SW-fallbacked redirect to other-origin in-scope.'); + + +url3 = SCOPE1; +url2 = OTHER_ORIGIN_SCOPE + 'url=' + encodeURIComponent(url3); +url1 = SCOPE1 + 'url=' + encodeURIComponent(url2); +redirect_test( + url1, + url3, + [ + [ + {url: url1, resultingClientIdTag: 'a'}, + {url: url3, resultingClientIdTag: 'x'} + ], + [], + [{url: url2, resultingClientIdTag: 'b'}] + ], + 'x', + 'SW-fallbacked redirect to other-origin and back to same-origin.'); + +// SW generated redirect. +// SW: event.respondWith(Response.redirect(params['url'])); +url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE); +url2 = OUT_SCOPE; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'x'}], [], []], + 'x', + 'SW-generated redirect to same-origin out-scope.'); + +url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE) + '#ref'; +url2 = OUT_SCOPE + '#ref'; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'x'}], [], []], + 'x', + 'SW-generated redirect to same-origin out-scope with a hash fragment.'); + +url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE + '#ref2') + '#ref'; +url2 = OUT_SCOPE + '#ref2'; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'x'}], [], []], + 'x', + 'SW-generated redirect to same-origin out-scope with different hash ' + + 'fragments.'); + +url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE1); +url2 = SCOPE1; +redirect_test( + url1, + url2, + [ + [ + {url: url1, resultingClientIdTag: 'x'}, + {url: url2, resultingClientIdTag: 'x'} + ], + [], + [] + ], + 'x', + 'SW-generated redirect to same-origin same-scope.'); + +url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE2); +url2 = SCOPE2; +redirect_test( + url1, + url2, + [ + [{url: url1, resultingClientIdTag: 'x'}], + [{url: url2, resultingClientIdTag: 'x'}], + [] + ], + 'x', + 'SW-generated redirect to same-origin other-scope.'); + +url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE); +url2 = OTHER_ORIGIN_OUT_SCOPE; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'a'}], [], []], + null, + 'SW-generated redirect to other-origin out-scope.'); + +url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE); +url2 = OTHER_ORIGIN_SCOPE; +redirect_test( + url1, + url2, + [ + [{url: url1, resultingClientIdTag: 'a'}], + [], + [{url: url2, resultingClientIdTag: 'x'}] + ], + 'x', + 'SW-generated redirect to other-origin in-scope.'); + + +// SW fetched redirect. +// SW: event.respondWith(fetch(event.request)); +url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OUT_SCOPE) +url2 = OUT_SCOPE; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'x'}], [], []], + 'x', + 'SW-fetched redirect to same-origin out-scope.'); + +url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE1); +url2 = SCOPE1; +redirect_test( + url1, + url2, + [ + [ + {url: url1, resultingClientIdTag: 'x'}, + {url: url2, resultingClientIdTag: 'x'} + ], + [], + [] + ], + 'x', + 'SW-fetched redirect to same-origin same-scope.'); + +url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE2); +url2 = SCOPE2; +redirect_test( + url1, + url2, + [ + [{url: url1, resultingClientIdTag: 'x'}], + [{url: url2, resultingClientIdTag: 'x'}], + [] + ], + 'x', + 'SW-fetched redirect to same-origin other-scope.'); + +url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE); +url2 = OTHER_ORIGIN_OUT_SCOPE; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'a'}], [], []], + null, + 'SW-fetched redirect to other-origin out-scope.'); + +url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE); +url2 = OTHER_ORIGIN_SCOPE; +redirect_test( + url1, + url2, + [ + [{url: url1, resultingClientIdTag: 'a'}], + [], + [{url: url2, resultingClientIdTag: 'x'}] + ], + 'x', + 'SW-fetched redirect to other-origin in-scope.'); + + +// SW responds with a fetch from a different url. +// SW: event.respondWith(fetch(params['url'])); +url2 = SCOPE1; +url1 = SCOPE1 + 'sw=fetch-url&url=' + encodeURIComponent(url2); +redirect_test( + url1, + url1, + [ + [ + {url: url1, resultingClientIdTag: 'x'} + ], + [], + [] + ], + 'x', + 'SW-fetched response from different URL, same-origin same-scope.'); + + +// Opaque redirect. +// SW: event.respondWith(fetch( +// new Request(event.request.url, {redirect: 'manual'}))); +url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OUT_SCOPE); +url2 = OUT_SCOPE; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'x'}], [], []], + 'x', + 'Redirect to same-origin out-scope with opaque redirect response.'); + +url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE1); +url2 = SCOPE1; +redirect_test( + url1, + url2, + [ + [ + {url: url1, resultingClientIdTag: 'x'}, + {url: url2, resultingClientIdTag: 'x'} + ], + [], + [] + ], + 'x', + 'Redirect to same-origin same-scope with opaque redirect response.'); + +url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE2); +url2 = SCOPE2; +redirect_test( + url1, + url2, + [ + [{url: url1, resultingClientIdTag: 'x'}], + [{url: url2, resultingClientIdTag: 'x'}], + [] + ], + 'x', + 'Redirect to same-origin other-scope with opaque redirect response.'); + +url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE); +url2 = OTHER_ORIGIN_OUT_SCOPE; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'a'}], [], []], + null, + 'Redirect to other-origin out-scope with opaque redirect response.'); + +url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE); +url2 = OTHER_ORIGIN_SCOPE; +redirect_test( + url1, + url2, + [ + [{url: url1, resultingClientIdTag: 'a'}], + [], + [{url: url2, resultingClientIdTag: 'x'}] + ], + 'x', + 'Redirect to other-origin in-scope with opaque redirect response.'); + +url= SCOPE1 + 'sw=manual&noLocationRedirect'; +redirect_test( + url, url, [[{url, resultingClientIdTag: 'x'}], [], []], + 'x', + 'No location redirect response.'); + + +// Opaque redirect passed through Cache. +// SW responds with an opaque redirectresponse from the Cache API. +url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(OUT_SCOPE); +url2 = OUT_SCOPE; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'x'}], [], []], + 'x', + 'Redirect to same-origin out-scope with opaque redirect response which ' + + 'is passed through Cache.'); + +url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE1); +url2 = SCOPE1; +redirect_test( + url1, + url2, + [ + [ + {url: url1, resultingClientIdTag: 'x'}, + {url: url2, resultingClientIdTag: 'x'} + ], + [], + [] + ], + 'x', + 'Redirect to same-origin same-scope with opaque redirect response which ' + + 'is passed through Cache.'); + +url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE2); +url2 = SCOPE2; +redirect_test( + url1, + url2, + [ + [{url: url1, resultingClientIdTag: 'x'}], + [{url: url2, resultingClientIdTag: 'x'}], + [] + ], + 'x', + 'Redirect to same-origin other-scope with opaque redirect response which ' + + 'is passed through Cache.'); + +url1 = SCOPE1 + 'sw=manualThroughCache&url=' + + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE); +url2 = OTHER_ORIGIN_OUT_SCOPE; +redirect_test( + url1, + url2, + [[{url: url1, resultingClientIdTag: 'a'}], [], []], + null, + 'Redirect to other-origin out-scope with opaque redirect response which ' + + 'is passed through Cache.'); + +url1 = SCOPE1 + 'sw=manualThroughCache&url=' + + encodeURIComponent(OTHER_ORIGIN_SCOPE); +url2 = OTHER_ORIGIN_SCOPE; +redirect_test( + url1, + url2, + [ + [{url: url1, resultingClientIdTag: 'a'}], + [], + [{url: url2, resultingClientIdTag: 'x'}], + ], + 'x', + 'Redirect to other-origin in-scope with opaque redirect response which ' + + 'is passed through Cache.'); + +url = SCOPE1 + 'sw=manualThroughCache&noLocationRedirect'; +redirect_test( + url, + url, + [[{url, resultingClientIdTag: 'x'}], [], []], + 'x', + 'No location redirect response via Cache.'); + +// Clean up the test environment. This promise_test() needs to be the last one. +promise_test(async t => { + registrations.forEach(async registration => { + if (registration) + await registration.unregister(); + }); + await send_to_iframe(other_origin_frame, {command: 'unregister'}); + other_origin_frame.remove(); +}, 'clean up global state'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-sets-cookie.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-sets-cookie.https.html new file mode 100644 index 0000000000..7f6c756f55 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-sets-cookie.https.html @@ -0,0 +1,133 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<meta name="timeout" content="long"> +<title>Service Worker: Navigation setting cookies</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="/cookies/resources/cookie-helper.sub.js"></script> +<body> +<script> +'use strict'; + +const scopepath = '/cookies/resources/setSameSite.py?with-sw'; + +async function unregister_service_worker(origin) { + let target_url = origin + + '/service-workers/service-worker/resources/unregister-rewrite-worker.html' + + '?scopepath=' + encodeURIComponent(scopepath); + const w = window.open(target_url); + try { + await wait_for_message('SW-UNREGISTERED'); + } finally { + w.close(); + } +} + +async function register_service_worker(origin) { + let target_url = origin + + '/service-workers/service-worker/resources/register-rewrite-worker.html' + + '?scopepath=' + encodeURIComponent(scopepath); + const w = window.open(target_url); + try { + await wait_for_message('SW-REGISTERED'); + } finally { + w.close(); + } +} + +async function clear_cookies(origin) { + let target_url = origin + '/cookies/samesite/resources/puppet.html'; + const w = window.open(target_url); + try { + await wait_for_message('READY'); + w.postMessage({ type: 'drop' }, '*'); + await wait_for_message('drop-complete'); + } finally { + w.close(); + } +} + +// The following tests are adapted from /cookies/samesite/setcookie-navigation.https.html + +// Asserts that cookies are present or not present (according to `expectation`) +// in the cookie string `cookies` with the correct names and value. +function assert_cookies_present(cookies, value, expected_cookie_names, expectation) { + for (name of expected_cookie_names) { + let re = new RegExp("(?:^|; )" + name + "=" + value + "(?:$|;)"); + let assertion = expectation ? assert_true : assert_false; + assertion(re.test(cookies), "`" + name + "=" + value + "` in cookies"); + } +} + +// Navigate from ORIGIN to |origin_to|, expecting the navigation to set SameSite +// cookies on |origin_to|. +function navigate_test(method, origin_to, query, title) { + promise_test(async function(t) { + // The cookies don't need to be cleared on each run because |value| is + // a new random value on each run, so on each run we are overwriting and + // checking for a cookie with a different random value. + let value = query + "&" + Math.random(); + let url_from = SECURE_ORIGIN + "/cookies/samesite/resources/navigate.html" + let url_to = origin_to + "/cookies/resources/setSameSite.py?" + value; + var w = window.open(url_from); + await wait_for_message('READY', SECURE_ORIGIN); + assert_equals(SECURE_ORIGIN, window.origin); + assert_equals(SECURE_ORIGIN, w.origin); + let command = (method === "POST") ? "post-form" : "navigate"; + w.postMessage({ type: command, url: url_to }, "*"); + let message = await wait_for_message('COOKIES_SET', origin_to); + let samesite_cookie_names = ['samesite_strict', 'samesite_lax', 'samesite_none', 'samesite_unspecified']; + assert_cookies_present(message.data.cookies, value, samesite_cookie_names, true); + w.close(); + }, title); +} + +promise_test(async t => { + await register_service_worker(SECURE_ORIGIN); + await register_service_worker(SECURE_CROSS_SITE_ORIGIN); +}, 'Setup service workers'); + +navigate_test("GET", SECURE_ORIGIN, "with-sw&ignore", + "Same-site top-level navigation with fallback service worker should be able to set SameSite=* cookies."); +navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&ignore", + "Cross-site top-level navigation with fallback service worker should be able to set SameSite=* cookies."); +navigate_test("POST", SECURE_ORIGIN, "with-sw&ignore", + "Same-site top-level POST with fallback service worker should be able to set SameSite=* cookies."); +navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw&ignore", + "Cross-site top-level with fallback service worker POST should be able to set SameSite=* cookies."); + +navigate_test("GET", SECURE_ORIGIN, "with-sw", + "Same-site top-level navigation with passthrough service worker should be able to set SameSite=* cookies."); +navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw", + "Cross-site top-level navigation with passthrough service worker should be able to set SameSite=* cookies."); +navigate_test("POST", SECURE_ORIGIN, "with-sw", + "Same-site top-level POST with passthrough service worker should be able to set SameSite=* cookies."); +navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw", + "Cross-site top-level with passthrough service worker POST should be able to set SameSite=* cookies."); + +navigate_test("GET", SECURE_ORIGIN, "with-sw&navpreload", + "Same-site top-level navigation with navpreload service worker should be able to set SameSite=* cookies."); +navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&navpreload", + "Cross-site top-level navigation with navpreload service worker should be able to set SameSite=* cookies."); +// navpreload not supported with POST method + +navigate_test("GET", SECURE_ORIGIN, "with-sw&change-request", + "Same-site top-level navigation with change-request service worker should be able to set SameSite=* cookies."); +navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&change-request", + "Cross-site top-level navigation with change-request service worker should be able to set SameSite=* cookies."); +navigate_test("POST", SECURE_ORIGIN, "with-sw&change-request", + "Same-site top-level POST with change-request service worker should be able to set SameSite=* cookies."); +navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw&change-request", + "Cross-site top-level with change-request service worker POST should be able to set SameSite=* cookies."); + +promise_test(async t => { + await unregister_service_worker(SECURE_ORIGIN); + await unregister_service_worker(SECURE_CROSS_SITE_ORIGIN); + await clear_cookies(SECURE_ORIGIN); + await clear_cookies(SECURE_CROSS_SITE_ORIGIN); +}, 'Cleanup service workers'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-timing-extended.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-timing-extended.https.html new file mode 100644 index 0000000000..acb02c6fe1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-timing-extended.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> + +<script> +const timingEventOrder = [ + 'startTime', + 'workerStart', + 'fetchStart', + 'requestStart', + 'responseStart', + 'responseEnd', +]; + +function navigate_in_frame(frame, url) { + frame.contentWindow.location = url; + return new Promise((resolve) => { + frame.addEventListener('load', () => { + const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0]; + const {timeOrigin} = frame.contentWindow.performance; + resolve({ + workerStart: timing.workerStart + timeOrigin, + fetchStart: timing.fetchStart + timeOrigin + }) + }); + }); +} + +const worker_url = 'resources/navigation-timing-worker-extended.js'; + +promise_test(async (t) => { + const scope = 'resources/timings/dummy.html'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activating'); + const frame = await with_iframe('resources/empty.html'); + t.add_cleanup(() => frame.remove()); + + const [timingFromEntry, timingFromWorker] = await Promise.all([ + navigate_in_frame(frame, scope), + new Promise(resolve => { + window.addEventListener('message', m => { + resolve(m.data) + }) + })]) + + assert_greater_than(timingFromWorker.activateWorkerEnd, timingFromEntry.workerStart, + 'workerStart marking should not wait for worker activation to finish'); + assert_greater_than(timingFromEntry.fetchStart, timingFromWorker.activateWorkerEnd, + 'fetchStart should be marked once the worker is activated'); + assert_greater_than(timingFromWorker.handleFetchEvent, timingFromEntry.fetchStart, + 'fetchStart should be marked before the Fetch event handler is called'); +}, 'Service worker controlled navigation timing'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-timing.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-timing.https.html new file mode 100644 index 0000000000..75cab40458 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/navigation-timing.https.html @@ -0,0 +1,77 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> + +<script> +const timingEventOrder = [ + 'startTime', + 'workerStart', + 'fetchStart', + 'requestStart', + 'responseStart', + 'responseEnd', +]; + +function verify(timing) { + for (let i = 0; i < timingEventOrder.length - 1; i++) { + assert_true(timing[timingEventOrder[i]] <= timing[timingEventOrder[i + 1]], + `Expected ${timingEventOrder[i]} <= ${timingEventOrder[i + 1]}`); + } +} + +function navigate_in_frame(frame, url) { + frame.contentWindow.location = url; + return new Promise((resolve) => { + frame.addEventListener('load', () => { + const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0]; + resolve(timing); + }); + }); +} + +const worker_url = 'resources/navigation-timing-worker.js'; + +promise_test(async (t) => { + const scope = 'resources/empty.html'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + const timing = await navigate_in_frame(frame, scope); + assert_greater_than(timing.workerStart, 0); + verify(timing); +}, 'Service worker controlled navigation timing'); + +promise_test(async (t) => { + const scope = 'resources/empty.html?network-fallback'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + const timing = await navigate_in_frame(frame, scope); + assert_greater_than(timing.workerStart, 0); + verify(timing); +}, 'Service worker controlled navigation timing network fallback'); + +promise_test(async (t) => { + const scope = 'resources/redirect.py?Redirect=empty.html'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + const timing = await navigate_in_frame(frame, scope); + verify(timing); + // Additional checks for redirected navigation. + assert_true(timing.redirectStart <= timing.redirectEnd, + 'Expected redirectStart <= redirectEnd'); + assert_true(timing.redirectEnd <= timing.fetchStart, + 'Expected redirectEnd <= fetchStart'); +}, 'Service worker controlled navigation timing redirect'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/nested-blob-url-workers.https.html b/testing/web-platform/tests/service-workers/service-worker/nested-blob-url-workers.https.html new file mode 100644 index 0000000000..7269cbb701 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/nested-blob-url-workers.https.html @@ -0,0 +1,42 @@ +<!doctype html> +<meta charset=utf-8> +<title>Service Worker: nested blob URL worker clients</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +const SCRIPT = 'resources/simple-intercept-worker.js'; +const SCOPE = 'resources/'; +const RESOURCE = 'resources/simple.txt'; + +promise_test((t) => { + return runTest(t, 'resources/nested-blob-url-workers.html'); +}, 'Nested blob URL workers should be intercepted by a service worker.'); + +promise_test((t) => { + return runTest(t, 'resources/nested-worker-created-from-blob-url-worker.html'); +}, 'Nested worker created from a blob URL worker should be intercepted by a service worker.'); + +promise_test((t) => { + return runTest(t, 'resources/nested-blob-url-worker-created-from-worker.html'); +}, 'Nested blob URL worker created from a worker should be intercepted by a service worker.'); + +async function runTest(t, iframe_url) { + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + t.add_cleanup(_ => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + const frame = await with_iframe(iframe_url); + t.add_cleanup(_ => frame.remove()); + assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, + null, 'frame should be controlled'); + + const response_text = await frame.contentWindow.fetch_in_worker(RESOURCE); + assert_equals(response_text, 'intercepted by service worker', + 'fetch() should be intercepted.'); +} + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/next-hop-protocol.https.html b/testing/web-platform/tests/service-workers/service-worker/next-hop-protocol.https.html new file mode 100644 index 0000000000..7a907438d5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/next-hop-protocol.https.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: Verify nextHopProtocol is set correctly</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +async function getNextHopProtocol(frame, url) { + let final_url = new URL(url, self.location).href; + await frame.contentWindow.fetch(final_url).then(r => r.text()); + let entryList = frame.contentWindow.performance.getEntriesByName(final_url); + let entry = entryList[entryList.length - 1]; + return entry.nextHopProtocol; +} + +async function runTest(t, base_url, expected_protocol) { + const scope = 'resources/empty.html?next-hop-protocol'; + const script = 'resources/fetch-rewrite-worker.js'; + let frame; + + const registration = + await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(async _ => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + frame = await with_iframe(scope); + t.add_cleanup(_ => frame.remove()); + + assert_equals(await getNextHopProtocol(frame, `${base_url}?generate-png`), + '', 'nextHopProtocol is not set on synthetic response'); + assert_equals(await getNextHopProtocol(frame, `${base_url}?ignore`), + expected_protocol, 'nextHopProtocol is set on fallback'); + assert_equals(await getNextHopProtocol(frame, `${base_url}`), + expected_protocol, 'nextHopProtocol is set on pass-through'); + assert_equals(await getNextHopProtocol(frame, `${base_url}?cache`), + expected_protocol, 'nextHopProtocol is set on cached response'); +} + +promise_test(async (t) => { + return runTest(t, 'resources/empty.js', 'http/1.1'); +}, 'nextHopProtocol reports H1 correctly when routed via a service worker.'); + +// This may be expected to fail if the WPT infrastructure does not fully +// support H2 protocol testing yet. +promise_test(async (t) => { + return runTest(t, 'resources/empty.h2.js', 'h2'); +}, 'nextHopProtocol reports H2 correctly when routed via a service worker.'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js b/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js new file mode 100644 index 0000000000..f7c2ef37b8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js @@ -0,0 +1,7 @@ +// META: global=serviceworker-module + +// This is imported to ensure import('./basic-module-2.js') fails even if +// it has been previously statically imported. +import './resources/basic-module-2.js'; + +import './resources/no-dynamic-import.js'; diff --git a/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import.any.js b/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import.any.js new file mode 100644 index 0000000000..25b370b709 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import.any.js @@ -0,0 +1,3 @@ +// META: global=serviceworker + +importScripts('resources/no-dynamic-import.js'); diff --git a/testing/web-platform/tests/service-workers/service-worker/onactivate-script-error.https.html b/testing/web-platform/tests/service-workers/service-worker/onactivate-script-error.https.html new file mode 100644 index 0000000000..f5e80bb9a4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/onactivate-script-error.https.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +function wait_for_install(worker) { + return new Promise(function(resolve, reject) { + worker.addEventListener('statechange', function(event) { + if (worker.state == 'installed') + resolve(); + else if (worker.state == 'redundant') + reject(); + }); + }); +} + +function wait_for_activate(worker) { + return new Promise(function(resolve, reject) { + worker.addEventListener('statechange', function(event) { + if (worker.state == 'activated') + resolve(); + else if (worker.state == 'redundant') + reject(); + }); + }); +} + +function make_test(name, script) { + promise_test(function(t) { + var scope = script; + var registration; + return service_worker_unregister_and_register(t, script, scope) + .then(function(r) { + registration = r; + + t.add_cleanup(function() { + return r.unregister(); + }); + + return wait_for_install(registration.installing); + }) + .then(function() { + // Activate should succeed regardless of script errors. + return wait_for_activate(registration.waiting); + }); + }, name); +} + +[ + { + name: 'activate handler throws an error', + script: 'resources/onactivate-throw-error-worker.js', + }, + { + name: 'activate handler throws an error, error handler does not cancel', + script: 'resources/onactivate-throw-error-with-empty-onerror-worker.js', + }, + { + name: 'activate handler dispatches an event that throws an error', + script: 'resources/onactivate-throw-error-from-nested-event-worker.js', + }, + { + name: 'activate handler throws an error that is cancelled', + script: 'resources/onactivate-throw-error-then-cancel-worker.js', + }, + { + name: 'activate handler throws an error and prevents default', + script: 'resources/onactivate-throw-error-then-prevent-default-worker.js', + } +].forEach(function(test_case) { + make_test(test_case.name, test_case.script); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/oninstall-script-error.https.html b/testing/web-platform/tests/service-workers/service-worker/oninstall-script-error.https.html new file mode 100644 index 0000000000..fe7f6e9012 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/oninstall-script-error.https.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +function wait_for_install_event(worker) { + return new Promise(function(resolve) { + worker.addEventListener('statechange', function(event) { + if (worker.state == 'installed') + resolve(true); + else if (worker.state == 'redundant') + resolve(false); + }); + }); +} + +function make_test(name, script, expect_install) { + promise_test(function(t) { + var scope = script; + return service_worker_unregister_and_register(t, script, scope) + .then(function(registration) { + return wait_for_install_event(registration.installing); + }) + .then(function(did_install) { + assert_equals(did_install, expect_install, + 'The worker was installed'); + }) + }, name); +} + +[ + { + name: 'install handler throws an error', + script: 'resources/oninstall-throw-error-worker.js', + expect_install: true + }, + { + name: 'install handler throws an error, error handler does not cancel', + script: 'resources/oninstall-throw-error-with-empty-onerror-worker.js', + expect_install: true + }, + { + name: 'install handler dispatches an event that throws an error', + script: 'resources/oninstall-throw-error-from-nested-event-worker.js', + expect_install: true + }, + { + name: 'install handler throws an error in the waitUntil', + script: 'resources/oninstall-waituntil-throw-error-worker.js', + expect_install: false + }, + + // The following two cases test what happens when the ServiceWorkerGlobalScope + // 'error' event handler cancels the resulting error event. Since the + // original 'install' event handler through, the installation should still + // be stopped in this case. See: + // https://github.com/slightlyoff/ServiceWorker/issues/778 + { + name: 'install handler throws an error that is cancelled', + script: 'resources/oninstall-throw-error-then-cancel-worker.js', + expect_install: true + }, + { + name: 'install handler throws an error and prevents default', + script: 'resources/oninstall-throw-error-then-prevent-default-worker.js', + expect_install: true + } +].forEach(function(test_case) { + make_test(test_case.name, test_case.script, test_case.expect_install); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/opaque-response-preloaded.https.html b/testing/web-platform/tests/service-workers/service-worker/opaque-response-preloaded.https.html new file mode 100644 index 0000000000..417aa4ebec --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/opaque-response-preloaded.https.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Opaque responses should not be reused for XHRs</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +const WORKER = + 'resources/opaque-response-preloaded-worker.js'; + +var done; + +// These test that the browser does not inappropriately use a cached opaque +// response for a request that is not no-cors. The test opens a controlled +// iframe that uses link rel=preload to issue a same-origin no-cors request. +// The service worker responds to the request with an opaque response. Then the +// iframe does an XHR (not no-cors) to that URL again. The request should fail. +promise_test(t => { + const SCOPE = + 'resources/opaque-response-being-preloaded-xhr.html'; + const promise = new Promise(resolve => done = resolve); + + return service_worker_unregister_and_register(t, WORKER, SCOPE) + .then(reg => { + add_completion_callback(() => reg.unregister()); + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(() => with_iframe(SCOPE)) + .then(frame => t.add_cleanup(() => frame.remove() )) + .then(() => promise) + .then(result => assert_equals(result, 'PASS')); + }, 'Opaque responses should not be reused for XHRs, loading case'); + +promise_test(t => { + const SCOPE = + 'resources/opaque-response-preloaded-xhr.html'; + const promise = new Promise(resolve => done = resolve); + + return service_worker_unregister_and_register(t, WORKER, SCOPE) + .then(reg => { + add_completion_callback(() => reg.unregister()); + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(() => with_iframe(SCOPE)) + .then(frame => t.add_cleanup(() => frame.remove() )) + .then(() => promise) + .then(result => assert_equals(result, 'PASS')); + }, 'Opaque responses should not be reused for XHRs, done case'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/opaque-script.https.html b/testing/web-platform/tests/service-workers/service-worker/opaque-script.https.html new file mode 100644 index 0000000000..7d2121855d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/opaque-script.https.html @@ -0,0 +1,71 @@ +<!doctype html> +<title>Cache Storage: verify scripts loaded from cache_storage are marked opaque</title> +<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script> +'use strict'; + +const SW_URL = 'resources/opaque-script-sw.js'; +const BASE_SCOPE = './resources/opaque-script-frame.html'; +const SAME_ORIGIN_BASE = new URL('./resources/', self.location.href).href; +const CROSS_ORIGIN_BASE = new URL('./resources/', + get_host_info().HTTPS_REMOTE_ORIGIN + base_path()).href; + +function wait_for_error() { + return new Promise(resolve => { + self.addEventListener('message', function messageHandler(evt) { + if (evt.data.type !== 'ErrorEvent') + return; + self.removeEventListener('message', messageHandler); + resolve(evt.data.msg); + }); + }); +} + +// Load an iframe that dynamically adds a script tag that is +// same/cross origin and large/small. It then calls a function +// defined in that loaded script that throws an unhandled error. +// The resulting message exposed in the global onerror handler +// is reported back from this function. Opaque cross origin +// scripts should not expose the details of the uncaught exception. +async function get_error_message(t, mode, size) { + const script_base = mode === 'same-origin' ? SAME_ORIGIN_BASE + : CROSS_ORIGIN_BASE; + const script = script_base + `opaque-script-${size}.js`; + const scope = BASE_SCOPE + `?script=${script}`; + const reg = await service_worker_unregister_and_register(t, SW_URL, scope); + t.add_cleanup(_ => reg.unregister()); + assert_true(!!reg.installing); + await wait_for_state(t, reg.installing, 'activated'); + const error_promise = wait_for_error(); + const f = await with_iframe(scope); + t.add_cleanup(_ => f.remove()); + const error = await error_promise; + return error; +} + +promise_test(async t => { + const error = await get_error_message(t, 'same-origin', 'small'); + assert_true(error.includes('Intentional error')); +}, 'Verify small same-origin cache_storage scripts are not opaque.'); + +promise_test(async t => { + const error = await get_error_message(t, 'same-origin', 'large'); + assert_true(error.includes('Intentional error')); +}, 'Verify large same-origin cache_storage scripts are not opaque.'); + +promise_test(async t => { + const error = await get_error_message(t, 'cross-origin', 'small'); + assert_false(error.includes('Intentional error')); +}, 'Verify small cross-origin cache_storage scripts are opaque.'); + +promise_test(async t => { + const error = await get_error_message(t, 'cross-origin', 'large'); + assert_false(error.includes('Intentional error')); +}, 'Verify large cross-origin cache_storage scripts are opaque.'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/partitioned-claim.tentative.https.html b/testing/web-platform/tests/service-workers/service-worker/partitioned-claim.tentative.https.html new file mode 100644 index 0000000000..1f42c528e0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/partitioned-claim.tentative.https.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<title>Service Worker: Partitioned Service Workers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/partitioned-utils.js"></script> + +<body> +This test creates a iframe in a first-party context and then registers a +service worker (such that the iframe client is unclaimed). +A third-party iframe is then created which has its SW call clients.claim() +and then the test checks that the 1p iframe was not claimed int he process. +Finally the test has its SW call clients.claim() and confirms the 1p iframe is +claimed. + +<script> +promise_test(async t => { + const script = './resources/partitioned-storage-sw.js'; + const scope = './resources/partitioned-'; + + // Add a 1p iframe. + const wait_frame_url = new URL( + './resources/partitioned-service-worker-iframe-claim.html?1p-mode', + self.location); + + const frame = await with_iframe(wait_frame_url, false); + t.add_cleanup(async () => { + frame.remove(); + }); + + // Add service worker to this 1P context. + const reg = await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + // Register the message listener. + self.addEventListener('message', messageEventHandler); + + // Now we need to create a third-party iframe whose SW will claim it and then + // the iframe will postMessage that its serviceWorker.controller state has + // changed. + const third_party_iframe_url = new URL( + './resources/partitioned-service-worker-iframe-claim.html?3p-mode', + get_host_info().HTTPS_ORIGIN + self.location.pathname); + + // Create the 3p window (which will in turn create the iframe with the SW) + // and await on its data. + const frame_3p_data = await loadAndReturnSwData(t, third_party_iframe_url, + 'window'); + assert_equals(frame_3p_data.status, "success", + "3p iframe was successfully claimed"); + + // Confirm that the 1p iframe wasn't claimed at the same time. + const controller_1p_iframe = makeMessagePromise(); + frame.contentWindow.postMessage({type: "get-controller"}); + const controller_1p_iframe_data = await controller_1p_iframe; + assert_equals(controller_1p_iframe_data.controller, null, + "Test iframe client isn't claimed yet."); + + + // Tell the SW to claim. + const claimed_1p_iframe = makeMessagePromise(); + reg.active.postMessage({type: "claim"}); + const claimed_1p_iframe_data = await claimed_1p_iframe; + + assert_equals(claimed_1p_iframe_data.status, "success", + "iframe client was successfully claimed."); + +}, "ServiceWorker's clients.claim() is partitioned"); +</script> + +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html b/testing/web-platform/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html new file mode 100644 index 0000000000..7a8bd3f5cf --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8"/> +<meta name="timeout" content="long"> +<title>Service Worker: Partitioned Cookies</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/partitioned-cookies-test-helpers.js"></script> +</head> + +<!-- + This test exercises partitioned service workers' interaction with partitioned + cookies. Partitioned service workers should only be able to interact with + partitioned cookies whose partition key matches the worker's partition. +--> + +<body> +<script> + +promise_test(async t => { + const script = './resources/partitioned-cookies-sw.js' + const scope = './resources/partitioned-cookies-' + 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')); + + // Set an unpartitioned cookie. + document.cookie = 'unpartitioned=456; Secure; Path=/; SameSite=None;'; + assert_true(document.cookie.includes('unpartitioned=456')); + + const reg = await service_worker_unregister_and_register(t, script, scope); + await wait_for_state(t, reg.installing, 'activated'); + t.add_cleanup(() => reg.unregister()); + + const next_message = worker_message_generator(); + + 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 == absolute_scope); + + // First test that the worker script started correctly and message passing + // is enabed. + filtered_registrations[0].active.postMessage({type: 'test_message'}); + const msg1 = await next_message(); + assert_true(msg1.ok, 'Message passing'); + + // Test that the partitioned cookie is available to this worker via HTTP. + filtered_registrations[0].active.postMessage({type: 'echo_cookies_http'}); + const msg2 = await next_message(); + assert_true(msg2.ok, 'Get cookies'); + assert_true( + msg2.cookies.includes('__Host-partitioned'), + 'Can access partitioned cookie via HTTP'); + assert_true( + msg2.cookies.includes('unpartitioned'), + 'Can access unpartitioned cookie via HTTP'); + + // Test that the partitioned cookie is available to this worker via + // CookieStore API. + filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'}); + const msg3 = await next_message(); + assert_true(msg3.ok, 'Get cookies'); + assert_true( + msg3.cookies.includes('__Host-partitioned'), + 'Can access partitioned cookie via JS'); + assert_true( + msg3.cookies.includes('unpartitioned'), + 'Can access unpartitioned cookie via JS'); + + // Test that the partitioned cookie is not available to this worker in HTTP + // requests from importScripts. + filtered_registrations[0].active.postMessage({type: 'echo_cookies_import'}); + const msg4 = await next_message(); + assert_true(msg4.ok, 'Get cookies'); + assert_true( + msg4.cookies.includes('__Host-partitioned'), + 'Can access partitioned cookie via importScripts'); + assert_true( + msg4.cookies.includes('unpartitioned'), + 'Can access unpartitioned cookie via importScripts'); + + const popup = window.open( + new URL( + `./resources/partitioned-cookies-3p-window.html?origin=${ + encodeURIComponent(self.location.origin)}`, + get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname)); + await fetch_tests_from_window(popup); +}); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html b/testing/web-platform/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html new file mode 100644 index 0000000000..7c4d4f1e02 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<title>Service Worker: Partitioned Service Workers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/partitioned-utils.js"></script> + +<body> +This test loads a SW in a first-party context and gets the SW's (randomly) +generated ID. It does the same thing for the SW but in a third-party context +and then confirms that the IDs are different. + +<script> +promise_test(async t => { + const script = './resources/partitioned-storage-sw.js' + const scope = './resources/partitioned-' + const absoluteScope = new URL(scope, window.location).href; + + // Add service worker to this 1P context. + const reg = await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + // Register the message listener. + self.addEventListener('message', messageEventHandler); + + // Open an iframe that will create a promise within the SW. + // The query param is there to track which request the service worker is + // handling. + // + // This promise is necessary to prevent the service worker from being + // shutdown during the test which would cause a new ID to be generated + // and thus invalidate the test. + const wait_frame_url = new URL( + './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame', + self.location); + + // We don't really need the data the SW sent us from this request + // but we can use the ID to confirm the SW wasn't shut down during the + // test. + const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url, + 'iframe'); + + // Now we need to create a third-party iframe that will send us its SW's + // ID. + const third_party_iframe_url = new URL( + './resources/partitioned-service-worker-third-party-iframe-getRegistrations.html', + get_host_info().HTTPS_ORIGIN + self.location.pathname); + + // Create the 3p window (which will in turn create the iframe with the SW) + // and await on its data. + const frame_3p_ID = await loadAndReturnSwData(t, third_party_iframe_url, + 'window'); + + // Now get this frame's SW's ID. + const frame_1p_ID_promise = makeMessagePromise(); + + 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); + + // Register a listener on the service worker container and then forward to + // the self event listener so we can reuse the existing message promise + // function. + navigator.serviceWorker.addEventListener('message', evt => { + self.postMessage(evt.data, '*'); + }); + + filtered_registrations[0].active.postMessage({type: "get-id"}); + + const frame_1p_ID = await frame_1p_ID_promise; + + // First check that the SW didn't shutdown during the run of the test. + // (Note: We're not using assert_equals because random values make it + // difficult to use a test expectations file.) + assert_true(wait_frame_1p_data.ID === frame_1p_ID.ID, + "1p SW didn't shutdown"); + // Now check that the 1p and 3p IDs differ. + assert_false(frame_1p_ID.ID === frame_3p_ID.ID, + "1p SW ID matches 3p SW ID"); + + // Finally, for clean up, resolve the SW's promise so it stops waiting. + const resolve_frame_url = new URL( + './resources/partitioned-resolve.fakehtml?From1pFrame', self.location); + + // We don't care about the data. + await loadAndReturnSwData(t, resolve_frame_url, 'iframe'); + +}, "ServiceWorker's getRegistrations() is partitioned"); + + +</script> + +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html b/testing/web-platform/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html new file mode 100644 index 0000000000..46beec819c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<title>Service Worker: Partitioned Service Workers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/partitioned-utils.js"></script> + +<body> +This test loads a SW in a first-party context and gets has the SW send +its list of clients from client.matchAll(). It does the same thing for the +SW in a third-party context as well and confirms that each SW see's the correct +clients and that they don't see eachother's clients. + +<script> +promise_test(async t => { + + const script = './resources/partitioned-storage-sw.js' + const scope = './resources/partitioned-' + + // Add service worker to this 1P context. + const reg = await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + // Register the message listener. + self.addEventListener('message', messageEventHandler); + + // Create a third-party iframe that will send us its SW's clients. + const third_party_iframe_url = new URL( + './resources/partitioned-service-worker-third-party-iframe-matchAll.html', + get_host_info().HTTPS_ORIGIN + self.location.pathname); + + const {urls_list: frame_3p_urls_list} = await loadAndReturnSwData(t, + third_party_iframe_url, 'window'); + + // Register a listener on the service worker container and then forward to + // the self event listener so we can reuse the existing message promise + // function. + navigator.serviceWorker.addEventListener('message', evt => { + self.postMessage(evt.data, '*'); + }); + + const frame_1p_data_promise = makeMessagePromise(); + + reg.active.postMessage({type: "get-match-all"}); + + const {urls_list: frame_1p_urls_list} = await frame_1p_data_promise; + + // If partitioning is working, the 1p and 3p SWs should only see a single + // client. + assert_equals(frame_3p_urls_list.length, 1); + assert_equals(frame_1p_urls_list.length, 1); + // Confirm that the expected URL was seen by each. + assert_equals(frame_3p_urls_list[0], third_party_iframe_url.toString(), + "3p SW has the correct client url."); + assert_equals(frame_1p_urls_list[0], window.location.href, + "1P SW has the correct client url."); +}, "ServiceWorker's matchAll() is partitioned"); + + +</script> + +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/partitioned.tentative.https.html b/testing/web-platform/tests/service-workers/service-worker/partitioned.tentative.https.html new file mode 100644 index 0000000000..17a375f9c7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/partitioned.tentative.https.html @@ -0,0 +1,188 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<title>Service Worker: Partitioned Service Workers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/partitioned-utils.js"></script> + +<body> + <!-- Debugging text for both test cases --> + The 3p iframe's postMessage: + <p id="iframe_response">No message received</p> + + The nested iframe's postMessage: + <p id="nested_iframe_response">No message received</p> + +<script> +promise_test(async t => { + const script = './resources/partitioned-storage-sw.js' + const scope = './resources/partitioned-' + + // Add service worker to this 1P context. wait_for_state() and + // service_worker_unregister_and_register() are helper functions + // for creating test ServiceWorkers defined in: + // service-workers/service-worker/resources/test-helpers.sub.js + const reg = await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + // Registers the message listener with messageEventHandler(), defined in: + // service-workers/service-worker/resources/partitioned-utils.js + self.addEventListener('message', messageEventHandler); + + // Open an iframe that will create a promise within the SW. + // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js: + // `waitUntilResolved.fakehtml`: URL scope that creates the promise. + // `?From1pFrame`: query param that tracks which request the service worker is + // handling. + const wait_frame_url = new URL( + './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame', + self.location); + + // Loads a child iframe with wait_frame_url as the content and returns + // a promise for the data messaged from the loaded iframe. + // loadAndReturnSwData() defined in: + // service-workers/service-worker/resources/partitioned-utils.js: + const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url, + 'iframe'); + assert_equals(wait_frame_1p_data.source, 'From1pFrame', + 'The data for the 1p frame came from the wrong source'); + + // Now create a 3p iframe that will try to resolve the SW in a 3p context. + const third_party_iframe_url = new URL( + './resources/partitioned-service-worker-third-party-iframe.html', + get_host_info().HTTPS_ORIGIN + self.location.pathname); + + // loadAndReturnSwData() creates a HTTPS_NOTSAMESITE_ORIGIN or 3p `window` + // element which embeds an iframe with the ServiceWorker and returns + // a promise of the data messaged from that frame. + const frame_3p_data = await loadAndReturnSwData(t, third_party_iframe_url, 'window'); + assert_equals(frame_3p_data.source, 'From3pFrame', + 'The data for the 3p frame came from the wrong source'); + + // Print some debug info to the main frame. + document.getElementById("iframe_response").innerHTML = + "3p iframe's has_pending: " + frame_3p_data.has_pending + " source: " + + frame_3p_data.source + ". "; + + // Now do the same for the 1p iframe. + // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js: + // `resolve.fakehtml`: URL scope that resolves the promise. + const resolve_frame_url = new URL( + './resources/partitioned-resolve.fakehtml?From1pFrame', self.location); + + const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url, + 'iframe'); + assert_equals(frame_1p_data.source, 'From1pFrame', + 'The data for the 1p frame came from the wrong source'); + // Both the 1p frames should have been serviced by the same service worker ID. + // If this isn't the case then that means the SW could have been deactivated + // which invalidates the test. + assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID, + 'The 1p frames were serviced by different service workers.'); + + document.getElementById("iframe_response").innerHTML += + "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " + + frame_1p_data.source; + + // If partitioning is working correctly then only the 1p iframe should see + // (and resolve) its SW's promise. Additionally the two frames should see + // different IDs. + assert_true(frame_1p_data.has_pending, + 'The 1p iframe saw a pending promise in the service worker.'); + assert_false(frame_3p_data.has_pending, + 'The 3p iframe saw a pending promise in the service worker.'); + assert_not_equals(frame_1p_data.ID, frame_3p_data.ID, + 'The frames were serviced by the same service worker thread.'); +}, 'Services workers under different top-level sites are partitioned.'); + +// Optional Test: Checking for partitioned ServiceWorkers in an A->B->A +// (nested-iframes with cross-site ancestor) scenario. +promise_test(async t => { + const script = './resources/partitioned-storage-sw.js' + const scope = './resources/partitioned-' + + // Add service worker to this 1P context. wait_for_state() and + // service_worker_unregister_and_register() are helper functions + // for creating test ServiceWorkers defined in: + // service-workers/service-worker/resources/test-helpers.sub.js + const reg = await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + // Registers the message listener with messageEventHandler(), defined in: + // service-workers/service-worker/resources/partitioned-utils.js + self.addEventListener('message', messageEventHandler); + + // Open an iframe that will create a promise within the SW. + // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js: + // `waitUntilResolved.fakehtml`: URL scope that creates the promise. + // `?From1pFrame`: query param that tracks which request the service worker is + // handling. + const wait_frame_url = new URL( + './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame', + self.location); + + // Load a child iframe with wait_frame_url as the content. + // loadAndReturnSwData() defined in: + // service-workers/service-worker/resources/partitioned-utils.js: + const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url, + 'iframe'); + assert_equals(wait_frame_1p_data.source, 'From1pFrame', + 'The data for the 1p frame came from the wrong source'); + + // Now create a set of nested iframes in the configuration A1->B->A2 + // where B is cross-site and A2 is same-site to this top-level + // site (A1). The innermost iframe of the nested iframes (A2) will + // create an additional iframe to finally resolve the ServiceWorker. + const nested_iframe_url = new URL( + './resources/partitioned-service-worker-nested-iframe-parent.html', + get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname); + + // Create the nested iframes (which will in turn create the iframe + // with the ServiceWorker) and await on receiving its data. + const nested_iframe_data = await loadAndReturnSwData(t, nested_iframe_url, 'iframe'); + assert_equals(nested_iframe_data.source, 'FromNestedFrame', + 'The data for the nested iframe frame came from the wrong source'); + + // Print some debug info to the main frame. + document.getElementById("nested_iframe_response").innerHTML = + "Nested iframe's has_pending: " + nested_iframe_data.has_pending + " source: " + + nested_iframe_data.source + ". "; + + // Now do the same for the 1p iframe. + // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js: + // `resolve.fakehtml`: URL scope that resolves the promise. + const resolve_frame_url = new URL( + './resources/partitioned-resolve.fakehtml?From1pFrame', self.location); + + const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url, + 'iframe'); + assert_equals(frame_1p_data.source, 'From1pFrame', + 'The data for the 1p frame came from the wrong source'); + // Both the 1p frames should have been serviced by the same service worker ID. + // If this isn't the case then that means the SW could have been deactivated + // which invalidates the test. + assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID, + 'The 1p frames were serviced by different service workers.'); + + document.getElementById("nested_iframe_response").innerHTML += + "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " + + frame_1p_data.source; + + // If partitioning is working correctly then only the 1p iframe should see + // (and resolve) its SW's promise. Additionally, the innermost iframe of + // the nested iframes (A2 in the configuration A1->B->A2) should have a + // different service worker ID than the 1p (A1) frame. + assert_true(frame_1p_data.has_pending, + 'The 1p iframe saw a pending promise in the service worker.'); + assert_false(nested_iframe_data.has_pending, + 'The 3p iframe saw a pending promise in the service worker.'); + assert_not_equals(frame_1p_data.ID, nested_iframe_data.ID, + 'The frames were serviced by the same service worker thread.'); +}, 'Services workers with cross-site ancestors are partitioned.'); + +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/service-workers/service-worker/performance-timeline.https.html b/testing/web-platform/tests/service-workers/service-worker/performance-timeline.https.html new file mode 100644 index 0000000000..e56e6fe416 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/performance-timeline.https.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +service_worker_test( + 'resources/performance-timeline-worker.js', + 'Test Performance Timeline API in Service Worker'); + +// The purpose of this test is to verify that service worker overhead +// is included in the Performance API's timing information. +promise_test(t => { + let script = 'resources/empty-but-slow-worker.js'; + let scope = 'resources/sample.txt?slow-sw-timing'; + let url = new URL(scope, window.location).href; + let slowURL = url + '&slow'; + let frame; + return service_worker_unregister_and_register(t, script, scope) + .then(reg => { + t.add_cleanup(() => service_worker_unregister(t, scope)); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(_ => with_iframe(scope)) + .then(f => { + frame = f; + return frame.contentWindow.fetch(url).then(r => r && r.text()); + }) + .then(_ => { + return frame.contentWindow.fetch(slowURL).then(r => r && r.text()); + }) + .then(_ => { + function elapsed(u) { + let entry = frame.contentWindow.performance.getEntriesByName(u); + return entry[0] ? entry[0].duration : undefined; + } + let urlTime = elapsed(url); + let slowURLTime = elapsed(slowURL); + // Verify the request slowed by the service worker is indeed measured + // to be slower. Note, we compare to smaller delay instead of the exact + // delay amount to avoid making the test racy under automation. + assert_greater_than(slowURLTime, urlTime + 1000, + 'Slow service worker request should measure increased delay.'); + frame.remove(); + }) +}, 'empty service worker fetch event included in performance timings'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/postMessage-client-worker.js b/testing/web-platform/tests/service-workers/service-worker/postMessage-client-worker.js new file mode 100644 index 0000000000..64d944d2b5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/postMessage-client-worker.js @@ -0,0 +1,23 @@ +async function doTest(e) +{ + if (e.resultingClientId) { + const promise = new Promise(async resolve => { + let counter = 0; + const client = await self.clients.get(e.resultingClientId); + if (client) + client.postMessage(counter++); + if (e.request.url.includes("repeatMessage")) { + setInterval(() => { + if (client) + client.postMessage(counter++); + }, 100); + } + setTimeout(() => { + resolve(fetch(e.request)); + }, 1000); + }); + e.respondWith(promise); + } +} + +self.addEventListener("fetch", e => e.waitUntil(doTest(e))); diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage-blob-url.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage-blob-url.https.html new file mode 100644 index 0000000000..16fddd57b8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/postmessage-blob-url.https.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<title>Service Worker: postMessage Blob URL</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(t => { + let script = 'resources/postmessage-blob-url.js'; + let scope = 'resources/blank.html'; + let registration; + let blobText = 'Blob text'; + let blob; + let blobUrl; + + return service_worker_unregister_and_register(t, script, scope) + .then(r => { + add_completion_callback(() => r.unregister()); + registration = r; + let worker = registration.installing; + blob = new Blob([blobText]); + blobUrl = URL.createObjectURL(blob); + return new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { resolve(e.data); } + worker.postMessage(blobUrl); + }); + }) + .then(response => { + assert_equals(response, 'Worker reply:' + blobText); + URL.revokeObjectURL(blobUrl); + return registration.unregister(); + }); + }, 'postMessage Blob URL to a ServiceWorker'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html new file mode 100644 index 0000000000..117def9eb2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<title>Service Worker: postMessage from waiting serviceworker</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +function echo(worker, data) { + return new Promise(resolve => { + navigator.serviceWorker.addEventListener('message', function onMsg(evt) { + navigator.serviceWorker.removeEventListener('message', onMsg); + resolve(evt); + }); + worker.postMessage(data); + }); +} + +promise_test(t => { + let script = 'resources/echo-message-to-source-worker.js'; + let scope = 'resources/client-postmessage-from-wait-serviceworker'; + let registration; + let frame; + return service_worker_unregister_and_register(t, script, scope) + .then(swr => { + t.add_cleanup(() => service_worker_unregister(t, scope)); + + registration = swr; + return wait_for_state(t, registration.installing, 'activated'); + }).then(_ => { + return with_iframe(scope); + }).then(f => { + frame = f; + return navigator.serviceWorker.register(script + '?update', { scope: scope }) + }).then(swr => { + assert_equals(swr, registration, 'should be same registration'); + return wait_for_state(t, registration.installing, 'installed'); + }).then(_ => { + return echo(registration.waiting, 'waiting'); + }).then(evt => { + assert_equals(evt.source, registration.waiting, + 'message event source should be correct'); + return echo(registration.active, 'active'); + }).then(evt => { + assert_equals(evt.source, registration.active, + 'message event source should be correct'); + frame.remove(); + }); +}, 'Client.postMessage() from waiting serviceworker.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html new file mode 100644 index 0000000000..29c056080c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<title>Service Worker: postMessage via MessagePort to Client</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(t => { + var script = 'resources/postmessage-msgport-to-client-worker.js'; + var scope = 'resources/blank.html'; + var port; + + return service_worker_unregister_and_register(t, script, scope) + .then(registration => { + add_completion_callback(() => registration.unregister()); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => with_iframe(scope)) + .then(frame => { + t.add_cleanup(() => frame.remove()); + return new Promise(resolve => { + var w = frame.contentWindow; + w.navigator.serviceWorker.onmessage = resolve; + w.navigator.serviceWorker.controller.postMessage('ping'); + }); + }) + .then(e => { + port = e.ports[0]; + port.postMessage({value: 1}); + port.postMessage({value: 2}); + port.postMessage({done: true}); + return new Promise(resolve => { port.onmessage = resolve; }); + }) + .then(e => { + assert_equals(e.data.ack, 'Acking value: 1'); + return new Promise(resolve => { port.onmessage = resolve; }); + }) + .then(e => { + assert_equals(e.data.ack, 'Acking value: 2'); + return new Promise(resolve => { port.onmessage = resolve; }); + }) + .then(e => { assert_true(e.data.done, 'done'); }); + }, 'postMessage MessagePorts from ServiceWorker to Client'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html new file mode 100644 index 0000000000..83e5f4540d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html @@ -0,0 +1,212 @@ +<!DOCTYPE html> +<title>Service Worker: postMessage to Client (message queue)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +// This function creates a message listener that captures all messages +// sent to this window and matches them with corresponding requests. +// This frees test code from having to use clunky constructs just to +// avoid race conditions, since the relative order of message and +// request arrival doesn't matter. +function create_message_listener(t) { + const listener = { + messages: new Set(), + requests: new Set(), + waitFor: function(predicate) { + for (const event of this.messages) { + // If a message satisfying the predicate has already + // arrived, it gets matched to this request. + if (predicate(event)) { + this.messages.delete(event); + return Promise.resolve(event); + } + } + + // If no match was found, the request is stored and a + // promise is returned. + const request = { predicate }; + const promise = new Promise(resolve => request.resolve = resolve); + this.requests.add(request); + return promise; + } + }; + window.onmessage = t.step_func(event => { + for (const request of listener.requests) { + // If the new message matches a stored request's + // predicate, the request's promise is resolved with this + // message. + if (request.predicate(event)) { + listener.requests.delete(request); + request.resolve(event); + return; + } + }; + + // No outstanding request for this message, store it in case + // it's requested later. + listener.messages.add(event); + }); + return listener; +} + +async function service_worker_register_and_activate(t, script, scope) { + const registration = await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => registration.unregister()); + const worker = registration.installing; + await wait_for_state(t, worker, 'activated'); + return worker; +} + +// Add an iframe (parent) whose document contains a nested iframe +// (child), then set the child's src attribute to child_url and return +// its Window (without waiting for it to finish loading). +async function with_nested_iframes(t, child_url) { + const parent = await with_iframe('resources/nested-iframe-parent.html?role=parent'); + t.add_cleanup(() => parent.remove()); + const child = parent.contentWindow.document.getElementById('child'); + child.setAttribute('src', child_url); + return child.contentWindow; +} + +// Returns a predicate matching a fetch message with the specified +// key. +function fetch_message(key) { + return event => event.data.type === 'fetch' && event.data.key === key; +} + +// Returns a predicate matching a ping message with the specified +// payload. +function ping_message(data) { + return event => event.data.type === 'ping' && event.data.data === data; +} + +// A client message queue test is a testharness.js test with some +// additional setup: +// 1. A listener (see create_message_listener) +// 2. An active service worker +// 3. Two nested iframes +// 4. A state transition function that controls the order of events +// during the test +function client_message_queue_test(url, test_function, description) { + promise_test(async t => { + t.listener = create_message_listener(t); + + const script = 'resources/stalling-service-worker.js'; + const scope = 'resources/'; + t.service_worker = await service_worker_register_and_activate(t, script, scope); + + // We create two nested iframes such that both are controlled by + // the newly installed service worker. + const child_url = url + '?role=child'; + t.frame = await with_nested_iframes(t, child_url); + + t.state_transition = async function(from, to, scripts) { + // A state transition begins with the child's parser + // fetching a script due to a <script> tag. The request + // arrives at the service worker, which notifies the + // parent, which in turn notifies the test. Note that the + // event loop keeps spinning while the parser is waiting. + const request = await this.listener.waitFor(fetch_message(to)); + + // The test instructs the service worker to send two ping + // messages through the Client interface: first to the + // child, then to the parent. + this.service_worker.postMessage(from); + + // When the parent receives the ping message, it forwards + // it to the test. Assuming that messages to both child + // and parent are mapped to the same task queue (this is + // not [yet] required by the spec), receiving this message + // guarantees that the child has already dispatched its + // message if it was allowed to do so. + await this.listener.waitFor(ping_message(from)); + + // Finally, reply to the service worker's fetch + // notification with the script it should use as the fetch + // request's response. This is a defensive mechanism that + // ensures the child's parser really is blocked until the + // test is ready to continue. + request.ports[0].postMessage([`state = '${to}';`].concat(scripts)); + }; + + await test_function(t); + }, description); +} + +function client_message_queue_enable_test( + install_script, + start_script, + earliest_dispatch, + description) +{ + function assert_state_less_than_equal(state1, state2, explanation) { + const states = ['init', 'install', 'start', 'finish', 'loaded']; + const index1 = states.indexOf(state1); + const index2 = states.indexOf(state2); + if (index1 > index2) + assert_unreached(explanation); + } + + client_message_queue_test('enable-client-message-queue.html', async t => { + // While parsing the child's document, the child transitions + // from the 'init' state all the way to the 'finish' state. + // Once parsing is finished it would enter the final 'loaded' + // state. All but the last transition require assitance from + // the test. + await t.state_transition('init', 'install', [install_script]); + await t.state_transition('install', 'start', [start_script]); + await t.state_transition('start', 'finish', []); + + // Wait for all messages to get dispatched on the child's + // ServiceWorkerContainer and then verify that each message + // was dispatched after |earliest_dispatch|. + const report = await t.frame.report; + ['init', 'install', 'start'].forEach(state => { + const explanation = `Message sent in state '${state}' was dispatched in '${report[state]}', should be dispatched no earlier than '${earliest_dispatch}'`; + assert_state_less_than_equal(earliest_dispatch, + report[state], + explanation); + }); + }, description); +} + +const empty_script = ``; + +const add_event_listener = + `navigator.serviceWorker.addEventListener('message', handle_message);`; + +const set_onmessage = `navigator.serviceWorker.onmessage = handle_message;`; + +const start_messages = `navigator.serviceWorker.startMessages();`; + +client_message_queue_enable_test(add_event_listener, empty_script, 'loaded', + 'Messages from ServiceWorker to Client only received after DOMContentLoaded event.'); + +client_message_queue_enable_test(add_event_listener, start_messages, 'start', + 'Messages from ServiceWorker to Client only received after calling startMessages().'); + +client_message_queue_enable_test(set_onmessage, empty_script, 'install', + 'Messages from ServiceWorker to Client only received after setting onmessage.'); + +const resolve_manual_promise = `resolve_manual_promise();` + +async function test_microtasks_when_client_message_queue_enabled(t, scripts) { + await t.state_transition('init', 'start', scripts.concat([resolve_manual_promise])); + let result = await t.frame.result; + assert_equals(result[0], 'microtask', 'The microtask was executed first.'); + assert_equals(result[1], 'message', 'The message was dispatched.'); +} + +client_message_queue_test('message-vs-microtask.html', t => { + return test_microtasks_when_client_message_queue_enabled(t, [ + add_event_listener, + start_messages, + ]); +}, 'Microtasks run before dispatching messages after calling startMessages().'); + +client_message_queue_test('message-vs-microtask.html', t => { + return test_microtasks_when_client_message_queue_enabled(t, [set_onmessage]); +}, 'Microtasks run before dispatching messages after setting onmessage.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client.https.html new file mode 100644 index 0000000000..f834a4bffe --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client.https.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<title>Service Worker: postMessage to Client</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(async t => { + const script = 'resources/postmessage-to-client-worker.js'; + const scope = 'resources/blank.html'; + + const registration = + await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + const w = frame.contentWindow; + + w.navigator.serviceWorker.controller.postMessage('ping'); + let e = await new Promise(r => w.navigator.serviceWorker.onmessage = r); + + assert_equals(e.constructor, w.MessageEvent, + 'message events should use MessageEvent interface.'); + assert_equals(e.type, 'message', 'type should be "message".'); + assert_false(e.bubbles, 'message events should not bubble.'); + assert_false(e.cancelable, 'message events should not be cancelable.'); + assert_equals(e.origin, location.origin, + 'origin of message should be origin of Service Worker.'); + assert_equals(e.lastEventId, '', + 'lastEventId should be an empty string.'); + assert_equals(e.source.constructor, w.ServiceWorker, + 'source should use ServiceWorker interface.'); + assert_equals(e.source, w.navigator.serviceWorker.controller, + 'source should be the service worker that sent the message.'); + assert_equals(e.ports.length, 0, 'ports should be an empty array.'); + assert_equals(e.data, 'Sending message via clients'); + + e = await new Promise(r => w.navigator.serviceWorker.onmessage = r); + assert_equals(e.data, 'quit'); +}, 'postMessage from ServiceWorker to Client.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage.https.html new file mode 100644 index 0000000000..7abb3022f9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/postmessage.https.html @@ -0,0 +1,202 @@ +<!DOCTYPE html> +<title>Service Worker: postMessage</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(t => { + var script = 'resources/postmessage-worker.js'; + var scope = 'resources/blank.html'; + var registration; + var worker; + var port; + + return service_worker_unregister_and_register(t, script, scope) + .then(r => { + t.add_cleanup(() => r.unregister()); + registration = r; + worker = registration.installing; + + var messageChannel = new MessageChannel(); + port = messageChannel.port1; + return new Promise(resolve => { + port.onmessage = resolve; + worker.postMessage({port: messageChannel.port2}, + [messageChannel.port2]); + worker.postMessage({value: 1}); + worker.postMessage({value: 2}); + worker.postMessage({done: true}); + }); + }) + .then(e => { + assert_equals(e.data, 'Acking value: 1'); + return new Promise(resolve => { port.onmessage = resolve; }); + }) + .then(e => { + assert_equals(e.data, 'Acking value: 2'); + return new Promise(resolve => { port.onmessage = resolve; }); + }) + .then(e => { + assert_equals(e.data, 'quit'); + return registration.unregister(scope); + }); + }, 'postMessage to a ServiceWorker (and back via MessagePort)'); + +promise_test(t => { + var script = 'resources/postmessage-transferables-worker.js'; + var scope = 'resources/blank.html'; + var sw = navigator.serviceWorker; + + var message = 'Hello, world!'; + var text_encoder = new TextEncoder; + var text_decoder = new TextDecoder; + + return service_worker_unregister_and_register(t, script, scope) + .then(r => { + t.add_cleanup(() => r.unregister()); + + var ab = text_encoder.encode(message); + assert_equals(ab.byteLength, message.length); + r.installing.postMessage(ab, [ab.buffer]); + assert_equals(text_decoder.decode(ab), ''); + assert_equals(ab.byteLength, 0); + + return new Promise(resolve => { sw.onmessage = resolve; }); + }) + .then(e => { + // Verify the integrity of the transferred array buffer. + assert_equals(e.data.content, message); + assert_equals(e.data.byteLength, message.length); + return new Promise(resolve => { sw.onmessage = resolve; }); + }) + .then(e => { + // Verify the integrity of the array buffer sent back from + // ServiceWorker via Client.postMessage. + assert_equals(text_decoder.decode(e.data), message); + assert_equals(e.data.byteLength, message.length); + return new Promise(resolve => { sw.onmessage = resolve; }); + }) + .then(e => { + // Verify that the array buffer on ServiceWorker is neutered. + assert_equals(e.data.content, ''); + assert_equals(e.data.byteLength, 0); + }); + }, 'postMessage a transferable ArrayBuffer between ServiceWorker and Client'); + +promise_test(t => { + var script = 'resources/postmessage-transferables-worker.js'; + var scope = 'resources/blank.html'; + var message = 'Hello, world!'; + var text_encoder = new TextEncoder; + var text_decoder = new TextDecoder; + var port; + + return service_worker_unregister_and_register(t, script, scope) + .then(r => { + t.add_cleanup(() => r.unregister()); + + var channel = new MessageChannel; + port = channel.port1; + r.installing.postMessage(undefined, [channel.port2]); + + var ab = text_encoder.encode(message); + assert_equals(ab.byteLength, message.length); + port.postMessage(ab, [ab.buffer]); + assert_equals(text_decoder.decode(ab), ''); + assert_equals(ab.byteLength, 0); + + return new Promise(resolve => { port.onmessage = resolve; }); + }) + .then(e => { + // Verify the integrity of the transferred array buffer. + assert_equals(e.data.content, message); + assert_equals(e.data.byteLength, message.length); + return new Promise(resolve => { port.onmessage = resolve; }); + }) + .then(e => { + // Verify the integrity of the array buffer sent back from + // ServiceWorker via Client.postMessage. + assert_equals(text_decoder.decode(e.data), message); + assert_equals(e.data.byteLength, message.length); + return new Promise(resolve => { port.onmessage = resolve; }); + }) + .then(e => { + // Verify that the array buffer on ServiceWorker is neutered. + assert_equals(e.data.content, ''); + assert_equals(e.data.byteLength, 0); + }); + }, 'postMessage a transferable ArrayBuffer between ServiceWorker and Client' + + ' over MessagePort'); + + promise_test(t => { + var script = 'resources/postmessage-dictionary-transferables-worker.js'; + var scope = 'resources/blank.html'; + var sw = navigator.serviceWorker; + + var message = 'Hello, world!'; + var text_encoder = new TextEncoder; + var text_decoder = new TextDecoder; + + return service_worker_unregister_and_register(t, script, scope) + .then(r => { + t.add_cleanup(() => r.unregister()); + + var ab = text_encoder.encode(message); + assert_equals(ab.byteLength, message.length); + r.installing.postMessage(ab, {transfer: [ab.buffer]}); + assert_equals(text_decoder.decode(ab), ''); + assert_equals(ab.byteLength, 0); + + return new Promise(resolve => { sw.onmessage = resolve; }); + }) + .then(e => { + // Verify the integrity of the transferred array buffer. + assert_equals(e.data.content, message); + assert_equals(e.data.byteLength, message.length); + return new Promise(resolve => { sw.onmessage = resolve; }); + }) + .then(e => { + // Verify the integrity of the array buffer sent back from + // ServiceWorker via Client.postMessage. + assert_equals(text_decoder.decode(e.data), message); + assert_equals(e.data.byteLength, message.length); + return new Promise(resolve => { sw.onmessage = resolve; }); + }) + .then(e => { + // Verify that the array buffer on ServiceWorker is neutered. + assert_equals(e.data.content, ''); + assert_equals(e.data.byteLength, 0); + }); + }, 'postMessage with dictionary a transferable ArrayBuffer between ServiceWorker and Client'); + + promise_test(async t => { + const firstScript = 'resources/postmessage-echo-worker.js?one'; + const secondScript = 'resources/postmessage-echo-worker.js?two'; + const scope = 'resources/'; + + const registration = await service_worker_unregister_and_register(t, firstScript, scope); + t.add_cleanup(() => registration.unregister()); + const firstWorker = registration.installing; + + const messagePromise = new Promise(resolve => { + navigator.serviceWorker.addEventListener('message', (event) => { + resolve(event.data); + }, {once: true}); + }); + + await wait_for_state(t, firstWorker, 'activated'); + await navigator.serviceWorker.register(secondScript, {scope}); + const secondWorker = registration.installing; + await wait_for_state(t, firstWorker, 'redundant'); + + // postMessage() to a redundant worker should be dropped silently. + // Historically, this threw an exception. + firstWorker.postMessage('firstWorker'); + + // To test somewhat that it was not received, send a message to another + // worker and check that we get a reply for that one. + secondWorker.postMessage('secondWorker'); + const data = await messagePromise; + assert_equals(data, 'secondWorker'); + }, 'postMessage to a redundant worker'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/ready.https.window.js b/testing/web-platform/tests/service-workers/service-worker/ready.https.window.js new file mode 100644 index 0000000000..6c4e270682 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/ready.https.window.js @@ -0,0 +1,223 @@ +// META: title=Service Worker: navigator.serviceWorker.ready +// META: script=resources/test-helpers.sub.js + +test(() => { + assert_equals( + navigator.serviceWorker.ready, + navigator.serviceWorker.ready, + 'repeated access to ready without intervening registrations should return the same Promise object' + ); +}, 'ready returns the same Promise object'); + +promise_test(async t => { + const frame = await with_iframe('resources/blank.html?uncontrolled'); + t.add_cleanup(() => frame.remove()); + + const promise = frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals( + Object.getPrototypeOf(promise), + frame.contentWindow.Promise.prototype, + 'the Promise should be in the context of the related document' + ); +}, 'ready returns a Promise object in the context of the related document'); + +promise_test(async t => { + const url = 'resources/empty-worker.js'; + const scope = 'resources/blank.html?ready-controlled'; + const expectedURL = normalizeURL(url); + const registration = await service_worker_unregister_and_register(t, url, scope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activated'); + + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + const readyReg = await frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals(readyReg.installing, null, 'installing should be null'); + assert_equals(readyReg.waiting, null, 'waiting should be null'); + assert_equals(readyReg.active.scriptURL, expectedURL, 'active after ready should not be null'); + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller, + readyReg.active, + 'the controller should be the active worker' + ); + assert_in_array( + readyReg.active.state, + ['activating', 'activated'], + '.ready should be resolved when the registration has an active worker' + ); +}, 'ready on a controlled document'); + +promise_test(async t => { + const url = 'resources/empty-worker.js'; + const scope = 'resources/blank.html?ready-potential-controlled'; + const expected_url = normalizeURL(url); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + const registration = await navigator.serviceWorker.register(url, { scope }); + t.add_cleanup(() => registration.unregister()); + + const readyReg = await frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals(readyReg.installing, null, 'installing should be null'); + assert_equals(readyReg.waiting, null, 'waiting should be null.') + assert_equals(readyReg.active.scriptURL, expected_url, 'active after ready should not be null'); + assert_in_array( + readyReg.active.state, + ['activating', 'activated'], + '.ready should be resolved when the registration has an active worker' + ); + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller, + null, + 'uncontrolled document should not have a controller' + ); +}, 'ready on a potential controlled document'); + +promise_test(async t => { + const url = 'resources/empty-worker.js'; + const scope = 'resources/blank.html?ready-installing'; + + await service_worker_unregister(t, scope); + + const frame = await with_iframe(scope); + const promise = frame.contentWindow.navigator.serviceWorker.ready; + navigator.serviceWorker.register(url, { scope }); + const registration = await promise; + + t.add_cleanup(async () => { + await registration.unregister(); + frame.remove(); + }); + + assert_equals(registration.installing, null, 'installing should be null'); + assert_equals(registration.waiting, null, 'waiting should be null'); + assert_not_equals(registration.active, null, 'active after ready should not be null'); + assert_in_array( + registration.active.state, + ['activating', 'activated'], + '.ready should be resolved when the registration has an active worker' + ); +}, 'ready on an iframe whose parent registers a new service worker'); + +promise_test(async t => { + const scope = 'resources/register-iframe.html'; + const frame = await with_iframe(scope); + + const registration = await frame.contentWindow.navigator.serviceWorker.ready; + + t.add_cleanup(async () => { + await registration.unregister(); + frame.remove(); + }); + + assert_equals(registration.installing, null, 'installing should be null'); + assert_equals(registration.waiting, null, 'waiting should be null'); + assert_not_equals(registration.active, null, 'active after ready should not be null'); + assert_in_array( + registration.active.state, + ['activating', 'activated'], + '.ready should be resolved with "active worker"' + ); + }, 'ready on an iframe that installs a new service worker'); + +promise_test(async t => { + const url = 'resources/empty-worker.js'; + const matchedScope = 'resources/blank.html?ready-after-match'; + const longerMatchedScope = 'resources/blank.html?ready-after-match-longer'; + + await service_worker_unregister(t, matchedScope); + await service_worker_unregister(t, longerMatchedScope); + + const frame = await with_iframe(longerMatchedScope); + const registration = await navigator.serviceWorker.register(url, { scope: matchedScope }); + + t.add_cleanup(async () => { + await registration.unregister(); + frame.remove(); + }); + + await wait_for_state(t, registration.installing, 'activated'); + + const longerRegistration = await navigator.serviceWorker.register(url, { scope: longerMatchedScope }); + + t.add_cleanup(() => longerRegistration.unregister()); + + const readyReg = await frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals( + readyReg.scope, + normalizeURL(longerMatchedScope), + 'longer matched registration should be returned' + ); + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller, + null, + 'controller should be null' + ); +}, 'ready after a longer matched registration registered'); + +promise_test(async t => { + const url = 'resources/empty-worker.js'; + const matchedScope = 'resources/blank.html?ready-after-resolve'; + const longerMatchedScope = 'resources/blank.html?ready-after-resolve-longer'; + const registration = await service_worker_unregister_and_register(t, url, matchedScope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activated'); + + const frame = await with_iframe(longerMatchedScope); + t.add_cleanup(() => frame.remove()); + + const readyReg1 = await frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals( + readyReg1.scope, + normalizeURL(matchedScope), + 'matched registration should be returned' + ); + + const longerReg = await navigator.serviceWorker.register(url, { scope: longerMatchedScope }); + t.add_cleanup(() => longerReg.unregister()); + + const readyReg2 = await frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals( + readyReg2.scope, + normalizeURL(matchedScope), + 'ready should only be resolved once' + ); +}, 'access ready after it has been resolved'); + +promise_test(async t => { + const url1 = 'resources/empty-worker.js'; + const url2 = url1 + '?2'; + const matchedScope = 'resources/blank.html?ready-after-unregister'; + const reg1 = await service_worker_unregister_and_register(t, url1, matchedScope); + t.add_cleanup(() => reg1.unregister()); + + await wait_for_state(t, reg1.installing, 'activating'); + + const frame = await with_iframe(matchedScope); + t.add_cleanup(() => frame.remove()); + + await reg1.unregister(); + + // Ready promise should be pending, waiting for a new registration to arrive + const readyPromise = frame.contentWindow.navigator.serviceWorker.ready; + + const reg2 = await navigator.serviceWorker.register(url2, { scope: matchedScope }); + t.add_cleanup(() => reg2.unregister()); + + const readyReg = await readyPromise; + + // Wait for registration update, since it comes from another global, the states are racy. + await wait_for_state(t, reg2.installing || reg2.waiting || reg2.active, 'activated'); + + assert_equals(readyReg.active.scriptURL, reg2.active.scriptURL, 'Resolves with the second registration'); + assert_not_equals(reg1, reg2, 'Registrations should be different'); +}, 'resolve ready after unregistering'); diff --git a/testing/web-platform/tests/service-workers/service-worker/redirected-response.https.html b/testing/web-platform/tests/service-workers/service-worker/redirected-response.https.html new file mode 100644 index 0000000000..71b35d0e12 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/redirected-response.https.html @@ -0,0 +1,471 @@ +<!DOCTYPE html> +<title>Service Worker: Redirected response</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +// Tests redirect behavior. It calls fetch_method(url, fetch_option) and tests +// the resulting response against the expected values. It also adds the +// response to |cache| and checks the cached response matches the expected +// values. +// +// |options|: a dictionary of parameters for the test +// |options.url|: the URL to fetch +// |options.fetch_option|: the options passed to |fetch_method| +// |options.fetch_method|: the method used to fetch. Useful for testing an +// iframe's fetch() vs. this page's fetch(). +// |options.expected_type|: The value of response.type +// |options.expected_redirected|: The value of response.redirected +// |options.expected_intercepted_urls|: The list of intercepted request URLs. +function redirected_test(options) { + return options.fetch_method.call(null, options.url, options.fetch_option).then(response => { + let cloned_response = response.clone(); + assert_equals( + response.type, options.expected_type, + 'The response type of response must match. URL: ' + options.url); + assert_equals( + cloned_response.type, options.expected_type, + 'The response type of cloned response must match. URL: ' + options.url); + assert_equals( + response.redirected, options.expected_redirected, + 'The redirected flag of response must match. URL: ' + options.url); + assert_equals( + cloned_response.redirected, options.expected_redirected, + 'The redirected flag of cloned response must match. URL: ' + options.url); + if (options.expected_response_url) { + assert_equals( + cloned_response.url, options.expected_response_url, + 'The URL does not meet expectation. URL: ' + options.url); + } + return cache.put(options.url, response); + }) + .then(_ => cache.match(options.url)) + .then(response => { + assert_equals( + response.type, options.expected_type, + 'The response type of response in CacheStorage must match. ' + + 'URL: ' + options.url); + assert_equals( + response.redirected, options.expected_redirected, + 'The redirected flag of response in CacheStorage must match. ' + + 'URL: ' + options.url); + return check_intercepted_urls(options.expected_intercepted_urls); + }); +} + +async function take_intercepted_urls() { + const message = new Promise((resolve) => { + let channel = new MessageChannel(); + channel.port1.onmessage = msg => { resolve(msg.data.requestInfos); }; + worker.postMessage({command: 'getRequestInfos', port: channel.port2}, + [channel.port2]); + }); + const request_infos = await message; + return request_infos.map(info => { return info.url; }); +} + +function check_intercepted_urls(expected_urls) { + return take_intercepted_urls().then((urls) => { + assert_object_equals(urls, expected_urls, 'Intercepted URLs matching.'); + }); +} + +function setup_and_clean() { + // To prevent interference from previous tests, take the intercepted URLs from + // the service worker. + return setup.then(() => take_intercepted_urls()); +} + + +let host_info = get_host_info(); +const REDIRECT_URL = host_info['HTTPS_ORIGIN'] + base_path() + + 'resources/redirect.py?Redirect='; +const TARGET_URL = host_info['HTTPS_ORIGIN'] + base_path() + + 'resources/simple.txt?'; +const REDIRECT_TO_TARGET_URL = REDIRECT_URL + encodeURIComponent(TARGET_URL); +let frame; +let cache; +let setup; +let worker; + +promise_test(t => { + const SCOPE = 'resources/blank.html?redirected-response'; + const SCRIPT = 'resources/redirect-worker.js'; + const CACHE_NAME = 'service-workers/service-worker/redirected-response'; + setup = service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(registration => { + promise_test( + () => registration.unregister(), + 'restore global state (service worker registration)'); + worker = registration.installing; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(_ => self.caches.open(CACHE_NAME)) + .then(c => { + cache = c; + promise_test( + () => self.caches.delete(CACHE_NAME), + 'restore global state (caches)'); + return with_iframe(SCOPE); + }) + .then(f => { + frame = f; + add_completion_callback(() => f.remove()); + return check_intercepted_urls( + [host_info['HTTPS_ORIGIN'] + base_path() + SCOPE]); + }); + return setup; + }, 'initialize global state (service worker registration and caches)'); + +// =============================================================== +// Tests for requests that are out-of-scope of the service worker. +// =============================================================== +promise_test(t => setup_and_clean() + .then(() => redirected_test({url: TARGET_URL, + fetch_option: {}, + fetch_method: self.fetch, + expected_type: 'basic', + expected_redirected: false, + expected_intercepted_urls: []})), + 'mode: "follow", non-intercepted request, no server redirect'); + +promise_test(t => setup_and_clean() + .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL, + fetch_option: {}, + fetch_method: self.fetch, + expected_type: 'basic', + expected_redirected: true, + expected_intercepted_urls: []})), + 'mode: "follow", non-intercepted request'); + +promise_test(t => setup_and_clean() + .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL + '&manual', + fetch_option: {redirect: 'manual'}, + fetch_method: self.fetch, + expected_type: 'opaqueredirect', + expected_redirected: false, + expected_intercepted_urls: []})), + 'mode: "manual", non-intercepted request'); + +promise_test(t => setup_and_clean() + .then(() => promise_rejects_js( + t, TypeError, + self.fetch(REDIRECT_TO_TARGET_URL + '&error', + {redirect:'error'}), + 'The redirect response from the server should be treated as' + + ' an error when the redirect flag of request was \'error\'.')) + .then(() => check_intercepted_urls([])), + 'mode: "error", non-intercepted request'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = TARGET_URL + '&sw=fetch'; + return redirected_test({url: url, + fetch_option: {}, + fetch_method: frame.contentWindow.fetch, + expected_type: 'basic', + expected_redirected: false, + expected_intercepted_urls: [url]}) + }), + 'mode: "follow", no mode change, no server redirect'); + +// ======================================================= +// Tests for requests that are in-scope of the service worker. The service +// worker returns a redirected response. +// ======================================================= +promise_test(t => setup_and_clean() + .then(() => { + const url = REDIRECT_TO_TARGET_URL + + '&original-redirect-mode=follow&sw=fetch'; + return redirected_test({url: url, + fetch_option: {redirect: 'follow'}, + fetch_method: frame.contentWindow.fetch, + expected_type: 'basic', + expected_redirected: true, + expected_intercepted_urls: [url]}) + }), + 'mode: "follow", no mode change'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = REDIRECT_TO_TARGET_URL + + '&original-redirect-mode=error&sw=follow'; + return promise_rejects_js( + t, frame.contentWindow.TypeError, + frame.contentWindow.fetch(url, {redirect: 'error'}), + 'The redirected response from the service worker should be ' + + 'treated as an error when the redirect flag of request was ' + + '\'error\'.') + .then(() => check_intercepted_urls([url])); + }), + 'mode: "error", mode change: "follow"'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = REDIRECT_TO_TARGET_URL + + '&original-redirect-mode=manual&sw=follow'; + return promise_rejects_js( + t, frame.contentWindow.TypeError, + frame.contentWindow.fetch(url, {redirect: 'manual'}), + 'The redirected response from the service worker should be ' + + 'treated as an error when the redirect flag of request was ' + + '\'manual\'.') + .then(() => check_intercepted_urls([url])); + }), + 'mode: "manual", mode change: "follow"'); + +// ======================================================= +// Tests for requests that are in-scope of the service worker. The service +// worker returns an opaqueredirect response. +// ======================================================= +promise_test(t => setup_and_clean() + .then(() => { + const url = REDIRECT_TO_TARGET_URL + + '&original-redirect-mode=follow&sw=manual'; + return promise_rejects_js( + t, frame.contentWindow.TypeError, + frame.contentWindow.fetch(url, {redirect: 'follow'}), + 'The opaqueredirect response from the service worker should ' + + 'be treated as an error when the redirect flag of request was' + + ' \'follow\'.') + .then(() => check_intercepted_urls([url])); + }), + 'mode: "follow", mode change: "manual"'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = REDIRECT_TO_TARGET_URL + + '&original-redirect-mode=error&sw=manual'; + return promise_rejects_js( + t, frame.contentWindow.TypeError, + frame.contentWindow.fetch(url, {redirect: 'error'}), + 'The opaqueredirect response from the service worker should ' + + 'be treated as an error when the redirect flag of request was' + + ' \'error\'.') + .then(() => check_intercepted_urls([url])); + }), + 'mode: "error", mode change: "manual"'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = REDIRECT_TO_TARGET_URL + + '&original-redirect-mode=manual&sw=manual'; + return redirected_test({url: url, + fetch_option: {redirect: 'manual'}, + fetch_method: frame.contentWindow.fetch, + expected_type: 'opaqueredirect', + expected_redirected: false, + expected_intercepted_urls: [url]}); + }), + 'mode: "manual", no mode change'); + +// ======================================================= +// Tests for requests that are in-scope of the service worker. The service +// worker returns a generated redirect response. +// ======================================================= +promise_test(t => setup_and_clean() + .then(() => { + const url = host_info['HTTPS_ORIGIN'] + base_path() + + 'sample?url=' + encodeURIComponent(TARGET_URL) + + '&original-redirect-mode=follow&sw=gen'; + return redirected_test({url: url, + fetch_option: {redirect: 'follow'}, + fetch_method: frame.contentWindow.fetch, + expected_type: 'basic', + expected_redirected: true, + expected_intercepted_urls: [url, TARGET_URL]}) + }), + 'mode: "follow", generated redirect response'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = host_info['HTTPS_ORIGIN'] + base_path() + + 'sample?url=' + encodeURIComponent(TARGET_URL) + + '&original-redirect-mode=error&sw=gen'; + return promise_rejects_js( + t, frame.contentWindow.TypeError, + frame.contentWindow.fetch(url, {redirect: 'error'}), + 'The generated redirect response from the service worker should ' + + 'be treated as an error when the redirect flag of request was' + + ' \'error\'.') + .then(() => check_intercepted_urls([url])); + }), + 'mode: "error", generated redirect response'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = host_info['HTTPS_ORIGIN'] + base_path() + + 'sample?url=' + encodeURIComponent(TARGET_URL) + + '&original-redirect-mode=manual&sw=gen'; + return redirected_test({url: url, + fetch_option: {redirect: 'manual'}, + fetch_method: frame.contentWindow.fetch, + expected_type: 'opaqueredirect', + expected_redirected: false, + expected_intercepted_urls: [url]}) + }), + 'mode: "manual", generated redirect response'); + +// ======================================================= +// Tests for requests that are in-scope of the service worker. The service +// worker returns a generated redirect response manually with the Response +// constructor. +// ======================================================= +promise_test(t => setup_and_clean() + .then(() => { + const url = host_info['HTTPS_ORIGIN'] + base_path() + + 'sample?url=' + encodeURIComponent(TARGET_URL) + + '&original-redirect-mode=follow&sw=gen-manual'; + return redirected_test({url: url, + fetch_option: {redirect: 'follow'}, + fetch_method: frame.contentWindow.fetch, + expected_type: 'basic', + expected_redirected: true, + expected_intercepted_urls: [url, TARGET_URL]}) + }), + 'mode: "follow", manually-generated redirect response'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = host_info['HTTPS_ORIGIN'] + base_path() + + 'sample?url=' + encodeURIComponent(TARGET_URL) + + '&original-redirect-mode=error&sw=gen-manual'; + return promise_rejects_js( + t, frame.contentWindow.TypeError, + frame.contentWindow.fetch(url, {redirect: 'error'}), + 'The generated redirect response from the service worker should ' + + 'be treated as an error when the redirect flag of request was' + + ' \'error\'.') + .then(() => check_intercepted_urls([url])); + }), + 'mode: "error", manually-generated redirect response'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = host_info['HTTPS_ORIGIN'] + base_path() + + 'sample?url=' + encodeURIComponent(TARGET_URL) + + '&original-redirect-mode=manual&sw=gen-manual'; + return redirected_test({url: url, + fetch_option: {redirect: 'manual'}, + fetch_method: frame.contentWindow.fetch, + expected_type: 'opaqueredirect', + expected_redirected: false, + expected_intercepted_urls: [url]}) + }), + 'mode: "manual", manually-generated redirect response'); + +// ======================================================= +// Tests for requests that are in-scope of the service worker. The service +// worker returns a generated redirect response with a relative location header. +// Generated responses do not have URLs, so this should fail to resolve. +// ======================================================= +promise_test(t => setup_and_clean() + .then(() => { + const url = host_info['HTTPS_ORIGIN'] + base_path() + + 'sample?url=blank.html' + + '&original-redirect-mode=follow&sw=gen-manual'; + return promise_rejects_js( + t, frame.contentWindow.TypeError, + frame.contentWindow.fetch(url, {redirect: 'follow'}), + 'Following the generated redirect response from the service worker '+ + 'should result fail.') + .then(() => check_intercepted_urls([url])); + }), + 'mode: "follow", generated relative redirect response'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = host_info['HTTPS_ORIGIN'] + base_path() + + 'sample?url=blank.html' + + '&original-redirect-mode=error&sw=gen-manual'; + return promise_rejects_js( + t, frame.contentWindow.TypeError, + frame.contentWindow.fetch(url, {redirect: 'error'}), + 'The generated redirect response from the service worker should ' + + 'be treated as an error when the redirect flag of request was' + + ' \'error\'.') + .then(() => check_intercepted_urls([url])); + }), + 'mode: "error", generated relative redirect response'); + +promise_test(t => setup_and_clean() + .then(() => { + const url = host_info['HTTPS_ORIGIN'] + base_path() + + 'sample?url=blank.html' + + '&original-redirect-mode=manual&sw=gen-manual'; + return redirected_test({url: url, + fetch_option: {redirect: 'manual'}, + fetch_method: frame.contentWindow.fetch, + expected_type: 'opaqueredirect', + expected_redirected: false, + expected_intercepted_urls: [url]}) + }), + 'mode: "manual", generated relative redirect response'); + +// ======================================================= +// Tests for requests that are in-scope of the service worker. The service +// worker returns a generated redirect response. And the fetch follows the +// redirection multiple times. +// ======================================================= +promise_test(t => setup_and_clean() + .then(() => { + // The Fetch spec says: "If request’s redirect count is twenty, return a + // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch + // So fetch can follow the redirect response 20 times. + let urls = [TARGET_URL]; + for (let i = 0; i < 20; ++i) { + urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' + + encodeURIComponent(urls[0])); + + } + return redirected_test({url: urls[0], + fetch_option: {redirect: 'follow'}, + fetch_method: frame.contentWindow.fetch, + expected_type: 'basic', + expected_redirected: true, + expected_intercepted_urls: urls}) + }), + 'Fetch should follow the redirect response 20 times'); + +promise_test(t => setup_and_clean() + .then(() => { + let urls = [TARGET_URL]; + // The Fetch spec says: "If request’s redirect count is twenty, return a + // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch + // So fetch can't follow the redirect response 21 times. + for (let i = 0; i < 21; ++i) { + urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' + + encodeURIComponent(urls[0])); + + } + return promise_rejects_js( + t, frame.contentWindow.TypeError, + frame.contentWindow.fetch(urls[0], {redirect: 'follow'}), + 'Fetch should not follow the redirect response 21 times.') + .then(() => { + urls.pop(); + return check_intercepted_urls(urls) + }); + }), + 'Fetch should not follow the redirect response 21 times.'); + +// ======================================================= +// A test for verifying the url of a service-worker-redirected request is +// propagated to the outer response. +// ======================================================= +promise_test(t => setup_and_clean() + .then(() => { + const url = host_info['HTTPS_ORIGIN'] + base_path() + 'sample?url=' + + encodeURIComponent(TARGET_URL) +'&sw=fetch-url'; + return redirected_test({url: url, + fetch_option: {}, + fetch_method: frame.contentWindow.fetch, + expected_type: 'basic', + expected_redirected: false, + expected_intercepted_urls: [url], + expected_response_url: TARGET_URL}); + }), + 'The URL for the service worker redirected request should be propagated to ' + + 'response.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/referer.https.html b/testing/web-platform/tests/service-workers/service-worker/referer.https.html new file mode 100644 index 0000000000..0957e4c533 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/referer.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<title>Service Worker: check referer of fetch()</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> +promise_test(function(t) { + var SCOPE = 'resources/referer-iframe.html'; + var SCRIPT = 'resources/fetch-rewrite-worker.js'; + var host_info = get_host_info(); + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, SCOPE); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(function(frame) { + var channel = new MessageChannel(); + t.add_cleanup(function() { + frame.remove(); + }); + + var onMsg = new Promise(function(resolve) { + channel.port1.onmessage = resolve; + }); + + frame.contentWindow.postMessage({}, + host_info['HTTPS_ORIGIN'], + [channel.port2]); + return onMsg; + }) + .then(function(e) { + assert_equals(e.data.results, 'finish'); + }); + }, 'Verify the referer'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/referrer-policy-header.https.html b/testing/web-platform/tests/service-workers/service-worker/referrer-policy-header.https.html new file mode 100644 index 0000000000..784343e6d8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/referrer-policy-header.https.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<title>Service Worker: check referer of fetch() with Referrer Policy</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +const SCOPE = 'resources/referrer-policy-iframe.html'; +const SCRIPT = 'resources/fetch-rewrite-worker-referrer-policy.js'; + +promise_test(async t => { + const registration = + await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + await wait_for_state(t, registration.installing, 'activated'); + t.add_cleanup(() => registration.unregister(), + 'Remove registration as a cleanup'); + + const full_scope_url = new URL(SCOPE, location.href); + const redirect_to = `${full_scope_url.href}?ignore=true`; + const frame = await with_iframe( + `${SCOPE}?pipe=status(302)|header(Location,${redirect_to})|` + + 'header(Referrer-Policy,origin)'); + assert_equals(frame.contentDocument.referrer, + full_scope_url.origin + '/'); + t.add_cleanup(() => frame.remove()); +}, 'Referrer for a main resource redirected with referrer-policy (origin) ' + + 'should only have origin.'); + +promise_test(async t => { + const registration = + await service_worker_unregister_and_register(t, SCRIPT, SCOPE, `{type: 'module'}`); + await wait_for_state(t, registration.installing, 'activated'); + t.add_cleanup(() => registration.unregister(), + 'Remove registration as a cleanup'); + + const full_scope_url = new URL(SCOPE, location.href); + const redirect_to = `${full_scope_url.href}?ignore=true`; + const frame = await with_iframe( + `${SCOPE}?pipe=status(302)|header(Location,${redirect_to})|` + + 'header(Referrer-Policy,origin)'); + assert_equals(frame.contentDocument.referrer, + full_scope_url.origin + '/'); + t.add_cleanup(() => frame.remove()); +}, 'Referrer for a main resource redirected with a module script with referrer-policy (origin) ' + + 'should only have origin.'); + +promise_test(async t => { + const registration = + await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + await wait_for_state(t, registration.installing, 'activated'); + t.add_cleanup(() => registration.unregister(), + 'Remove registration as a cleanup'); + + const host_info = get_host_info(); + const frame = await with_iframe(SCOPE); + const channel = new MessageChannel(); + t.add_cleanup(() => frame.remove()); + const e = await new Promise(resolve => { + channel.port1.onmessage = resolve; + frame.contentWindow.postMessage( + {}, host_info['HTTPS_ORIGIN'], [channel.port2]); + }); + assert_equals(e.data.results, 'finish'); +}, 'Referrer for fetch requests initiated from a service worker with ' + + 'referrer-policy (origin) should only have origin.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html b/testing/web-platform/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html new file mode 100644 index 0000000000..65c60a11db --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<title>Service Worker: check referrer of top-level script fetch</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> + +async function get_toplevel_script_headers(worker) { + worker.postMessage("getHeaders"); + return new Promise((resolve) => { + navigator.serviceWorker.onmessage = (event) => { + resolve(event.data); + }; + }); +} + +promise_test(async (t) => { + const script = "resources/test-request-headers-worker.py"; + const scope = "resources/blank.html"; + const host_info = get_host_info(); + + const registration = await service_worker_unregister_and_register( + t, script, scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + await wait_for_state(t, registration.installing, "activated"); + + const expected_referrer = host_info["HTTPS_ORIGIN"] + location.pathname; + + // Check referrer for register(). + const register_headers = await get_toplevel_script_headers(registration.active); + assert_equals(register_headers["referer"], expected_referrer, "referrer of register()"); + + // Check referrer for update(). + await registration.update(); + await wait_for_state(t, registration.installing, "installed"); + const update_headers = await get_toplevel_script_headers(registration.waiting); + assert_equals(update_headers["referer"], expected_referrer, "referrer of update()"); +}, "Referrer of the top-level script fetch should be the document URL"); + +promise_test(async (t) => { + const script = "resources/test-request-headers-worker.py"; + const scope = "resources/blank.html"; + const host_info = get_host_info(); + + const registration = await service_worker_unregister_and_register( + t, script, scope, {type: 'module'}); + t.add_cleanup(() => service_worker_unregister(t, scope)); + await wait_for_state(t, registration.installing, "activated"); + + const expected_referrer = host_info["HTTPS_ORIGIN"] + location.pathname; + + // Check referrer for register(). + const register_headers = await get_toplevel_script_headers(registration.active); + assert_equals(register_headers["referer"], expected_referrer, "referrer of register()"); + + // Check referrer for update(). + await registration.update(); + await wait_for_state(t, registration.installing, "installed"); + const update_headers = await get_toplevel_script_headers(registration.waiting); + assert_equals(update_headers["referer"], expected_referrer, "referrer of update()"); +}, "Referrer of the module script fetch should be the document URL"); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/register-closed-window.https.html b/testing/web-platform/tests/service-workers/service-worker/register-closed-window.https.html new file mode 100644 index 0000000000..9c1b639bb7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/register-closed-window.https.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<title>Service Worker: Register() on Closed Window</title> +<meta name=timeout content=long> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +var host_info = get_host_info(); +var frameURL = host_info['HTTPS_ORIGIN'] + base_path() + + 'resources/register-closed-window-iframe.html'; + +async_test(function(t) { + var frame; + with_iframe(frameURL).then(function(f) { + frame = f; + return new Promise(function(resolve) { + window.addEventListener('message', function messageHandler(evt) { + window.removeEventListener('message', messageHandler); + resolve(evt.data); + }); + frame.contentWindow.postMessage('START', '*'); + }); + }).then(function(result) { + assert_equals(result, 'OK', 'frame should complete without crashing'); + frame.remove(); + t.done(); + }).catch(unreached_rejection(t)); +}, 'Call register() on ServiceWorkerContainer owned by closed window.'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/register-default-scope.https.html b/testing/web-platform/tests/service-workers/service-worker/register-default-scope.https.html new file mode 100644 index 0000000000..1d86548eb5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/register-default-scope.https.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<title>register() and scope</title> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var script_url = new URL(script, location.href); + var expected_scope = new URL('./', script_url).href; + return service_worker_unregister(t, expected_scope) + .then(function() { + return navigator.serviceWorker.register('resources/empty-worker.js'); + }).then(function(registration) { + assert_equals(registration.scope, expected_scope, + 'The default scope should be URL("./", script_url)'); + return registration.unregister(); + }).then(function() { + t.done(); + }); + }, 'default scope'); + +promise_test(function(t) { + // This script must be different than the 'default scope' test, or else + // the scopes will collide. + var script = 'resources/empty.js'; + var script_url = new URL(script, location.href); + var expected_scope = new URL('./', script_url).href; + return service_worker_unregister(t, expected_scope) + .then(function() { + return navigator.serviceWorker.register('resources/empty.js', + { scope: undefined }); + }).then(function(registration) { + assert_equals(registration.scope, expected_scope, + 'The default scope should be URL("./", script_url)'); + return registration.unregister(); + }).then(function() { + t.done(); + }); + }, 'undefined scope'); + +promise_test(function(t) { + var script = 'resources/simple-fetch-worker.js'; + var script_url = new URL(script, location.href); + var expected_scope = new URL('./', script_url).href; + return service_worker_unregister(t, expected_scope) + .then(function() { + return navigator.serviceWorker.register('resources/empty.js', + { scope: null }); + }) + .then( + function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, registration.scope); + }); + + assert_unreached('register should fail'); + }, + function(error) { + assert_equals(error.name, 'SecurityError', + 'passing a null scope should be interpreted as ' + + 'scope="null" which violates the path restriction'); + t.done(); + }); + }, 'null scope'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html b/testing/web-platform/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html new file mode 100644 index 0000000000..6eb00f3071 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html @@ -0,0 +1,233 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var script1 = normalizeURL('resources/empty-worker.js'); +var script2 = normalizeURL('resources/empty-worker.js?new'); + +async_test(function(t) { + var scope = 'resources/scope/register-new-script-concurrently'; + var register_promise1; + var register_promise2; + + service_worker_unregister(t, scope) + .then(function() { + register_promise1 = navigator.serviceWorker.register(script1, + {scope: scope}); + register_promise2 = navigator.serviceWorker.register(script2, + {scope: scope}); + return register_promise1; + }) + .then(function(registration) { + assert_equals(registration.installing.scriptURL, script1, + 'on first register, first script should be installing'); + assert_equals(registration.waiting, null, + 'on first register, waiting should be null'); + assert_equals(registration.active, null, + 'on first register, active should be null'); + return register_promise2; + }) + .then(function(registration) { + assert_equals( + registration.installing.scriptURL, script2, + 'on second register, second script should be installing'); + // Spec allows racing: the first register may have finished + // or the second one could have terminated the installing worker. + assert_true(registration.waiting == null || + registration.waiting.scriptURL == script1, + 'on second register, .waiting should be null or the ' + + 'first script'); + assert_true(registration.active == null || + (registration.waiting == null && + registration.active.scriptURL == script1), + 'on second register, .active should be null or the ' + + 'first script'); + return registration.unregister(); + }) + .then(function() { + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Register different scripts concurrently'); + +async_test(function(t) { + var scope = 'resources/scope/register-then-register-new-script'; + var registration; + + service_worker_unregister_and_register(t, script1, scope) + .then(function(r) { + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + assert_equals(registration.installing, null, + 'on activated, installing should be null'); + assert_equals(registration.waiting, null, + 'on activated, waiting should be null'); + assert_equals(registration.active.scriptURL, script1, + 'on activated, the first script should be active'); + return navigator.serviceWorker.register(script2, {scope:scope}); + }) + .then(function(r) { + registration = r; + assert_equals(registration.installing.scriptURL, script2, + 'on second register, the second script should be ' + + 'installing'); + assert_equals(registration.waiting, null, + 'on second register, waiting should be null'); + assert_equals(registration.active.scriptURL, script1, + 'on second register, the first script should be ' + + 'active'); + return wait_for_state(t, registration.installing, 'installed'); + }) + .then(function() { + assert_equals(registration.installing, null, + 'on installed, installing should be null'); + assert_equals(registration.waiting.scriptURL, script2, + 'on installed, the second script should be waiting'); + assert_equals(registration.active.scriptURL, script1, + 'on installed, the first script should be active'); + return registration.unregister(); + }) + .then(function() { + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Register then register new script URL'); + +async_test(function(t) { + var scope = 'resources/scope/register-then-register-new-script-404'; + var registration; + + service_worker_unregister_and_register(t, script1, scope) + .then(function(r) { + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + assert_equals(registration.installing, null, + 'on activated, installing should be null'); + assert_equals(registration.waiting, null, + 'on activated, waiting should be null'); + assert_equals(registration.active.scriptURL, script1, + 'on activated, the first script should be active'); + return navigator.serviceWorker.register('this-will-404.js', + {scope:scope}); + }) + .then( + function() { assert_unreached('register should reject'); }, + function(error) { + assert_equals(registration.installing, null, + 'on rejected, installing should be null'); + assert_equals(registration.waiting, null, + 'on rejected, waiting should be null'); + assert_equals(registration.active.scriptURL, script1, + 'on rejected, the first script should be active'); + return registration.unregister(); + }) + .then(function() { + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Register then register new script URL that 404s'); + +async_test(function(t) { + var scope = 'resources/scope/register-then-register-new-script-reject-install'; + var reject_script = normalizeURL('resources/reject-install-worker.js'); + var registration; + + service_worker_unregister_and_register(t, script1, scope) + .then(function(r) { + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + assert_equals(registration.installing, null, + 'on activated, installing should be null'); + assert_equals(registration.waiting, null, + 'on activated, waiting should be null'); + assert_equals(registration.active.scriptURL, script1, + 'on activated, the first script should be active'); + return navigator.serviceWorker.register(reject_script, {scope:scope}); + }) + .then(function(r) { + registration = r; + assert_equals(registration.installing.scriptURL, reject_script, + 'on update, the second script should be installing'); + assert_equals(registration.waiting, null, + 'on update, waiting should be null'); + assert_equals(registration.active.scriptURL, script1, + 'on update, the first script should be active'); + return wait_for_state(t, registration.installing, 'redundant'); + }) + .then(function() { + assert_equals(registration.installing, null, + 'on redundant, installing should be null'); + assert_equals(registration.waiting, null, + 'on redundant, waiting should be null'); + assert_equals(registration.active.scriptURL, script1, + 'on redundant, the first script should be active'); + return registration.unregister(); + }) + .then(function() { + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Register then register new script that does not install'); + +async_test(function(t) { + var scope = 'resources/scope/register-new-script-controller'; + var iframe; + var registration; + + service_worker_unregister_and_register(t, script1, scope) + .then(function(r) { + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(frame) { + iframe = frame; + return navigator.serviceWorker.register(script2, { scope: scope }) + }) + .then(function(r) { + registration = r; + return wait_for_state(t, registration.installing, 'installed'); + }) + .then(function() { + var sw_container = iframe.contentWindow.navigator.serviceWorker; + assert_equals(sw_container.controller.scriptURL, script1, + 'the old version should control the old doc'); + return with_iframe(scope); + }) + .then(function(frame) { + var sw_container = frame.contentWindow.navigator.serviceWorker; + assert_equals(sw_container.controller.scriptURL, script1, + 'the old version should control a new doc'); + var onactivated_promise = wait_for_state(t, + registration.waiting, + 'activated'); + frame.remove(); + iframe.remove(); + return onactivated_promise; + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(frame) { + var sw_container = frame.contentWindow.navigator.serviceWorker; + assert_equals(sw_container.controller.scriptURL, script2, + 'the new version should control a new doc'); + frame.remove(); + return registration.unregister(); + }) + .then(function() { + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Register same-scope new script url effect on controller'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html b/testing/web-platform/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html new file mode 100644 index 0000000000..0920b5cb22 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<title>Service Worker: Register wait-forever-in-install-worker</title> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +promise_test(function(t) { + var bad_script = 'resources/wait-forever-in-install-worker.js'; + var good_script = 'resources/empty-worker.js'; + var scope = 'resources/wait-forever-in-install-worker'; + var other_scope = 'resources/wait-forever-in-install-worker-other'; + var registration; + var registerPromise; + + return navigator.serviceWorker.register(bad_script, {scope: scope}) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + assert_equals(registration.installing.scriptURL, + normalizeURL(bad_script)); + + // This register job should not start until the first + // register for the same scope completes. + registerPromise = + navigator.serviceWorker.register(good_script, {scope: scope}); + + // In order to test that the above register does not complete + // we will perform a register() on a different scope. The + // assumption here is that the previous register call would + // have completed in the same timeframe if it was able to do + // so. + return navigator.serviceWorker.register(good_script, + {scope: other_scope}); + }) + .then(function(swr) { + return swr.unregister(); + }) + .then(function() { + assert_equals(registration.installing.scriptURL, + normalizeURL(bad_script)); + registration.installing.postMessage('STOP_WAITING'); + return registerPromise; + }) + .then(function(swr) { + assert_equals(registration.installing.scriptURL, + normalizeURL(good_script)); + return wait_for_state(t, registration.installing, 'activated'); + }) + }, 'register worker that calls waitUntil with a promise that never ' + + 'resolves in oninstall'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-basic.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-basic.https.html new file mode 100644 index 0000000000..759b4244a2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-basic.https.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<title>Service Worker: Registration (basic)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +const script = 'resources/registration-worker.js'; + +promise_test(async (t) => { + const scope = 'resources/registration/normal'; + const registration = await navigator.serviceWorker.register(script, {scope}); + t.add_cleanup(() => registration.unregister()); + assert_true( + registration instanceof ServiceWorkerRegistration, + 'Successfully registered.'); +}, 'Registering normal scope'); + +promise_test(async (t) => { + const scope = 'resources/registration/scope-with-fragment#ref'; + const registration = await navigator.serviceWorker.register(script, {scope}); + t.add_cleanup(() => registration.unregister()); + assert_true( + registration instanceof ServiceWorkerRegistration, + 'Successfully registered.'); + assert_equals( + registration.scope, + normalizeURL('resources/registration/scope-with-fragment'), + 'A fragment should be removed from scope'); +}, 'Registering scope with fragment'); + +promise_test(async (t) => { + const scope = 'resources/'; + const registration = await navigator.serviceWorker.register(script, {scope}) + t.add_cleanup(() => registration.unregister()); + assert_true( + registration instanceof ServiceWorkerRegistration, + 'Successfully registered.'); +}, 'Registering same scope as the script directory'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-end-to-end.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-end-to-end.https.html new file mode 100644 index 0000000000..1af4582d38 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-end-to-end.https.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<title>Service Worker: registration end-to-end</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var t = async_test('Registration: end-to-end'); +t.step(function() { + + var scope = 'resources/in-scope/'; + var serviceWorkerStates = []; + var lastServiceWorkerState = ''; + var receivedMessageFromPort = ''; + + assert_true(navigator.serviceWorker instanceof ServiceWorkerContainer); + assert_equals(typeof navigator.serviceWorker.register, 'function'); + assert_equals(typeof navigator.serviceWorker.getRegistration, 'function'); + + service_worker_unregister_and_register( + t, 'resources/end-to-end-worker.js', scope) + .then(onRegister) + .catch(unreached_rejection(t)); + + function sendMessagePort(worker, from) { + var messageChannel = new MessageChannel(); + worker.postMessage({from:from, port:messageChannel.port2}, [messageChannel.port2]); + return messageChannel.port1; + } + + function onRegister(registration) { + var sw = registration.installing; + serviceWorkerStates.push(sw.state); + lastServiceWorkerState = sw.state; + + var sawMessage = new Promise(t.step_func(function(resolve) { + sendMessagePort(sw, 'registering doc').onmessage = t.step_func(function (e) { + receivedMessageFromPort = e.data; + resolve(); + }); + })); + + var sawActive = new Promise(t.step_func(function(resolve) { + sw.onstatechange = t.step_func(function() { + serviceWorkerStates.push(sw.state); + + switch (sw.state) { + case 'installed': + assert_equals(lastServiceWorkerState, 'installing'); + break; + case 'activating': + assert_equals(lastServiceWorkerState, 'installed'); + break; + case 'activated': + assert_equals(lastServiceWorkerState, 'activating'); + break; + default: + // We won't see 'redundant' because onstatechange is + // overwritten before calling unregister. + assert_unreached('Unexpected state: ' + sw.state); + } + + lastServiceWorkerState = sw.state; + if (sw.state === 'activated') + resolve(); + }); + })); + + Promise.all([sawMessage, sawActive]).then(t.step_func(function() { + assert_array_equals(serviceWorkerStates, + ['installing', 'installed', 'activating', 'activated'], + 'Service worker should pass through all states'); + + assert_equals(receivedMessageFromPort, 'Ack for: registering doc'); + + var sawRedundant = new Promise(t.step_func(function(resolve) { + sw.onstatechange = t.step_func(function() { + assert_equals(sw.state, 'redundant'); + resolve(); + }); + })); + registration.unregister(); + sawRedundant.then(t.step_func(function() { + t.done(); + })); + })); + } +}); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-events.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-events.https.html new file mode 100644 index 0000000000..5bcfd66846 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-events.https.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<title>Service Worker: registration events</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(function(t) { + var scope = 'resources/in-scope/'; + return service_worker_unregister_and_register( + t, 'resources/events-worker.js', scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return onRegister(registration.installing); + }); + + function sendMessagePort(worker, from) { + var messageChannel = new MessageChannel(); + worker.postMessage({from:from, port:messageChannel.port2}, [messageChannel.port2]); + return messageChannel.port1; + } + + function onRegister(sw) { + return new Promise(function(resolve) { + sw.onstatechange = function() { + if (sw.state === 'activated') + resolve(); + }; + }).then(function() { + return new Promise(function(resolve) { + sendMessagePort(sw, 'registering doc').onmessage = resolve; + }); + }).then(function(e) { + assert_array_equals(e.data.events, + ['install', 'activate'], + 'Worker should see install then activate events'); + }); + } +}, 'Registration: events'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-iframe.https.html new file mode 100644 index 0000000000..ae39ddfea3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-iframe.https.html @@ -0,0 +1,116 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: Registration for iframe</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +// Set script url and scope url relative to the iframe's document's url. Assert +// the implementation parses the urls against the iframe's document's url. +async_test(function(t) { + const url = 'resources/blank.html'; + const iframe_scope = 'registration-with-valid-scope'; + const scope = normalizeURL('resources/' + iframe_scope); + const iframe_script = 'empty-worker.js'; + const script = normalizeURL('resources/' + iframe_script); + var frame; + var registration; + + service_worker_unregister(t, scope) + .then(function() { return with_iframe(url); }) + .then(function(f) { + frame = f; + return frame.contentWindow.navigator.serviceWorker.register( + iframe_script, + { scope: iframe_scope }); + }) + .then(function(r) { + registration = r; + return wait_for_state(t, r.installing, 'activated'); + }) + .then(function() { + assert_equals(registration.scope, scope, + 'registration\'s scope must be parsed against the ' + + '"relevant global object"'); + assert_equals(registration.active.scriptURL, script, + 'worker\'s scriptURL must be parsed against the ' + + '"relevant global object"'); + return registration.unregister(); + }) + .then(function() { + frame.remove(); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'register method should use the "relevant global object" to parse its ' + + 'scriptURL and scope - normal case'); + +// Set script url and scope url relative to the parent frame's document's url. +// Assert the implementation throws a TypeError exception. +async_test(function(t) { + const url = 'resources/blank.html'; + const iframe_scope = 'resources/registration-with-scope-to-non-existing-url'; + const scope = normalizeURL('resources/' + iframe_scope); + const script = 'resources/empty-worker.js'; + var frame; + var registration; + + service_worker_unregister(t, scope) + .then(function() { return with_iframe(url); }) + .then(function(f) { + frame = f; + return frame.contentWindow.navigator.serviceWorker.register( + script, + { scope: iframe_scope }); + }) + .then( + function() { + assert_unreached('register() should reject'); + }, + function(e) { + assert_equals(e.name, 'TypeError', + 'register method with scriptURL and scope parsed to ' + + 'nonexistent location should reject with TypeError'); + frame.remove(); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'register method should use the "relevant global object" to parse its ' + + 'scriptURL and scope - error case'); + +// Set the scope url to a non-subdirectory of the script url. Assert the +// implementation throws a SecurityError exception. +async_test(function(t) { + const url = 'resources/blank.html'; + const scope = 'registration-with-disallowed-scope'; + const iframe_scope = '../' + scope; + const script = 'empty-worker.js'; + var frame; + var registration; + + service_worker_unregister(t, scope) + .then(function() { return with_iframe(url); }) + .then(function(f) { + frame = f; + return frame.contentWindow.navigator.serviceWorker.register( + script, + { scope: iframe_scope }); + }) + .then( + function() { + assert_unreached('register() should reject'); + }, + function(e) { + assert_equals(e.name, 'SecurityError', + 'The scope set to a non-subdirectory of the scriptURL ' + + 'should reject with SecurityError'); + frame.remove(); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'A scope url should start with the given script url'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-mime-types.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-mime-types.https.html new file mode 100644 index 0000000000..3a21aac5c7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-mime-types.https.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<title>Service Worker: Registration (MIME types)</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="resources/registration-tests-mime-types.js"></script> +<script> +registration_tests_mime_types((script, options) => navigator.serviceWorker.register(script, options)); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-schedule-job.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-schedule-job.https.html new file mode 100644 index 0000000000..25d758ee8f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-schedule-job.https.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name=timeout content=long> +<title>Service Worker: Schedule Job algorithm</title> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +// Tests for https://w3c.github.io/ServiceWorker/#schedule-job-algorithm +// Non-equivalent register jobs should not be coalesced. +const scope = 'resources/'; +const script1 = 'resources/empty.js'; +const script2 = 'resources/empty.js?change'; + +async function cleanup() { + const registration = await navigator.serviceWorker.getRegistration(scope); + if (registration) + await registration.unregister(); +} + +function absolute_url(url) { + return new URL(url, self.location).toString(); +} + +// Test that a change to `script` starts a new register job. +promise_test(async t => { + await cleanup(); + t.add_cleanup(cleanup); + + // Make a registration. + const registration = await + navigator.serviceWorker.register(script1, {scope}); + + // Schedule two more register jobs. + navigator.serviceWorker.register(script1, {scope}); + await navigator.serviceWorker.register(script2, {scope}); + + // The jobs should not have been coalesced. + const worker = get_newest_worker(registration); + assert_equals(worker.scriptURL, absolute_url(script2)); +}, 'different scriptURL'); + +// Test that a change to `updateViaCache` starts a new register job. +promise_test(async t => { + await cleanup(); + t.add_cleanup(cleanup); + + // Check defaults. + const registration = await + navigator.serviceWorker.register(script1, {scope}); + assert_equals(registration.updateViaCache, 'imports'); + + // Schedule two more register jobs. + navigator.serviceWorker.register(script1, {scope}); + await navigator.serviceWorker.register(script1, {scope, + updateViaCache: 'none'}); + + // The jobs should not have been coalesced. + assert_equals(registration.updateViaCache, 'none'); +}, 'different updateViaCache'); + +// Test that a change to `type` starts a new register job. +promise_test(async t => { + await cleanup(); + t.add_cleanup(cleanup); + + const scriptForTypeCheck = 'resources/type-check-worker.js'; + // Check defaults. + const registration = await + navigator.serviceWorker.register(scriptForTypeCheck, {scope}); + + let worker_type = await new Promise((resolve) => { + navigator.serviceWorker.onmessage = (event) => { + resolve(event.data); + }; + // The jobs should not have been coalesced. get_newest_worker() helps the + // test fail with stable output on browers that incorrectly coalesce + // register jobs, since then sometimes registration is not a new worker as + // expected. + const worker = get_newest_worker(registration); + // The argument of postMessage doesn't matter for this case. + worker.postMessage(''); + }); + + assert_equals(worker_type, 'classic'); + + // Schedule two more register jobs. + navigator.serviceWorker.register(scriptForTypeCheck, {scope}); + await navigator.serviceWorker.register(scriptForTypeCheck, {scope, type: 'module'}); + + worker_type = await new Promise((resolve) => { + navigator.serviceWorker.onmessage = (event) => { + resolve(event.data); + }; + // The jobs should not have been coalesced. get_newest_worker() helps the + // test fail with stable output on browers that incorrectly coalesce + // register jobs, since then sometimes registration is not a new worker as + // expected. + const worker = get_newest_worker(registration); + // The argument of postMessage doesn't matter for this case. + worker.postMessage(''); + }); + + assert_equals(worker_type, 'module'); +}, 'different type'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-scope-module-static-import.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-scope-module-static-import.https.html new file mode 100644 index 0000000000..5c75295aed --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-scope-module-static-import.https.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<title>Service Worker: Static imports from module top-level scripts shouldn't be affected by the service worker script path restriction</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +// https://w3c.github.io/ServiceWorker/#path-restriction +// is applied to top-level scripts in +// https://w3c.github.io/ServiceWorker/#update-algorithm +// but not to submodules imported from top-level scripts. +async function runTest(t, script, scope) { + const script_url = new URL(script, location.href); + await service_worker_unregister(t, scope); + const registration = await + navigator.serviceWorker.register(script, {type: 'module'}); + t.add_cleanup(_ => registration.unregister()); + const msg = await new Promise(resolve => { + registration.installing.postMessage('ping'); + navigator.serviceWorker.onmessage = resolve; + }); + assert_equals(msg.data, 'pong'); +} + +promise_test(async t => { + await runTest(t, + 'resources/scope2/imported-module-script.js', + 'resources/scope2/'); + }, 'imported-module-script.js works when used as top-level'); + +promise_test(async t => { + await runTest(t, + 'resources/scope1/module-worker-importing-scope2.js', + 'resources/scope1/'); + }, 'static imports to outside path restriction should be allowed'); + +promise_test(async t => { + await runTest(t, + 'resources/scope1/module-worker-importing-redirect-to-scope2.js', + 'resources/scope1/'); + }, 'static imports redirecting to outside path restriction should be allowed'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-scope.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-scope.https.html new file mode 100644 index 0000000000..141875f584 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-scope.https.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<title>Service Worker: Registration (scope)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="resources/registration-tests-scope.js"></script> +<script> +registration_tests_scope((script, options) => navigator.serviceWorker.register(script, options)); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-script-module.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-script-module.https.html new file mode 100644 index 0000000000..9e39a1f75b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-script-module.https.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<title>Service Worker: Registration (module script)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="resources/registration-tests-script.js"></script> +<script> +registration_tests_script( + (script, options) => navigator.serviceWorker.register( + script, + Object.assign({type: 'module'}, options)), + 'module'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-script-url.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-script-url.https.html new file mode 100644 index 0000000000..bda61adb00 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-script-url.https.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<title>Service Worker: Registration (scriptURL)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="resources/registration-tests-script-url.js"></script> +<script> +registration_tests_script_url((script, options) => navigator.serviceWorker.register(script, options)); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-script.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-script.https.html new file mode 100644 index 0000000000..f1e51fd265 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-script.https.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<title>Service Worker: Registration (script)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="resources/registration-tests-script.js"></script> +<script> +registration_tests_script( + (script, options) => navigator.serviceWorker.register(script, options), + 'classic' +); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-security-error.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-security-error.https.html new file mode 100644 index 0000000000..860c2d22ea --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-security-error.https.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<title>Service Worker: Registration (SecurityError)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="resources/registration-tests-security-error.js"></script> +<script> +registration_tests_security_error((script, options) => navigator.serviceWorker.register(script, options)); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-service-worker-attributes.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-service-worker-attributes.https.html new file mode 100644 index 0000000000..f7b52d5ddc --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-service-worker-attributes.https.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; +promise_test(function(t) { + var scope = 'resources/scope/installing-waiting-active-after-registration'; + var worker_url = 'resources/empty-worker.js'; + var expected_url = normalizeURL(worker_url); + var newest_worker; + var registration; + + return service_worker_unregister_and_register(t, worker_url, scope) + .then(function(r) { + t.add_cleanup(function() { + return r.unregister(); + }); + registration = r; + newest_worker = registration.installing; + assert_equals(registration.installing.scriptURL, expected_url, + 'installing before updatefound'); + assert_equals(registration.waiting, null, + 'waiting before updatefound'); + assert_equals(registration.active, null, + 'active before updatefound'); + return wait_for_update(t, registration); + }) + .then(function() { + assert_equals(registration.installing, newest_worker, + 'installing after updatefound'); + assert_equals(registration.waiting, null, + 'waiting after updatefound'); + assert_equals(registration.active, null, + 'active after updatefound'); + return wait_for_state(t, registration.installing, 'installed'); + }) + .then(function() { + assert_equals(registration.installing, null, + 'installing after installed'); + assert_equals(registration.waiting, newest_worker, + 'waiting after installed'); + assert_equals(registration.active, null, + 'active after installed'); + return wait_for_state(t, registration.waiting, 'activated'); + }) + .then(function() { + assert_equals(registration.installing, null, + 'installing after activated'); + assert_equals(registration.waiting, null, + 'waiting after activated'); + assert_equals(registration.active, newest_worker, + 'active after activated'); + return Promise.all([ + wait_for_state(t, registration.active, 'redundant'), + registration.unregister() + ]); + }) + .then(function() { + assert_equals(registration.installing, null, + 'installing after redundant'); + assert_equals(registration.waiting, null, + 'waiting after redundant'); + // According to spec, Clear Registration runs Update State which is + // immediately followed by setting active to null, which means by the + // time the event loop turns and the Promise for statechange is + // resolved, this will be gone. + assert_equals(registration.active, null, + 'active should be null after redundant'); + }); + }, 'installing/waiting/active after registration'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-updateviacache.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-updateviacache.https.html new file mode 100644 index 0000000000..b2f6bbc6f8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/registration-updateviacache.https.html @@ -0,0 +1,204 @@ +<!DOCTYPE html> +<title>Service Worker: Registration-updateViaCache</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + const UPDATE_VIA_CACHE_VALUES = [undefined, 'imports', 'all', 'none']; + const SCRIPT_URL = 'resources/update-max-aged-worker.py'; + const SCOPE = 'resources/blank.html'; + + async function cleanup() { + const reg = await navigator.serviceWorker.getRegistration(SCOPE); + if (!reg) return; + if (reg.scope == new URL(SCOPE, location).href) { + return reg.unregister(); + }; + } + + function getScriptTimes(sw, testName) { + return new Promise(resolve => { + navigator.serviceWorker.addEventListener('message', function listener(event) { + if (event.data.test !== testName) return; + navigator.serviceWorker.removeEventListener('message', listener); + resolve({ + mainTime: event.data.mainTime, + importTime: event.data.importTime + }); + }); + + sw.postMessage(''); + }); + } + + // Test creating registrations & triggering an update. + for (const updateViaCache of UPDATE_VIA_CACHE_VALUES) { + const testName = `register-with-updateViaCache-${updateViaCache}`; + + promise_test(async t => { + await cleanup(); + + const opts = {scope: SCOPE}; + + if (updateViaCache) opts.updateViaCache = updateViaCache; + + const reg = await navigator.serviceWorker.register( + `${SCRIPT_URL}?test=${testName}`, + opts + ); + + assert_equals(reg.updateViaCache, updateViaCache || 'imports', "reg.updateViaCache"); + + const sw = reg.installing || reg.waiting || reg.active; + await wait_for_state(t, sw, 'activated'); + const values = await getScriptTimes(sw, testName); + await reg.update(); + + if (updateViaCache == 'all') { + assert_equals(reg.installing, null, "No new service worker"); + } + else { + const newWorker = reg.installing; + assert_true(!!newWorker, "New worker installing"); + const newValues = await getScriptTimes(newWorker, testName); + + if (!updateViaCache || updateViaCache == 'imports') { + assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated"); + assert_equals(values.importTime, newValues.importTime, "Imported script should be the same"); + } + else if (updateViaCache == 'none') { + assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated"); + assert_not_equals(values.importTime, newValues.importTime, "Imported script should have updated"); + } + else { + // We should have handled all of the possible values for updateViaCache. + // If this runs, something's gone very wrong. + throw Error(`Unexpected updateViaCache value: ${updateViaCache}`); + } + } + + await cleanup(); + }, testName); + } + + // Test changing the updateViaCache value of an existing registration. + for (const updateViaCache1 of UPDATE_VIA_CACHE_VALUES) { + for (const updateViaCache2 of UPDATE_VIA_CACHE_VALUES) { + const testName = `register-with-updateViaCache-${updateViaCache1}-then-${updateViaCache2}`; + + promise_test(async t => { + await cleanup(); + + const fullScriptUrl = `${SCRIPT_URL}?test=${testName}`; + let opts = {scope: SCOPE}; + if (updateViaCache1) opts.updateViaCache = updateViaCache1; + + const reg = await navigator.serviceWorker.register(fullScriptUrl, opts); + + const sw = reg.installing; + await wait_for_state(t, sw, 'activated'); + const values = await getScriptTimes(sw, testName); + + const frame = await with_iframe(SCOPE); + const reg_in_frame = await frame.contentWindow.navigator.serviceWorker.getRegistration(normalizeURL(SCOPE)); + assert_equals(reg_in_frame.updateViaCache, updateViaCache1 || 'imports', "reg_in_frame.updateViaCache"); + + opts = {scope: SCOPE}; + if (updateViaCache2) opts.updateViaCache = updateViaCache2; + + await navigator.serviceWorker.register(fullScriptUrl, opts); + + const expected_updateViaCache = updateViaCache2 || 'imports'; + + assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache updated"); + // If the update happens via the cache, the scripts will come back byte-identical. + // We bypass the byte-identical check if the script URL has changed, but not if + // only the updateViaCache value has changed. + if (updateViaCache2 == 'all') { + assert_equals(reg.installing, null, "No new service worker"); + } + // If there's no change to the updateViaCache value, register should be a no-op. + // The default value should behave as 'imports'. + else if ((updateViaCache1 || 'imports') == (updateViaCache2 || 'imports')) { + assert_equals(reg.installing, null, "No new service worker"); + } + else { + const newWorker = reg.installing; + assert_true(!!newWorker, "New worker installing"); + const newValues = await getScriptTimes(newWorker, testName); + + if (!updateViaCache2 || updateViaCache2 == 'imports') { + assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated"); + assert_equals(values.importTime, newValues.importTime, "Imported script should be the same"); + } + else if (updateViaCache2 == 'none') { + assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated"); + assert_not_equals(values.importTime, newValues.importTime, "Imported script should have updated"); + } + else { + // We should have handled all of the possible values for updateViaCache2. + // If this runs, something's gone very wrong. + throw Error(`Unexpected updateViaCache value: ${updateViaCache}`); + } + } + + // Wait for all registration related tasks on |frame| to complete. + await wait_for_activation_on_sample_scope(t, frame.contentWindow); + // The updateViaCache change should have been propagated to all + // corresponding JS registration objects. + assert_equals(reg_in_frame.updateViaCache, expected_updateViaCache, "reg_in_frame.updateViaCache updated"); + frame.remove(); + + await cleanup(); + }, testName); + } + } + + // Test accessing updateViaCache of an unregistered registration. + for (const updateViaCache of UPDATE_VIA_CACHE_VALUES) { + const testName = `access-updateViaCache-after-unregister-${updateViaCache}`; + + promise_test(async t => { + await cleanup(); + + const opts = {scope: SCOPE}; + + if (updateViaCache) opts.updateViaCache = updateViaCache; + + const reg = await navigator.serviceWorker.register( + `${SCRIPT_URL}?test=${testName}`, + opts + ); + + const expected_updateViaCache = updateViaCache || 'imports'; + assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache"); + + await reg.unregister(); + + // Keep the original value. + assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache"); + + await cleanup(); + }, testName); + } + + promise_test(async t => { + await cleanup(); + t.add_cleanup(cleanup); + + const registration = await navigator.serviceWorker.register( + 'resources/empty.js', + {scope: SCOPE}); + assert_equals(registration.updateViaCache, 'imports', + 'before update attempt'); + + const fail = navigator.serviceWorker.register( + 'resources/malformed-worker.py?parse-error', + {scope: SCOPE, updateViaCache: 'none'}); + await promise_rejects_js(t, TypeError, fail); + assert_equals(registration.updateViaCache, 'imports', + 'after update attempt'); + }, 'updateViaCache is not updated if register() rejects'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/rejections.https.html b/testing/web-platform/tests/service-workers/service-worker/rejections.https.html new file mode 100644 index 0000000000..8002ad9a81 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/rejections.https.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<title>Service Worker: Rejection Types</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +(function() { + var t = async_test('Rejections are DOMExceptions'); + t.step(function() { + + navigator.serviceWorker.register('http://example.com').then( + t.step_func(function() { assert_unreached('Registration should fail'); }), + t.step_func(function(reason) { + assert_true(reason instanceof DOMException); + assert_true(reason instanceof Error); + t.done(); + })); + }); +}()); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/request-end-to-end.https.html b/testing/web-platform/tests/service-workers/service-worker/request-end-to-end.https.html new file mode 100644 index 0000000000..a39ceadd9f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/request-end-to-end.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<title>Service Worker: FetchEvent.request passed to onfetch</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +promise_test(t => { + var url = 'resources/request-end-to-end-worker.js'; + var scope = 'resources/blank.html'; + return service_worker_unregister_and_register(t, url, scope) + .then(r => { + add_completion_callback(() => { r.unregister(); }); + return wait_for_state(t, r.installing, 'activated'); + }) + .then(() => { return with_iframe(scope); }) + .then(frame => { + add_completion_callback(() => { frame.remove(); }); + + var result = JSON.parse(frame.contentDocument.body.textContent); + assert_equals(result.url, frame.src, 'request.url'); + assert_equals(result.method, 'GET', 'request.method'); + assert_equals(result.referrer, location.href, 'request.referrer'); + assert_equals(result.mode, 'navigate', 'request.mode'); + assert_equals(result.request_construct_error, '', + 'Constructing a Request with a Request whose mode ' + + 'is navigate and non-empty RequestInit must not throw a ' + + 'TypeError.') + assert_equals(result.credentials, 'include', 'request.credentials'); + assert_equals(result.redirect, 'manual', 'request.redirect'); + assert_equals(result.headers['user-agent'], undefined, + 'Default User-Agent header should not be passed to ' + + 'onfetch event.') + assert_equals(result.append_header_error, 'TypeError', + 'Appending a new header to the request must throw a ' + + 'TypeError.') + }); + }, 'Test FetchEvent.request passed to onfetch'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resource-timing-bodySize.https.html b/testing/web-platform/tests/service-workers/service-worker/resource-timing-bodySize.https.html new file mode 100644 index 0000000000..5c2b1eba8c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resource-timing-bodySize.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +const {REMOTE_ORIGIN} = get_host_info(); + +/* + This test does the following: + - Loads a service worker + - Loads an iframe in the service worker's scope + - The service worker tries to fetch a resource which is either: + - constructed inside the service worker + - fetched from a different URL ny the service worker + - Streamed from a differend URL by the service worker + - Passes through + - By default the RT entry should have encoded/decoded body size. except for + the case where the response is an opaque pass-through. +*/ +function test_scenario({tao, mode, name}) { + promise_test(async (t) => { + const uid = token(); + const worker_url = `resources/fetch-response.js?uid=${uid}`; + const scope = `resources/fetch-response.html?uid=${uid}`; + const iframe = document.createElement('iframe'); + const path = name === "passthrough" ? `element-timing/resources/TAOImage.py?origin=*&tao=${ + tao === "pass" ? "wildcard" : "none"})}` : name; + + iframe.src = `${scope}&path=${encodeURIComponent( + `${mode === "same-origin" ? "" : REMOTE_ORIGIN}/${path}`)}&mode=${mode}`; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + t.add_cleanup(() => iframe.remove()); + await wait_for_state(t, registration.installing, 'activated'); + const waitForMessage = new Promise(resolve => + window.addEventListener('message', ({data}) => resolve(data))); + document.body.appendChild(iframe); + const {buffer, entry} = await waitForMessage; + const expectPass = name !== "passthrough" || mode !== "no-cors"; + assert_equals(buffer.byteLength, expectPass ? entry.decodedBodySize : 0); + assert_equals(buffer.byteLength, expectPass ? entry.encodedBodySize : 0); + }, `Response body size: ${name}, ${mode}, TAO ${tao}`); +} +for (const mode of ["cors", "no-cors", "same-origin"]) { + for (const tao of ["pass", "fail"]) + for (const name of ['constructed', 'forward', 'stream', 'passthrough']) { + test_scenario({tao, mode, name}); + } +} + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin.https.html b/testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin.https.html new file mode 100644 index 0000000000..2155d7ff6e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin.https.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates Resource Timing for cross origin content fetched by Service Worker from an originally same-origin URL.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +</head> + +<body> +<script> +function test_sw_resource_timing({ mode }) { + promise_test(async t => { + const worker_url = `resources/worker-fetching-cross-origin.js?mode=${mode}`; + const scope = 'resources/iframe-with-image.html'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(scope); + const frame_performance = frame.contentWindow.performance; + // Check that there is one entry for which the timing allow check algorithm failed. + const entries = frame_performance.getEntriesByType('resource'); + assert_equals(entries.length, 1); + const entry = entries[0]; + assert_equals(entry.redirectStart, 0, 'redirectStart should be 0 in cross-origin request.'); + assert_equals(entry.redirectEnd, 0, 'redirectEnd should be 0 in cross-origin request.'); + assert_equals(entry.domainLookupStart, entry.fetchStart, 'domainLookupStart should be 0 in cross-origin request.'); + assert_equals(entry.domainLookupEnd, entry.fetchStart, 'domainLookupEnd should be 0 in cross-origin request.'); + assert_equals(entry.connectStart, entry.fetchStart, 'connectStart should be 0 in cross-origin request.'); + assert_equals(entry.connectEnd, entry.fetchStart, 'connectEnd should be 0 in cross-origin request.'); + assert_greater_than(entry.responseStart, entry.fetchStart, 'responseStart should be 0 in cross-origin request.'); + assert_equals(entry.secureConnectionStart, entry.fetchStart, 'secureConnectionStart should be 0 in cross-origin request.'); + assert_equals(entry.transferSize, 0, 'decodedBodySize should be 0 in cross-origin request.'); + frame.remove(); + await registration.unregister(); + }, `Test that timing allow check fails when service worker changes origin from same to cross origin (${mode}).`); +} + +test_sw_resource_timing({ mode: "cors" }); +test_sw_resource_timing({ mode: "no-cors" }); + + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html b/testing/web-platform/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html new file mode 100644 index 0000000000..8d4f0be01a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test various interactions between fetch, service-workers and resource timing</title> +<meta charset="utf-8" /> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<link rel="help" href="https://w3c.github.io/resource-timing/" > +<!-- + This test checks that the different properties in a PerformanceResourceTimingEntry + measure what they are supposed to measure according to spec. + + It is achieved by generating programmatic delays and redirects inside a service worker, + and checking how the different metrics respond to the delays and redirects. + + The deltas are not measured precisely, but rather relatively to the delay. + The delay needs to be long enough so that it's clear that what's measured is the test's + programmatic delay and not arbitrary system delays. +--> +</head> + +<body> +<script> + +const delay = 200; +const absolutePath = `${base_path()}/simple.txt` +function toSequence({before, after, entry}) { + /* + The order of keys is the same as in this chart: + https://w3c.github.io/resource-timing/#attribute-descriptions + */ + const keys = [ + 'startTime', + 'redirectStart', + 'redirectEnd', + 'workerStart', + 'fetchStart', + 'connectStart', + 'requestStart', + 'responseStart', + 'responseEnd' + ]; + + let cursor = before; + const step = value => { + // A zero/null value, reflect that in the sequence + if (!value) + return value; + + // Value is the same as before + if (value === cursor) + return "same"; + + // Oops, value is in the wrong place + if (value < cursor) + return "back"; + + // Delta is greater than programmatic delay, this is where the delay is measured. + if ((value - cursor) >= delay) + return "delay"; + + // Some small delta, probably measuring an actual networking stack delay + return "tick"; + } + + const res = keys.map(key => { + const value = step(entry[key]); + if (entry[key]) + cursor = entry[key]; + return [key, value]; + }); + + return Object.fromEntries([...res, ['after', step(after)]]); +} +async function testVariant(t, variant) { + const worker_url = 'resources/fetch-variants-worker.js'; + const url = encodeURIComponent(`simple.txt?delay=${delay}&variant=${variant}`); + const scope = `resources/iframe-with-fetch-variants.html?url=${url}`; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + const result = await new Promise(resolve => window.addEventListener('message', message => { + resolve(message.data); + })) + + return toSequence(result); +} + +promise_test(async t => { + const result = await testVariant(t, 'redirect'); + assert_equals(result.redirectStart, 0); +}, 'Redirects done from within a service-worker should not be exposed to client ResourceTiming'); + +promise_test(async t => { + const result = await testVariant(t, 'forward'); + assert_equals(result.connectStart, 'same'); +}, 'Connection info from within a service-worker should not be exposed to client ResourceTiming'); + +promise_test(async t => { + const result = await testVariant(t, 'forward'); + assert_not_equals(result.requestStart, 'back'); +}, 'requestStart should never be before fetchStart'); + +promise_test(async t => { + const result = await testVariant(t, 'delay-after-fetch'); + const whereIsDelayMeasured = Object.entries(result).find(r => r[1] === 'delay')[0]; + assert_equals(whereIsDelayMeasured, 'responseStart'); +}, 'Delay from within service-worker (after internal fetching) should be accessible through `responseStart`'); + +promise_test(async t => { + const result = await testVariant(t, 'delay-before-fetch'); + const whereIsDelayMeasured = Object.entries(result).find(r => r[1] === 'delay')[0]; + assert_equals(whereIsDelayMeasured, 'responseStart'); +}, 'Delay from within service-worker (before internal fetching) should be measured before responseStart in the client ResourceTiming entry'); +</script> + +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resource-timing.sub.https.html b/testing/web-platform/tests/service-workers/service-worker/resource-timing.sub.https.html new file mode 100644 index 0000000000..e8328f3597 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resource-timing.sub.https.html @@ -0,0 +1,150 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +function resourceUrl(path) { + return "https://{{host}}:{{ports[https][0]}}" + base_path() + path; +} + +function crossOriginUrl(path) { + return "https://{{hosts[alt][]}}:{{ports[https][0]}}" + base_path() + path; +} + +// Verify existance of a PerformanceEntry and the order between the timings. +// +// |options| has these properties: +// performance: Performance interface to verify existance of the entry. +// resource: the path to the resource. +// mode: 'cross-origin' to load the resource from third-party origin. +// description: the description passed to each assertion. +// should_no_performance_entry: no entry is expected to be recorded when it's +// true. +function verify(options) { + const url = options.mode === 'cross-origin' ? crossOriginUrl(options.resource) + : resourceUrl(options.resource); + const entryList = options.performance.getEntriesByName(url, 'resource'); + if (options.should_no_performance_entry) { + // The performance timeline may not have an entry for a resource + // which failed to load. + assert_equals(entryList.length, 0, options.description); + return; + } + + assert_equals(entryList.length, 1, options.description); + const entry = entryList[0]; + assert_equals(entry.entryType, 'resource', options.description); + + // workerStart is recorded between startTime and fetchStart. + assert_greater_than(entry.workerStart, 0, options.description); + assert_greater_than_equal(entry.workerStart, entry.startTime, options.description); + assert_less_than_equal(entry.workerStart, entry.fetchStart, options.description); + + if (options.mode === 'cross-origin') { + assert_equals(entry.responseStart, 0, options.description); + assert_greater_than_equal(entry.responseEnd, entry.fetchStart, options.description); + } else { + assert_greater_than_equal(entry.responseStart, entry.fetchStart, options.description); + assert_greater_than_equal(entry.responseEnd, entry.responseStart, options.description); + } + + // responseEnd follows fetchStart. + assert_greater_than(entry.responseEnd, entry.fetchStart, options.description); + // duration always has some value. + assert_greater_than(entry.duration, 0, options.description); + + if (options.resource.indexOf('redirect.py') != -1) { + assert_less_than_equal(entry.workerStart, entry.redirectStart, + options.description); + } else { + assert_equals(entry.redirectStart, 0, options.description); + } +} + +promise_test(async (t) => { + const worker_url = 'resources/resource-timing-worker.js'; + const scope = 'resources/resource-timing-iframe.sub.html'; + + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + const performance = frame.contentWindow.performance; + verify({ + performance: performance, + resource: 'resources/sample.js', + mode: 'same-origin', + description: 'Generated response', + }); + verify({ + performance: performance, + resource: 'resources/empty.js', + mode: 'same-origin', + description: 'Network fallback', + }); + verify({ + performance: performance, + resource: 'resources/redirect.py?Redirect=empty.js', + mode: 'same-origin', + description: 'Redirect', + }); + verify({ + performance: performance, + resource: 'resources/square.png', + mode: 'same-origin', + description: 'Network fallback image', + }); + // Test that worker start is available on cross-origin no-cors + // subresources. + verify({ + performance: performance, + resource: 'resources/square.png', + mode: 'cross-origin', + description: 'Network fallback cross-origin image', + }); + + // Tests for resouces which failed to load. + verify({ + performance: performance, + resource: 'resources/missing.jpg', + mode: 'same-origin', + description: 'Network fallback load failure', + }); + verify({ + performance: performance, + resource: 'resources/missing.asis', // ORB-compatible 404 response. + mode: 'cross-origin', + description: 'Network fallback cross-origin load failure (404 response)', + }); + // Tests for respondWith(fetch()). + verify({ + performance: performance, + resource: 'resources/missing.jpg?SWRespondsWithFetch', + mode: 'same-origin', + description: 'Resource in iframe, nonexistent but responded with fetch to another.', + }); + verify({ + performance: performance, + resource: 'resources/sample.txt?SWFetched', + mode: 'same-origin', + description: 'Resource fetched as response from missing.jpg?SWRespondsWithFetch.', + should_no_performance_entry: true, + }); + // Test for a normal resource that is unaffected by the Service Worker. + verify({ + performance: performance, + resource: 'resources/empty-worker.js', + mode: 'same-origin', + description: 'Resource untouched by the Service Worker.', + }); +}, 'Controlled resource loads'); + +test(() => { + const url = resourceUrl('resources/test-helpers.sub.js'); + const entry = window.performance.getEntriesByName(url, 'resource')[0]; + assert_equals(entry.workerStart, 0, 'Non-controlled'); +}, 'Non-controlled resource loads'); + +</script> 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/controlled-frame-postMessage.html b/testing/web-platform/tests/service-workers/service-worker/resources/controlled-frame-postMessage.html new file mode 100644 index 0000000000..c4428e88a3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/controlled-frame-postMessage.html @@ -0,0 +1,39 @@ +<html> +<body> +<script> +var messageData; +function registerMessage() +{ + navigator.serviceWorker.onmessage = (e) => { + if (window.messageData === undefined) + window.messageData = e.data; + } +} + +function listenToMessages() +{ + messageData = []; + setTimeout(() => { + navigator.serviceWorker.addEventListener("message", (e) => { + messageData.push(e.data); + }, { once:true }); + }, 500); + setTimeout(() => { + navigator.serviceWorker.onmessage = (e) => { + messageData.push(e.data); + }; + }, 1000); +} + +if (window.location.search === "?repeatMessages") { + setTimeout(() => { + registerMessage(); + }, 500); +} else if (window.location.search.includes("listener")) { + listenToMessages(); +} else { + registerMessage(); +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js b/testing/web-platform/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js new file mode 100644 index 0000000000..41d2db43b1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js @@ -0,0 +1,6 @@ +setTimeout(() => { + navigator.serviceWorker.onmessage = e => self.postMessage(e.data); +}, 500); +setTimeout(() => { + self.postMessage("No message received"); +}, 5000); diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js b/testing/web-platform/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js new file mode 100644 index 0000000000..628dc65db1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js @@ -0,0 +1,4 @@ +navigator.serviceWorker.onmessage = e => self.postMessage(e.data); +setTimeout(() => { + self.postMessage("No message received"); +}, 5000); 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..67c837bca7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control.py @@ -0,0 +1,114 @@ +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: + if b"mp4" in request.GET: + headers.append((b"Content-Type", b"video/mp4")) + body = open(os.path.join(request.doc_root, u"media", u"movie_5.mp4"), "rb").read() + else: + headers.append((b"Content-Type", b"video/webm")) + body = open(os.path.join(request.doc_root, u"media", u"movie_5.webm"), "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/missing.asis b/testing/web-platform/tests/service-workers/service-worker/resources/missing.asis new file mode 100644 index 0000000000..4846fe01d6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/missing.asis @@ -0,0 +1,4 @@ +HTTP/1.1 404 Not Found +Content-Type: text/javascript + +alert("hello"); 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..97f3a9c3df --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html @@ -0,0 +1,77 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8"/> +<meta name="timeout" content="long"> +<title>Service Worker: Partitioned Cookies 3P Iframe</title> +<script src="partitioned-cookies-test-helpers.js"></script> +<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'); + + 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); + + const next_message = worker_message_generator(); + + // First test that the worker script started correctly and message passing + // is enabled. + filtered_registrations[0].active.postMessage({type: 'test_message'}); + const msg1 = await next_message(); + assert_true(msg1.ok, 'Message passing'); + + // Test that the partitioned cookie is not available to this worker via HTTP. + filtered_registrations[0].active.postMessage({type: 'echo_cookies_http'}); + const msg2 = await next_message(); + assert_true(msg2.ok, 'Get cookies'); + assert_false( + msg2.cookies.includes('__Host-partitioned'), + 'Worker cannot access partitioned cookie via HTTP'); + assert_true( + msg2.cookies.includes('unpartitioned'), + 'Worker can access unpartitioned cookie via HTTP'); + + // Test that the partitioned cookie is not available to this worker via + // CookieStore API. + filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'}); + const msg3 = await next_message(); + assert_true(msg3.ok, 'Get cookies'); + assert_false( + msg3.cookies.includes('__Host-partitioned'), + 'Worker cannot access partitioned cookie via JS'); + assert_true( + msg3.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. + filtered_registrations[0].active.postMessage({type: 'echo_cookies_import'}); + const msg4 = await next_message(); + assert_true(msg4.ok, 'Get cookies'); + assert_false( + msg4.cookies.includes('__Host-partitioned'), + 'Worker cannot access partitioned cookie via importScripts'); + assert_true( + msg4.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..c9713b6c50 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js @@ -0,0 +1,3 @@ +// Import the code from the 1P partitioned worker. +// We use separate URLs for each worker script to keep their scopes unique. +importScripts('./partitioned-cookies-sw.js'); 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..40d38b3f79 --- /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); + await 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); + await 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-cookies-test-helpers.js b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-test-helpers.js new file mode 100644 index 0000000000..73f3b0bde0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-test-helpers.js @@ -0,0 +1,31 @@ +// Return a generator containing the worker's message. +// +// Usage: +// await navigator.serviceWorker.register(...) +// ... +// const nextMessage = worker_message_generator(); +// const msg_1 = await nextMessage(); +// const msg_2 = await nextMessage(); +// const msg_3 = await nextMessage(); +// +// Worker should have its own onmessage event listener that +// postMessages replies to the DOM. +function worker_message_generator() { + const buffer = []; + let resolve = null; + + navigator.serviceWorker.addEventListener('message', message => { + buffer.push(message.data); + if (resolve) { + resolve(); + } + }); + + return async function () { + if (buffer.length != 0) { + return buffer.shift(); + } + await new Promise(r => resolve = r); + return buffer.shift(); + } +} 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/range-request-with-synth-head-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-synth-head-worker.js new file mode 100644 index 0000000000..6025d91b1a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-synth-head-worker.js @@ -0,0 +1,36 @@ +// This worker is meant to test range requests where the initial response +// synthesizes a 206 Partial Content response with leading bytes. +// Then the worker lets subsequent range requests fall back to network. + +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; + } + + // Synthesize the response to the first request. + if (is_initial_request()) { + // Consistent with fetch-access-control.py?VIDEO + const init = { + status: 206, + headers: { + "Accept-Ranges": "bytes", + "Content-Type": "video/webm", + "Content-Range": "bytes 0-1/44447", + "Content-Length": "2", + }, + }; + e.respondWith(new Response(new Uint8Array([0x1a, 0x45]), init)); + return; + } + + // Fall back for subsequent range requests. + }); 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..ec4c726331 --- /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.asis"> +<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..025ea671c5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/scope1/redirect.py @@ -0,0 +1,8 @@ +import os + +from tools.wpt.utils import load_source + +# Use the file from the parent directory. +mod = 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..025ea671c5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py @@ -0,0 +1,8 @@ +import os + +from tools.wpt.utils import load_source + +# Use the file from the parent directory. +mod = 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..025ea671c5 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py @@ -0,0 +1,8 @@ +import os + +from tools.wpt.utils import load_source + +# Use the file from the parent directory. +mod = 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> diff --git a/testing/web-platform/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html b/testing/web-platform/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html new file mode 100644 index 0000000000..f6713d8921 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<title>Service Worker responds with .body accessed response.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +let frame; + +promise_test(t => { + const SCOPE = 'resources/respond-with-body-accessed-response-iframe.html'; + const SCRIPT = 'resources/respond-with-body-accessed-response-worker.js'; + + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(reg => { + promise_test(t => { + if (frame) + frame.remove(); + return reg.unregister(); + }, 'restore global state'); + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(() => { return with_iframe(SCOPE); }) + .then(f => { frame = f; }); + }, 'initialize global state'); + +const TEST_CASES = [ + "type=basic", + "type=opaque", + "type=default", + "type=basic&clone=1", + "type=opaque&clone=1", + "type=default&clone=1", + "type=basic&clone=2", + "type=opaque&clone=2", + "type=default&clone=2", + "type=basic&passThroughCache=true", + "type=opaque&passThroughCache=true", + "type=default&passThroughCache=true", + "type=basic&clone=1&passThroughCache=true", + "type=opaque&clone=1&passThroughCache=true", + "type=default&clone=1&passThroughCache=true", + "type=basic&clone=2&passThroughCache=true", + "type=opaque&clone=2&passThroughCache=true", + "type=default&clone=2&passThroughCache=true", +]; + +TEST_CASES.forEach(param => { + promise_test(t => { + const url = 'TestRequest?' + param; + return frame.contentWindow.getJSONP(url) + .then(result => { assert_equals(result, 'OK'); }); + }, 'test: ' + param); + }); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/same-site-cookies.https.html b/testing/web-platform/tests/service-workers/service-worker/same-site-cookies.https.html new file mode 100644 index 0000000000..1d9b60d447 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/same-site-cookies.https.html @@ -0,0 +1,496 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<meta name="timeout" content="long"> +<title>Service Worker: Same-site cookie behavior</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="/cookies/resources/cookie-helper.sub.js"></script> +<body> +<script> +'use strict'; + +const COOKIE_VALUE = 'COOKIE_VALUE'; + +function make_nested_url(nested_origins, target_url) { + for (let i = nested_origins.length - 1; i >= 0; --i) { + target_url = new URL( + `./resources/nested-parent.html?target=${encodeURIComponent(target_url)}`, + nested_origins[i] + self.location.pathname); + } + return target_url; +} + +const scopepath = '/cookies/resources/postToParent.py?with-sw'; + +async function unregister_service_worker(origin, nested_origins=[]) { + let target_url = origin + + '/service-workers/service-worker/resources/unregister-rewrite-worker.html' + + '?scopepath=' + encodeURIComponent(scopepath); + target_url = make_nested_url(nested_origins, target_url); + const w = window.open(target_url); + try { + await wait_for_message('SW-UNREGISTERED'); + } finally { + w.close(); + } +} + +async function register_service_worker(origin, nested_origins=[]) { + let target_url = origin + + '/service-workers/service-worker/resources/register-rewrite-worker.html' + + '?scopepath=' + encodeURIComponent(scopepath); + target_url = make_nested_url(nested_origins, target_url); + const w = window.open(target_url); + try { + await wait_for_message('SW-REGISTERED'); + } finally { + w.close(); + } +} + +async function run_test(t, origin, navaction, swaction, expected, + redirect_origins=[], nested_origins=[]) { + if (swaction === 'navpreload') { + assert_true('navigationPreload' in ServiceWorkerRegistration.prototype, + 'navigation preload must be supported'); + } + const sw_param = swaction === 'no-sw' ? 'no-sw' : 'with-sw'; + let action_param = ''; + if (swaction === 'fallback') { + action_param = '&ignore'; + } else if (swaction !== 'no-sw') { + action_param = '&' + swaction; + } + const navpreload_param = swaction === 'navpreload' ? '&navpreload' : ''; + const change_request_param = swaction === 'change-request' ? '&change-request' : ''; + const target_string = origin + `/cookies/resources/postToParent.py?` + + `${sw_param}${action_param}` + let target_url = new URL(target_string); + + for (let i = redirect_origins.length - 1; i >= 0; --i) { + const redirect_url = new URL( + `./resources/redirect.py?Status=307&Redirect=${encodeURIComponent(target_url)}`, + redirect_origins[i] + self.location.pathname); + target_url = redirect_url; + } + + if (navaction === 'window.open') { + target_url = new URL( + `./resources/window-opener.html?target=${encodeURIComponent(target_url)}`, + self.origin + self.location.pathname); + } else if (navaction === 'form post') { + target_url = new URL( + `./resources/form-poster.html?target=${encodeURIComponent(target_url)}`, + self.origin + self.location.pathname); + } else if (navaction === 'set location') { + target_url = new URL( + `./resources/location-setter.html?target=${encodeURIComponent(target_url)}`, + self.origin + self.location.pathname); + } + + const w = window.open(make_nested_url(nested_origins, target_url)); + t.add_cleanup(() => w.close()); + + const result = await wait_for_message('COOKIES'); + verifySameSiteCookieState(expected, COOKIE_VALUE, result.data); +} + +promise_test(async t => { + await resetSameSiteCookies(self.origin, COOKIE_VALUE); + await register_service_worker(self.origin); + + await resetSameSiteCookies(SECURE_SUBDOMAIN_ORIGIN, COOKIE_VALUE); + await register_service_worker(SECURE_SUBDOMAIN_ORIGIN); + + await resetSameSiteCookies(SECURE_CROSS_SITE_ORIGIN, COOKIE_VALUE); + await register_service_worker(SECURE_CROSS_SITE_ORIGIN); + + await register_service_worker(self.origin, + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'Setup service workers'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'no-sw', + SameSiteStatus.STRICT); +}, 'same-origin, window.open with no service worker'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'fallback', + SameSiteStatus.STRICT); +}, 'same-origin, window.open with fallback'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'passthrough', + SameSiteStatus.STRICT); +}, 'same-origin, window.open with passthrough'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'change-request', + SameSiteStatus.STRICT); +}, 'same-origin, window.open with change-request'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'navpreload', + SameSiteStatus.STRICT); +}, 'same-origin, window.open with navpreload'); + +promise_test(t => { + return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'no-sw', + SameSiteStatus.STRICT); +}, 'same-site, window.open with no service worker'); + +promise_test(t => { + return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'fallback', + SameSiteStatus.STRICT); +}, 'same-site, window.open with fallback'); + +promise_test(t => { + return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'passthrough', + SameSiteStatus.STRICT); +}, 'same-site, window.open with passthrough'); + +promise_test(t => { + return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'change-request', + SameSiteStatus.STRICT); +}, 'same-site, window.open with change-request'); + +promise_test(t => { + return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'navpreload', + SameSiteStatus.STRICT); +}, 'same-site, window.open with navpreload'); + +promise_test(t => { + return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'no-sw', + SameSiteStatus.LAX); +}, 'cross-site, window.open with no service worker'); + +promise_test(t => { + return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'fallback', + SameSiteStatus.LAX); +}, 'cross-site, window.open with fallback'); + +promise_test(t => { + return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'passthrough', + SameSiteStatus.LAX); +}, 'cross-site, window.open with passthrough'); + +promise_test(t => { + return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'change-request', + SameSiteStatus.STRICT); +}, 'cross-site, window.open with change-request'); + +promise_test(t => { + return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'navpreload', + SameSiteStatus.LAX); +}, 'cross-site, window.open with navpreload'); + +// +// window.open redirect tests +// +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'no-sw', + SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]); +}, 'same-origin, window.open with no service worker and same-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'fallback', + SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]); +}, 'same-origin, window.open with fallback and same-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'passthrough', + SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]); +}, 'same-origin, window.open with passthrough and same-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'change-request', + SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]); +}, 'same-origin, window.open with change-request and same-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'navpreload', + SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]); +}, 'same-origin, window.open with navpreload and same-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'no-sw', + SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, window.open with no service worker and cross-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'fallback', + SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, window.open with fallback and cross-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'passthrough', + SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, window.open with passthrough and cross-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'change-request', + SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, window.open with change-request and cross-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'navpreload', + SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, window.open with navpreload and cross-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'no-sw', + SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]); +}, 'same-origin, window.open with no service worker, cross-site redirect, and ' + + 'same-origin redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'fallback', + SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]); +}, 'same-origin, window.open with fallback, cross-site redirect, and ' + + 'same-origin redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'passthrough', + SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]); +}, 'same-origin, window.open with passthrough, cross-site redirect, and ' + + 'same-origin redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'change-request', + SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN, self.origin]); +}, 'same-origin, window.open with change-request, cross-site redirect, and ' + + 'same-origin redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'navpreload', + SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]); +}, 'same-origin, window.open with navpreload, cross-site redirect, and ' + + 'same-origin redirect'); + +// +// Double-nested frame calling open.window() tests +// +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'no-sw', + SameSiteStatus.STRICT, [], + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, nested window.open with cross-site middle frame and ' + + 'no service worker'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'fallback', + SameSiteStatus.STRICT, [], + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, nested window.open with cross-site middle frame and ' + + 'fallback service worker'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'passthrough', + SameSiteStatus.STRICT, [], + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, nested window.open with cross-site middle frame and ' + + 'passthrough service worker'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'change-request', + SameSiteStatus.STRICT, [], + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, nested window.open with cross-site middle frame and ' + + 'change-request service worker'); + +promise_test(t => { + return run_test(t, self.origin, 'window.open', 'navpreload', + SameSiteStatus.STRICT, [], + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, nested window.open with cross-site middle frame and ' + + 'navpreload service worker'); + +// +// Double-nested frame setting location tests +// +promise_test(t => { + return run_test(t, self.origin, 'set location', 'no-sw', + SameSiteStatus.CROSS_SITE, [], + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, nested set location with cross-site middle frame and ' + + 'no service worker'); + +promise_test(t => { + return run_test(t, self.origin, 'set location', 'fallback', + SameSiteStatus.CROSS_SITE, [], + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, nested set location with cross-site middle frame and ' + + 'fallback service worker'); + +promise_test(t => { + return run_test(t, self.origin, 'set location', 'passthrough', + SameSiteStatus.CROSS_SITE, [], + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, nested set location with cross-site middle frame and ' + + 'passthrough service worker'); + +promise_test(t => { + return run_test(t, self.origin, 'set location', 'change-request', + SameSiteStatus.CROSS_SITE, [], + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, nested set location with cross-site middle frame and ' + + 'change-request service worker'); + +promise_test(t => { + return run_test(t, self.origin, 'set location', 'navpreload', + SameSiteStatus.CROSS_SITE, [], + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, nested set location with cross-site middle frame and ' + + 'navpreload service worker'); + +// +// Form POST tests +// +promise_test(t => { + return run_test(t, self.origin, 'form post', 'no-sw', SameSiteStatus.STRICT); +}, 'same-origin, form post with no service worker'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'fallback', + SameSiteStatus.STRICT); +}, 'same-origin, form post with fallback'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'passthrough', + SameSiteStatus.STRICT); +}, 'same-origin, form post with passthrough'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'change-request', + SameSiteStatus.STRICT); +}, 'same-origin, form post with change-request'); + +promise_test(t => { + return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'no-sw', + SameSiteStatus.STRICT); +}, 'same-site, form post with no service worker'); + +promise_test(t => { + return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'fallback', + SameSiteStatus.STRICT); +}, 'same-site, form post with fallback'); + +promise_test(t => { + return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'passthrough', + SameSiteStatus.STRICT); +}, 'same-site, form post with passthrough'); + +promise_test(t => { + return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'change-request', + SameSiteStatus.STRICT); +}, 'same-site, form post with change-request'); + +promise_test(t => { + return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'no-sw', + SameSiteStatus.CROSS_SITE); +}, 'cross-site, form post with no service worker'); + +promise_test(t => { + return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'fallback', + SameSiteStatus.CROSS_SITE); +}, 'cross-site, form post with fallback'); + +promise_test(t => { + return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'passthrough', + SameSiteStatus.CROSS_SITE); +}, 'cross-site, form post with passthrough'); + +promise_test(t => { + return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'change-request', + SameSiteStatus.STRICT); +}, 'cross-site, form post with change-request'); + +// +// Form POST redirect tests +// +promise_test(t => { + return run_test(t, self.origin, 'form post', 'no-sw', + SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]); +}, 'same-origin, form post with no service worker and same-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'fallback', + SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]); +}, 'same-origin, form post with fallback and same-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'passthrough', + SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]); +}, 'same-origin, form post with passthrough and same-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'change-request', + SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]); +}, 'same-origin, form post with change-request and same-site redirect'); + +// navpreload is not supported for POST requests + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'no-sw', + SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, form post with no service worker and cross-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'fallback', + SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, form post with fallback and cross-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'passthrough', + SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, form post with passthrough and cross-site redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'change-request', + SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN]); +}, 'same-origin, form post with change-request and cross-site redirect'); + +// navpreload is not supported for POST requests + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'no-sw', + SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN, + self.origin]); +}, 'same-origin, form post with no service worker, cross-site redirect, and ' + + 'same-origin redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'fallback', + SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN, + self.origin]); +}, 'same-origin, form post with fallback, cross-site redirect, and ' + + 'same-origin redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'passthrough', + SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN, + self.origin]); +}, 'same-origin, form post with passthrough, cross-site redirect, and ' + + 'same-origin redirect'); + +promise_test(t => { + return run_test(t, self.origin, 'form post', 'change-request', + SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN, + self.origin]); +}, 'same-origin, form post with change-request, cross-site redirect, and ' + + 'same-origin redirect'); + +// navpreload is not supported for POST requests + +promise_test(async t => { + await unregister_service_worker(self.origin); + await unregister_service_worker(SECURE_SUBDOMAIN_ORIGIN); + await unregister_service_worker(SECURE_CROSS_SITE_ORIGIN); + await unregister_service_worker(self.origin, + [self.origin, SECURE_CROSS_SITE_ORIGIN]); +}, 'Cleanup service workers'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html b/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html new file mode 100644 index 0000000000..ba34e790ff --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html @@ -0,0 +1,536 @@ +<!DOCTYPE html> +<title>ServiceWorker FetchEvent for sandboxed iframe.</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +var lastCallbackId = 0; +var callbacks = {}; +function doTest(frame, type) { + return new Promise(function(resolve) { + var id = ++lastCallbackId; + callbacks[id] = resolve; + frame.contentWindow.postMessage({id: id, type: type}, '*'); + }); +} + +// Asks the service worker for data about requests and clients seen. The +// worker posts a message back with |data| where: +// |data.requests|: the requests the worker received FetchEvents for +// |data.clients|: the URLs of all the worker's clients +// The worker clears its data after responding. +function getResultsFromWorker(worker) { + return new Promise(resolve => { + let channel = new MessageChannel(); + channel.port1.onmessage = msg => { + resolve(msg.data); + }; + worker.postMessage({port: channel.port2}, [channel.port2]); + }); +} + +window.onmessage = function (e) { + message = e.data; + var id = message['id']; + var callback = callbacks[id]; + delete callbacks[id]; + callback(message['result']); +}; + +const SCOPE = 'resources/sandboxed-iframe-fetch-event-iframe.py'; +const SCRIPT = 'resources/sandboxed-iframe-fetch-event-worker.js'; +const expected_base_url = new URL(SCOPE, location.href); +// Service worker controlling |SCOPE|. +let worker; +// A normal iframe. +// This should be controlled by a service worker. +let normal_frame; +// An iframe created by <iframe sandbox='allow-scripts'>. +// This should NOT be controlled by a service worker. +let sandboxed_frame; +// An iframe created by <iframe sandbox='allow-scripts allow-same-origin'>. +// This should be controlled by a service worker. +let sandboxed_same_origin_frame; +// An iframe whose response header has +// 'Content-Security-Policy: allow-scripts'. +// This should NOT be controlled by a service worker. +let sandboxed_frame_by_header; +// An iframe whose response header has +// 'Content-Security-Policy: allow-scripts allow-same-origin'. +// This should be controlled by a service worker. +let sandboxed_same_origin_frame_by_header; + +promise_test(t => { + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + add_completion_callback(() => registration.unregister()); + worker = registration.installing; + return wait_for_state(t, registration.installing, 'activated'); + }); +}, 'Prepare a service worker.'); + +promise_test(t => { + return with_iframe(SCOPE + '?iframe') + .then(f => { + normal_frame = f; + add_completion_callback(() => f.remove()); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1); + assert_equals(requests[0], expected_base_url + '?iframe'); + assert_true(data.clients.includes(expected_base_url + '?iframe')); + }); +}, 'Prepare a normal iframe.'); + +promise_test(t => { + return with_sandboxed_iframe(SCOPE + '?sandboxed-iframe', 'allow-scripts') + .then(f => { + sandboxed_frame = f; + add_completion_callback(() => f.remove()); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0); + assert_false(data.clients.includes(expected_base_url + + '?sandboxed-iframe')); + }); +}, 'Prepare an iframe sandboxed by <iframe sandbox="allow-scripts">.'); + +promise_test(t => { + return with_sandboxed_iframe(SCOPE + '?sandboxed-iframe-same-origin', + 'allow-scripts allow-same-origin') + .then(f => { + sandboxed_same_origin_frame = f; + add_completion_callback(() => f.remove()); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1); + assert_equals(requests[0], + expected_base_url + '?sandboxed-iframe-same-origin'); + assert_true(data.clients.includes( + expected_base_url + '?sandboxed-iframe-same-origin')); + }) +}, 'Prepare an iframe sandboxed by ' + + '<iframe sandbox="allow-scripts allow-same-origin">.'); + +promise_test(t => { + const iframe_full_url = expected_base_url + '?sandbox=allow-scripts&' + + 'sandboxed-frame-by-header'; + return with_iframe(iframe_full_url) + .then(f => { + sandboxed_frame_by_header = f; + add_completion_callback(() => f.remove()); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'Service worker should provide the response'); + assert_equals(requests[0], iframe_full_url); + assert_false(data.clients.includes(iframe_full_url), + 'Service worker should NOT control the sandboxed page'); + }); +}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts.'); + +promise_test(t => { + const iframe_full_url = + expected_base_url + '?sandbox=allow-scripts%20allow-same-origin&' + + 'sandboxed-iframe-same-origin-by-header'; + return with_iframe(iframe_full_url) + .then(f => { + sandboxed_same_origin_frame_by_header = f; + add_completion_callback(() => f.remove()); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1); + assert_equals(requests[0], iframe_full_url); + assert_true(data.clients.includes(iframe_full_url)); + }) +}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts and ' + + 'allow-same-origin.'); + +promise_test(t => { + let frame = normal_frame; + return doTest(frame, 'fetch') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The fetch request should be handled by SW.'); + assert_equals(requests[0], frame.src + '&test=fetch'); + }); +}, 'Fetch request from a normal iframe'); + +promise_test(t => { + let frame = normal_frame; + return doTest(frame, 'fetch-from-worker') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The fetch request should be handled by SW.'); + assert_equals(requests[0], frame.src + '&test=fetch-from-worker'); + }); +}, 'Fetch request from a worker in a normal iframe'); + +promise_test(t => { + let frame = normal_frame; + return doTest(frame, 'iframe') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The request should be handled by SW.'); + assert_equals(requests[0], frame.src + '&test=iframe'); + assert_true(data.clients.includes(frame.src + '&test=iframe')); + + }); +}, 'Request for an iframe in the normal iframe'); + +promise_test(t => { + let frame = normal_frame; + return doTest(frame, 'sandboxed-iframe') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + assert_false(data.clients.includes( + frame.src + '&test=sandboxed-iframe')); + }); +}, 'Request for an sandboxed iframe with allow-scripts flag in the normal ' + + 'iframe'); + +promise_test(t => { + let frame = normal_frame; + return doTest(frame, 'sandboxed-iframe-same-origin') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The request should be handled by SW.'); + assert_equals(requests[0], + frame.src + '&test=sandboxed-iframe-same-origin'); + assert_true(data.clients.includes( + frame.src + '&test=sandboxed-iframe-same-origin')); + }); +}, 'Request for an sandboxed iframe with allow-scripts and ' + + 'allow-same-origin flag in the normal iframe'); + +promise_test(t => { + let frame = sandboxed_frame; + return doTest(frame, 'fetch') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The fetch request should NOT be handled by SW.'); + }); +}, 'Fetch request from iframe sandboxed by an attribute with allow-scripts ' + + 'flag'); + +promise_test(t => { + let frame = sandboxed_frame; + return doTest(frame, 'fetch-from-worker') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The fetch request should NOT be handled by SW.'); + }); +}, 'Fetch request from a worker in iframe sandboxed by an attribute with ' + + 'allow-scripts flag'); + +promise_test(t => { + let frame = sandboxed_frame; + return doTest(frame, 'iframe') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + assert_false(data.clients.includes(frame.src + '&test=iframe')); + }); +}, 'Request for an iframe in the iframe sandboxed by an attribute with ' + + 'allow-scripts flag'); + +promise_test(t => { + let frame = sandboxed_frame; + return doTest(frame, 'sandboxed-iframe') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + assert_false(data.clients.includes( + frame.src + '&test=sandboxed-iframe')); + }); +}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' + + 'sandboxed by an attribute with allow-scripts flag'); + +promise_test(t => { + let frame = sandboxed_frame; + return doTest(frame, 'sandboxed-iframe-same-origin') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + assert_false(data.clients.includes( + frame.src + '&test=sandboxed-iframe-same-origin')); + }); +}, 'Request for an sandboxed iframe with allow-scripts and ' + + 'allow-same-origin flag in the iframe sandboxed by an attribute with ' + + 'allow-scripts flag'); + +promise_test(t => { + let frame = sandboxed_same_origin_frame; + return doTest(frame, 'fetch') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The fetch request should be handled by SW.'); + assert_equals(requests[0], frame.src + '&test=fetch'); + }); +}, 'Fetch request from iframe sandboxed by an attribute with allow-scripts ' + + 'and allow-same-origin flag'); + +promise_test(t => { + let frame = sandboxed_same_origin_frame; + return doTest(frame, 'fetch-from-worker') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The fetch request should be handled by SW.'); + assert_equals(requests[0], + frame.src + '&test=fetch-from-worker'); + }); +}, 'Fetch request from a worker in iframe sandboxed by an attribute with ' + + 'allow-scripts and allow-same-origin flag'); + +promise_test(t => { + let frame = sandboxed_same_origin_frame; + return doTest(frame, 'iframe') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The request should be handled by SW.'); + assert_equals(requests[0], frame.src + '&test=iframe'); + assert_true(data.clients.includes(frame.src + '&test=iframe')); + }); +}, 'Request for an iframe in the iframe sandboxed by an attribute with ' + + 'allow-scripts and allow-same-origin flag'); + +promise_test(t => { + let frame = sandboxed_same_origin_frame; + return doTest(frame, 'sandboxed-iframe') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + assert_false(data.clients.includes( + frame.src + '&test=sandboxed-iframe')); + }); +}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' + + 'sandboxed by attribute with allow-scripts and allow-same-origin flag'); + +promise_test(t => { + let frame = sandboxed_same_origin_frame; + return doTest(frame, 'sandboxed-iframe-same-origin') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The request should be handled by SW.'); + assert_equals(requests[0], + frame.src + '&test=sandboxed-iframe-same-origin'); + assert_true(data.clients.includes( + frame.src + '&test=sandboxed-iframe-same-origin')); + }); +}, 'Request for an sandboxed iframe with allow-scripts and ' + + 'allow-same-origin flag in the iframe sandboxed by attribute with ' + + 'allow-scripts and allow-same-origin flag'); + +promise_test(t => { + let frame = sandboxed_frame_by_header; + return doTest(frame, 'fetch') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + }); +}, 'Fetch request from iframe sandboxed by CSP HTTP header with ' + + 'allow-scripts flag'); + +promise_test(t => { + let frame = sandboxed_frame_by_header; + return doTest(frame, 'iframe') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + assert_false(data.clients.includes(frame.src + '&test=iframe')); + }); +}, 'Request for an iframe in the iframe sandboxed by CSP HTTP header with ' + + 'allow-scripts flag'); + +promise_test(t => { + let frame = sandboxed_frame_by_header; + return doTest(frame, 'sandboxed-iframe') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + assert_false(data.clients.includes( + frame.src + '&test=sandboxed-iframe')); + }); +}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' + + 'sandboxed by CSP HTTP header with allow-scripts flag'); + +promise_test(t => { + let frame = sandboxed_frame_by_header; + return doTest(frame, 'sandboxed-iframe-same-origin') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + assert_false(data.clients.includes( + frame.src + '&test=sandboxed-iframe-same-origin')); + }); +}, 'Request for an sandboxed iframe with allow-scripts and ' + + 'allow-same-origin flag in the iframe sandboxed by CSP HTTP header with ' + + 'allow-scripts flag'); + +promise_test(t => { + let frame = sandboxed_same_origin_frame_by_header; + return doTest(frame, 'fetch') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The request should be handled by SW.'); + assert_equals(requests[0], frame.src + '&test=fetch'); + }); +}, 'Fetch request from iframe sandboxed by CSP HTTP header with ' + + 'allow-scripts and allow-same-origin flag'); + +promise_test(t => { + let frame = sandboxed_same_origin_frame_by_header; + return doTest(frame, 'iframe') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The request should be handled by SW.'); + assert_equals(requests[0], frame.src + '&test=iframe'); + assert_true(data.clients.includes(frame.src + '&test=iframe')); + }); +}, 'Request for an iframe in the iframe sandboxed by CSP HTTP header with ' + + 'allow-scripts and allow-same-origin flag'); + +promise_test(t => { + let frame = sandboxed_same_origin_frame_by_header; + return doTest(frame, 'sandboxed-iframe') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + assert_false( + data.clients.includes(frame.src + '&test=sandboxed-iframe')); + }); +}, 'Request for an sandboxed iframe with allow-scripts flag in the ' + + 'iframe sandboxed by CSP HTTP header with allow-scripts and ' + + 'allow-same-origin flag'); + +promise_test(t => { + let frame = sandboxed_same_origin_frame_by_header; + return doTest(frame, 'sandboxed-iframe-same-origin') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The request should be handled by SW.'); + assert_equals(requests[0], + frame.src + '&test=sandboxed-iframe-same-origin'); + assert_true(data.clients.includes( + frame.src + '&test=sandboxed-iframe-same-origin')); + }); +}, 'Request for an sandboxed iframe with allow-scripts and ' + + 'allow-same-origin flag in the iframe sandboxed by CSP HTTP header with ' + + 'allow-scripts and allow-same-origin flag'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html b/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html new file mode 100644 index 0000000000..70be6ef9b0 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html @@ -0,0 +1,120 @@ +<!DOCTYPE html> +<title>Accessing navigator.serviceWorker in sandboxed iframe.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +var lastCallbackId = 0; +var callbacks = {}; +function postMessageAndWaitResult(frame) { + return new Promise(function(resolve, reject) { + var id = ++lastCallbackId; + callbacks[id] = resolve; + frame.contentWindow.postMessage({id:id}, '*'); + const timeout = 1000; + step_timeout(() => reject("no msg back after " + timeout + "ms"), timeout); + }); +} + +window.onmessage = function(e) { + message = e.data; + var id = message['id']; + var callback = callbacks[id]; + delete callbacks[id]; + callback(message.result); +}; + +promise_test(function(t) { + var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html'; + var frame; + return with_iframe(url) + .then(function(f) { + frame = f; + add_result_callback(() => { frame.remove(); }); + return postMessageAndWaitResult(f); + }) + .then(function(result) { + assert_equals(result, 'ok'); + }); + }, 'Accessing navigator.serviceWorker in normal iframe should not throw.'); + +promise_test(function(t) { + var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html'; + var frame; + return with_sandboxed_iframe(url, 'allow-scripts') + .then(function(f) { + frame = f; + add_result_callback(() => { frame.remove(); }); + return postMessageAndWaitResult(f); + }) + .then(function(result) { + assert_equals( + result, + 'navigator.serviceWorker failed: SecurityError'); + }); + }, 'Accessing navigator.serviceWorker in sandboxed iframe should throw.'); + +promise_test(function(t) { + var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html'; + var frame; + return with_sandboxed_iframe(url, 'allow-scripts allow-same-origin') + .then(function(f) { + frame = f; + add_result_callback(() => { frame.remove(); }); + return postMessageAndWaitResult(f); + }) + .then(function(result) { + assert_equals(result, 'ok'); + }); + }, + 'Accessing navigator.serviceWorker in sandboxed iframe with ' + + 'allow-same-origin flag should not throw.'); + +promise_test(function(t) { + var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html'; + var frame; + return new Promise(function(resolve) { + frame = document.createElement('iframe'); + add_result_callback(() => { frame.remove(); }); + frame.sandbox = ''; + frame.src = url; + frame.onload = resolve; + document.body.appendChild(frame); + // Switch the sandbox attribute while loading the iframe. + frame.sandbox = 'allow-scripts allow-same-origin'; + }) + .then(function() { + return postMessageAndWaitResult(frame) + }) + .then(function(result) { + // The HTML spec seems to say that changing the sandbox attribute + // after the iframe is inserted into its parent document does not + // affect the sandboxing. If that's true, the frame should still + // act as if it still doesn't have + // 'allow-scripts allow-same-origin' set and throw a SecurityError. + // + // 1) From Section 4.8.5 "The iframe element": + // "When an iframe element is inserted into a document that has a + // browsing context, the user agent must create a new browsing + // context..." + // 2) "Create a new browsing context" expands to Section 7.1 + // "Browsing contexts", which includes creating a Document and + // "Implement the sandboxing for document." + // 3) "Implement the sandboxing" expands to Section 7.6 "Sandboxing", + // which includes "populate document's active sandboxing flag set". + // + // It's not clear whether navigation subsequently creates a new + // Document, but I'm assuming it wouldn't. + // https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-sandbox + assert_true( + false, + 'should NOT get message back from a sandboxed frame where scripts are not allowed to execute'); + }) + .catch(msg => { + assert_true(msg.startsWith('no msg back'), 'expecting error message "no msg back"'); + }); + }, 'Switching iframe sandbox attribute while loading the iframe'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/secure-context.https.html b/testing/web-platform/tests/service-workers/service-worker/secure-context.https.html new file mode 100644 index 0000000000..666a5d3787 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/secure-context.https.html @@ -0,0 +1,57 @@ +<!doctype html> +<meta charset=utf-8> +<title>Ensure service worker is bypassed in insecure contexts</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +// This test checks that an HTTPS iframe embedded in an HTTP document is not +// loaded via a service worker, since it's not a secure context. To that end, we +// first register a service worker, wait for its activation, and create an +// iframe that is controlled by said service worker. We use the iframe as a +// way to receive messages from the service worker. +// The bulk of the test begins by opening an HTTP window with the noopener +// option, installing a message event handler, and embedding an HTTPS iframe. If +// the browser behaves correctly then the iframe will be loaded from the network +// and will contain a script that posts a message to the parent window, +// informing it that it was loaded from the network. If, however, the iframe is +// intercepted, the service worker will return a page with a script that posts a +// message to the parent window, informing it that it was intercepted. +// Upon getting either result, the window will report the result to the service +// worker by navigating to a reporting URL. The service worker will then inform +// all clients about the result, including the controlled iframe from the +// beginning of the test. The message event handler will verify that the result +// is as expected, concluding the test. +promise_test(t => { + const SCRIPT = "resources/secure-context-service-worker.js"; + const SCOPE = "resources/"; + const HTTP_IFRAME_URL = get_host_info().HTTP_ORIGIN + base_path() + SCOPE + "secure-context/window.html"; + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(registration => { + t.add_cleanup(() => { + return registration.unregister(); + }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { + return with_iframe(SCOPE + "blank.html"); + }) + .then(iframe => { + t.add_cleanup(() => { + iframe.remove(); + }); + return new Promise(resolve => { + iframe.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => { + assert_equals(event.data, 'network'); + resolve(); + }); + window.open(HTTP_IFRAME_URL, 'MyWindow', 'noopener'); + }); + }); +}) + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-connect.https.html b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-connect.https.html new file mode 100644 index 0000000000..226f4a40e4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-connect.https.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<title>Service Worker: CSP connect directive for ServiceWorker script</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> +service_worker_test( + 'resources/service-worker-csp-worker.py?directive=connect', + 'CSP test for connect-src in ServiceWorkerGlobalScope'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-default.https.html b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-default.https.html new file mode 100644 index 0000000000..1d4e7624d8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-default.https.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<title>Service Worker: CSP default directive for ServiceWorker script</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> +service_worker_test( + 'resources/service-worker-csp-worker.py?directive=default', + 'CSP test for default-src in ServiceWorkerGlobalScope'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-script.https.html b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-script.https.html new file mode 100644 index 0000000000..14c2eb72bd --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-script.https.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<title>Service Worker: CSP script directive for ServiceWorker script</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> +service_worker_test( + 'resources/service-worker-csp-worker.py?directive=script', + 'CSP test for script-src in ServiceWorkerGlobalScope'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/service-worker-header.https.html b/testing/web-platform/tests/service-workers/service-worker/service-worker-header.https.html new file mode 100644 index 0000000000..fb902cd1b4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/service-worker-header.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<title>Service Worker: Service-Worker header</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +promise_test(async t => { + const script = 'resources/service-worker-header.py' + + '?header&import=service-worker-header.py?no-header'; + const scope = 'resources/service-worker-header'; + const expected_url = normalizeURL(script); + const registration = + await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => registration.unregister()); + assert_true(registration instanceof ServiceWorkerRegistration); + + await wait_for_state(t, registration.installing, 'activated'); + await registration.update(); +}, 'A request to fetch service worker main script should have Service-Worker ' + + 'header and imported scripts should not have one'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html b/testing/web-platform/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html new file mode 100644 index 0000000000..fac8f2076f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<title>Service Worker: ServiceWorkerMessageEvent</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +promise_test(function(t) { + var scope = 'resources/blank.html'; + var url = 'resources/postmessage-to-client-worker.js'; + return service_worker_unregister_and_register(t, url, scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, r.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(frame) { + var w = frame.contentWindow; + var worker = w.navigator.serviceWorker.controller; + assert_equals( + self.ServiceWorkerMessageEvent, undefined, + 'ServiceWorkerMessageEvent should not be defined.'); + return new Promise(function(resolve) { + w.navigator.serviceWorker.onmessage = t.step_func(function(e) { + assert_true( + e instanceof w.MessageEvent, + 'message events should use MessageEvent interface.'); + assert_true(e.source instanceof w.ServiceWorker); + assert_equals(e.type, 'message'); + assert_equals(e.source, worker, + 'source should equal to the controller.'); + assert_equals(e.ports.length, 0); + resolve(); + }); + worker.postMessage('PING'); + }); + }); + }, 'Test MessageEvent supplants ServiceWorkerMessageEvent.'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html b/testing/web-platform/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html new file mode 100644 index 0000000000..6004985a34 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<title>ServiceWorker object: scriptURL property</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +function url_test(name, url) { + const scope = 'resources/scope/' + name; + const expectedURL = normalizeURL(url); + + promise_test(async t => { + const registration = + await service_worker_unregister_and_register(t, url, scope); + const worker = registration.installing; + assert_equals(worker.scriptURL, expectedURL, 'scriptURL'); + await registration.unregister(); + }, 'Verify the scriptURL property: ' + name); +} + +url_test('relative', 'resources/empty-worker.js'); +url_test('with-fragment', 'resources/empty-worker.js#ref'); +url_test('with-query', 'resources/empty-worker.js?ref'); +url_test('absolute', normalizeURL('./resources/empty-worker.js')); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/skip-waiting-installed.https.html b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-installed.https.html new file mode 100644 index 0000000000..b604f651b3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-installed.https.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<title>Service Worker: Skip waiting installed worker</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +promise_test(function(t) { + var scope = 'resources/blank.html?skip-waiting-installed'; + var url1 = 'resources/empty.js'; + var url2 = 'resources/skip-waiting-installed-worker.js'; + var frame, frame_sw, service_worker, registration, onmessage, oncontrollerchanged; + var saw_message = new Promise(function(resolve) { + onmessage = function(e) { + resolve(e.data); + }; + }) + .then(function(message) { + assert_equals( + message, 'PASS', + 'skipWaiting promise should be resolved with undefined'); + }); + var saw_controllerchanged = new Promise(function(resolve) { + oncontrollerchanged = function() { + assert_equals( + frame_sw.controller.scriptURL, normalizeURL(url2), + 'Controller scriptURL should change to the second one'); + assert_equals(registration.active.scriptURL, normalizeURL(url2), + 'Worker which calls skipWaiting should be active by controllerchange'); + resolve(); + }; + }); + return service_worker_unregister_and_register(t, url1, scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, r.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(f) { + frame = f; + frame_sw = f.contentWindow.navigator.serviceWorker; + assert_equals( + frame_sw.controller.scriptURL, normalizeURL(url1), + 'Document controller scriptURL should equal to the first one'); + frame_sw.oncontrollerchange = t.step_func(oncontrollerchanged); + return navigator.serviceWorker.register(url2, {scope: scope}); + }) + .then(function(r) { + registration = r; + service_worker = r.installing; + return wait_for_state(t, service_worker, 'installed'); + }) + .then(function() { + var channel = new MessageChannel(); + channel.port1.onmessage = t.step_func(onmessage); + service_worker.postMessage({port: channel.port2}, [channel.port2]); + return Promise.all([saw_message, saw_controllerchanged]); + }) + .then(function() { + frame.remove(); + }); + }, 'Test skipWaiting when a installed worker is waiting'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/skip-waiting-using-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-using-registration.https.html new file mode 100644 index 0000000000..412ee2a443 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-using-registration.https.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<title>Service Worker: Skip waiting using registration</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +promise_test(function(t) { + var scope = 'resources/blank.html?skip-waiting-using-registration'; + var url1 = 'resources/empty.js'; + var url2 = 'resources/skip-waiting-worker.js'; + var frame, frame_sw, sw_registration, oncontrollerchanged; + var saw_controllerchanged = new Promise(function(resolve) { + oncontrollerchanged = function(e) { + resolve(e); + }; + }) + .then(function(e) { + assert_equals(e.type, 'controllerchange', + 'Event name should be "controllerchange"'); + assert_true( + e.target instanceof frame.contentWindow.ServiceWorkerContainer, + 'Event target should be a ServiceWorkerContainer'); + assert_equals(e.target.controller.state, 'activating', + 'Controller state should be activating'); + assert_equals( + frame_sw.controller.scriptURL, normalizeURL(url2), + 'Controller scriptURL should change to the second one'); + }); + + return service_worker_unregister_and_register(t, url1, scope) + .then(function(registration) { + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(f) { + t.add_cleanup(function() { + f.remove(); + }); + frame = f; + frame_sw = f.contentWindow.navigator.serviceWorker; + assert_equals( + frame_sw.controller.scriptURL, normalizeURL(url1), + 'Document controller scriptURL should equal to the first one'); + frame_sw.oncontrollerchange = t.step_func(oncontrollerchanged); + return navigator.serviceWorker.register(url2, {scope: scope}); + }) + .then(function(registration) { + sw_registration = registration; + t.add_cleanup(function() { + return registration.unregister(); + }); + return saw_controllerchanged; + }) + .then(function() { + assert_not_equals(sw_registration.active, null, + 'Registration active worker should not be null'); + return fetch_tests_from_worker(sw_registration.active); + }); + }, 'Test skipWaiting while a client is using the registration'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-client.https.html b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-client.https.html new file mode 100644 index 0000000000..62060a8247 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-client.https.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<title>Service Worker: Skip waiting without client</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +service_worker_test( + 'resources/skip-waiting-worker.js', + 'Test single skipWaiting() when no client attached'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html new file mode 100644 index 0000000000..ced64e5f67 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<title>Service Worker: Skip waiting without using registration</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +promise_test(function(t) { + var scope = 'resources/blank.html?skip-waiting-without-using-registration'; + var url = 'resources/skip-waiting-worker.js'; + var frame_sw, sw_registration; + + return service_worker_unregister(t, scope) + .then(function() { + return with_iframe(scope); + }) + .then(function(f) { + t.add_cleanup(function() { + f.remove(); + }); + frame_sw = f.contentWindow.navigator.serviceWorker; + assert_equals(frame_sw.controller, null, + 'Document controller should be null'); + return navigator.serviceWorker.register(url, {scope: scope}); + }) + .then(function(registration) { + sw_registration = registration; + t.add_cleanup(function() { + return registration.unregister(); + }); + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + assert_equals(frame_sw.controller, null, + 'Document controller should still be null'); + assert_not_equals(sw_registration.active, null, + 'Registration active worker should not be null'); + return fetch_tests_from_worker(sw_registration.active); + }); + }, 'Test skipWaiting while a client is not being controlled'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/skip-waiting.https.html b/testing/web-platform/tests/service-workers/service-worker/skip-waiting.https.html new file mode 100644 index 0000000000..f8392fc955 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/skip-waiting.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<title>Service Worker: Skip waiting</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +promise_test(function(t) { + var scope = 'resources/blank.html?skip-waiting'; + var url1 = 'resources/empty.js'; + var url2 = 'resources/empty-worker.js'; + var url3 = 'resources/skip-waiting-worker.js'; + var sw_registration, activated_worker, waiting_worker; + return service_worker_unregister_and_register(t, url1, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + sw_registration = registration; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(f) { + t.add_cleanup(function() { + f.remove(); + }); + return navigator.serviceWorker.register(url2, {scope: scope}); + }) + .then(function(registration) { + return wait_for_state(t, registration.installing, 'installed'); + }) + .then(function() { + activated_worker = sw_registration.active; + waiting_worker = sw_registration.waiting; + assert_equals(activated_worker.scriptURL, normalizeURL(url1), + 'Worker with url1 should be activated'); + assert_equals(waiting_worker.scriptURL, normalizeURL(url2), + 'Worker with url2 should be waiting'); + return navigator.serviceWorker.register(url3, {scope: scope}); + }) + .then(function(registration) { + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + assert_equals(activated_worker.state, 'redundant', + 'Worker with url1 should be redundant'); + assert_equals(waiting_worker.state, 'redundant', + 'Worker with url2 should be redundant'); + assert_equals(sw_registration.active.scriptURL, normalizeURL(url3), + 'Worker with url3 should be activated'); + }); + }, 'Test skipWaiting with both active and waiting workers'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/state.https.html b/testing/web-platform/tests/service-workers/service-worker/state.https.html new file mode 100644 index 0000000000..7358e58ff1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/state.https.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +promise_test(function (t) { + var currentState = 'test-is-starting'; + var scope = 'resources/state/'; + + return service_worker_unregister_and_register( + t, 'resources/empty-worker.js', scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + var sw = registration.installing; + + assert_equals(sw.state, 'installing', + 'the service worker should be in "installing" state.'); + checkStateTransition(sw.state); + return onStateChange(sw); + }); + + function checkStateTransition(newState) { + switch (currentState) { + case 'test-is-starting': + break; // anything goes + case 'installing': + assert_in_array(newState, ['installed', 'redundant']); + break; + case 'installed': + assert_in_array(newState, ['activating', 'redundant']); + break; + case 'activating': + assert_in_array(newState, ['activated', 'redundant']); + break; + case 'activated': + assert_equals(newState, 'redundant'); + break; + case 'redundant': + assert_unreached('a ServiceWorker should not transition out of ' + + 'the "redundant" state'); + break; + default: + assert_unreached('should not transition into unknown state "' + + newState + '"'); + break; + } + currentState = newState; + } + + function onStateChange(expectedTarget) { + return new Promise(function(resolve) { + expectedTarget.addEventListener('statechange', resolve); + }).then(function(event) { + assert_true(event.target instanceof ServiceWorker, + 'the target of the statechange event should be a ' + + 'ServiceWorker.'); + assert_equals(event.target, expectedTarget, + 'the target of the statechange event should be ' + + 'the installing ServiceWorker'); + assert_equals(event.type, 'statechange', + 'the type of the event should be "statechange".'); + + checkStateTransition(event.target.state); + + if (event.target.state != 'activated') + return onStateChange(expectedTarget); + }); + } +}, 'Service Worker state property and "statechange" event'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/svg-target-reftest.https.html b/testing/web-platform/tests/service-workers/service-worker/svg-target-reftest.https.html new file mode 100644 index 0000000000..3710ee61d8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/svg-target-reftest.https.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="utf-8"> +<title>Service worker interception does not break SVG fragment targets</title> +<meta name="assert" content="SVG with link fragment should render correctly when intercepted by a service worker."> +<script src="resources/test-helpers.sub.js"></script> +<link rel="match" href="resources/svg-target-reftest-001.html"> +<p>Pass if you see a green box below.</p> +<script> +// We want to use utility functions designed for testharness.js where +// there is a test object. We don't have a test object in reftests +// so fake one for now. +const fake_test = { step_func: f => f }; + +async function runTest() { + const script = './resources/pass-through-worker.js'; + const scope = './resources/svg-target-reftest-frame.html'; + let reg = await navigator.serviceWorker.register(script, { scope }); + await wait_for_state(fake_test, reg.installing, 'activated'); + let f = await with_iframe(scope); + document.documentElement.classList.remove('reftest-wait'); + await reg.unregister(); + // Note, we cannot remove the frame explicitly because we can't + // tell when the reftest completes. +} +runTest(); +</script> +</html> diff --git a/testing/web-platform/tests/service-workers/service-worker/synced-state.https.html b/testing/web-platform/tests/service-workers/service-worker/synced-state.https.html new file mode 100644 index 0000000000..0e9f63a9a2 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/synced-state.https.html @@ -0,0 +1,93 @@ +<!doctype html> +<title>ServiceWorker: worker objects have synced state</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +// Tests that ServiceWorker objects representing the same Service Worker +// entity have the same state. JS-level equality is now required according to +// the spec. +'use strict'; + +function nextChange(worker) { + return new Promise(function(resolve, reject) { + worker.addEventListener('statechange', function handler(event) { + try { + worker.removeEventListener('statechange', handler); + resolve(event.currentTarget.state); + } catch (err) { + reject(err); + } + }); + }); +} + +promise_test(function(t) { + var scope = 'resources/synced-state'; + var script = 'resources/empty-worker.js'; + var registration, worker; + + return service_worker_unregister_and_register(t, script, scope) + .then(function(r) { + registration = r; + worker = registration.installing; + + t.add_cleanup(function() { + return r.unregister(); + }); + + return nextChange(worker); + }) + .then(function(state) { + assert_equals(state, 'installed', + 'original SW should be installed'); + assert_equals(registration.installing, null, + 'in installed, .installing should be null'); + assert_equals(registration.waiting, worker, + 'in installed, .waiting should be equal to the ' + + 'original worker'); + assert_equals(registration.waiting.state, 'installed', + 'in installed, .waiting should be installed'); + assert_equals(registration.active, null, + 'in installed, .active should be null'); + + return nextChange(worker); + }) + .then(function(state) { + assert_equals(state, 'activating', + 'original SW should be activating'); + assert_equals(registration.installing, null, + 'in activating, .installing should be null'); + assert_equals(registration.waiting, null, + 'in activating, .waiting should be null'); + assert_equals(registration.active, worker, + 'in activating, .active should be equal to the ' + + 'original worker'); + assert_equals( + registration.active.state, 'activating', + 'in activating, .active should be activating'); + + return nextChange(worker); + }) + .then(function(state) { + assert_equals(state, 'activated', + 'original SW should be activated'); + assert_equals(registration.installing, null, + 'in activated, .installing should be null'); + assert_equals(registration.waiting, null, + 'in activated, .waiting should be null'); + assert_equals(registration.active, worker, + 'in activated, .active should be equal to the ' + + 'original worker'); + assert_equals(registration.active.state, 'activated', + 'in activated .active should be activated'); + }) + .then(function() { + return navigator.serviceWorker.getRegistration(scope); + }) + .then(function(r) { + assert_equals(r, registration, 'getRegistration should return the ' + + 'same object'); + }); + }, 'worker objects for the same entity have the same state'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/README.md b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/README.md new file mode 100644 index 0000000000..5429b61d40 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/README.md @@ -0,0 +1,4 @@ +A test suite for the ServiceWorker Static Routing API. + +WICG proposal: https://github.com/WICG/proposals/issues/102 +Specification PR: https://github.com/w3c/ServiceWorker/pull/1686 diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.css b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.css new file mode 100644 index 0000000000..c812213f53 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.css @@ -0,0 +1,3 @@ +body { + width: 100%; +} diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.html new file mode 100644 index 0000000000..e98d70207a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<title>Direct</title> +<body> + Here's a direct html from network. +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.js b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.js new file mode 100644 index 0000000000..ed6f0e5f98 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.js @@ -0,0 +1 @@ +window.router_condition_request_destination_script = true; diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.py b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.py new file mode 100644 index 0000000000..d30d41b44e --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.py @@ -0,0 +1,11 @@ +import os +import time + +def main(request, response): + if 'server_slow' in request.url_parts.query: + time.sleep(0.2) + if 'server_no_content' in request.url_parts.query: + return 204, [(b'Content-Type', b'text/plain')], u'Network with %s request' % request.method + if 'server_not_found' in request.url_parts.query: + return 404, [(b'Content-Type', b'text/plain')], u'Not Found' + return 200, [(b'Content-Type', b'text/plain')], u'Network with %s request' % request.method diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt new file mode 100644 index 0000000000..f3d9861c13 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt @@ -0,0 +1 @@ +Network diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/imported-sw.js b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/imported-sw.js new file mode 100644 index 0000000000..04a894d77f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/imported-sw.js @@ -0,0 +1,13 @@ +'use strict'; + +import {routerRules} from './router-rules.js'; + +const params = new URLSearchParams(location.search); +const key = params.get('imported-sw-router-key'); + +if (key) { + self.addEventListener('install', async e => { + await e.addRoutes(routerRules[key]); + self.skipWaiting(); + }); +} diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct1.text b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct1.text new file mode 100644 index 0000000000..f3d9861c13 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct1.text @@ -0,0 +1 @@ +Network diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct1.text.headers b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct1.text.headers new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct1.text.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct2.text b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct2.text new file mode 100644 index 0000000000..f3d9861c13 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct2.text @@ -0,0 +1 @@ +Network diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct2.text.headers b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct2.text.headers new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct2.text.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/router-rules.js b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/router-rules.js new file mode 100644 index 0000000000..4e6f8bb955 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/router-rules.js @@ -0,0 +1,76 @@ +const routerRules = { + 'condition-urlpattern-constructed-source-network': [{ + condition: {urlPattern: new URLPattern({pathname: '/**/direct.txt'})}, + source: 'network' + }], + 'condition-urlpattern-urlpatterninit-source-network': [ + {condition: {urlPattern: {pathname: '/**/direct.txt'}}, source: 'network'}, + ], + 'condition-urlpattern-string-source-network': [ + {condition: {urlPattern: '/**/direct.txt'}, source: 'network'}, + ], + 'condition-urlpattern-string-source-cache': [ + {condition: {urlPattern: '/**/cache.txt'}, source: 'cache'}, + ], + 'condition-urlpattern-constructed-ignore-case-source-network': [{ + condition: { + urlPattern: + new URLPattern({pathname: '/**/DiReCT.TxT'}, {ignoreCase: true}) + }, + source: 'network' + }], + 'condition-urlpattern-constructed-respect-case-source-network': [{ + condition: {urlPattern: new URLPattern({pathname: '/**/DiReCT.TxT'})}, + source: 'network' + }], + 'condition-request-source-network': + [{condition: {requestMode: 'no-cors'}, source: 'network'}], + 'condition-request-navigate-source-cache': + [{condition: {requestMode: 'navigate'}, source: 'cache'}], + 'condition-request-method-get-network': + [{condition: {requestMethod: 'GET'}, source: 'network'}], + 'condition-request-method-post-network': + [{condition: {requestMethod: 'POST'}, source: 'network'}], + 'condition-request-method-put-network': + [{condition: {requestMethod: 'PUT'}, source: 'network'}], + 'condition-request-method-delete-network': + [{condition: {requestMethod: 'DELETE'}, source: 'network'}], + 'condition-invalid-request-method': [{ + condition: {requestMethod: String.fromCodePoint(0x3042)}, + source: 'network' + }], + 'condition-request-destination-script-network': + [{condition: {requestDestination: 'script'}, source: 'network'}], + 'condition-or-source-network': [{ + condition: { + or: [ + { + or: [{urlPattern: '/**/or-test/direct1.*??*'}], + }, + {urlPattern: '/**/or-test/direct2.*??*'} + ] + }, + source: 'network' + }], + 'condition-request-source-fetch-event': + [{condition: {requestMode: 'no-cors'}, source: 'fetch-event'}], + 'condition-urlpattern-string-source-fetch-event': + [{condition: {urlPattern: '/**/*'}, source: 'fetch-event'}], + 'multiple-router-rules': [ + { + condition: { + urlPattern: '/**/direct.txt', + }, + source: 'network' + }, + {condition: {urlPattern: '/**/direct.html'}, source: 'network'} + ], + 'condition-urlpattern-string-source-race-network-and-fetch-handler': [ + { + condition: {urlPattern: '/**/direct.py'}, + source: 'race-network-and-fetch-handler' + }, + ], +}; + +export {routerRules}; diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.html new file mode 100644 index 0000000000..0c3e3e7870 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.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/tentative/static-router/resources/simple.csv b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/simple.csv new file mode 100644 index 0000000000..ecbef786e7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/simple.csv @@ -0,0 +1 @@ +matched,with,non-url,conditions diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/simple.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/simple.html new file mode 100644 index 0000000000..0c3e3e7870 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/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/tentative/static-router/resources/static-router-helpers.sub.js b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-helpers.sub.js new file mode 100644 index 0000000000..cf34e98635 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-helpers.sub.js @@ -0,0 +1,76 @@ +// Helper functions for ServiceWorker static routing API. +// +// test-helpers.sub.js must be loaded before using this. + +// Get a dictionary of information recorded inside ServiceWorker. +// It includes: +// - request URL and mode. +// - errors. +// +// See: static-router-sw.js for details. +const get_info_from_worker = + async worker => { + const promise = new Promise(function(resolve) { + var channel = new MessageChannel(); + channel.port1.onmessage = function(msg) { resolve(msg); }; + worker.postMessage({port: channel.port2}, [channel.port2]); + }); + const message = await promise; + + return message.data; +} + +// Reset information stored inside ServiceWorker. +const reset_info_in_worker = + async worker => { + const promise = new Promise(function(resolve) { + var channel = new MessageChannel(); + channel.port1.onmessage = function(msg) { resolve(msg); }; + worker.postMessage({port: channel.port2, reset: true}, [channel.port2]); + }); + await promise; +} + +// Register the ServiceWorker and wait until activated. +// {ruleKey} represents the key of routerRules defined in router-rules.js. +// {swScript} represents the service worker source URL. +const registerAndActivate = async (test, ruleKey, swScript) => { + if (!swScript) { + swScript = 'resources/static-router-sw.js' + } + const swURL = `${swScript}?key=${ruleKey}`; + const swScope = 'resources/'; + const reg = await service_worker_unregister_and_register( + test, swURL, swScope, { type: 'module' }); + add_completion_callback(() => reg.unregister()); + const worker = reg.installing; + await wait_for_state(test, worker, 'activated'); + + return worker; +}; + +// Create iframe with the given url. This automatically removes the iframe in a +// cleanup. +const createIframe = async (t, url) => { + const iframe = await with_iframe(url); + t.add_cleanup(() => iframe.remove()); + + return iframe; +}; + +// Register a service worker, then create an iframe at url. +function iframeTest(url, ruleKey, callback, name) { + return promise_test(async t => { + const worker = await registerAndActivate(t, ruleKey); + const iframe = await createIframe(t, url); + await callback(t, iframe.contentWindow, worker); + }, name); +}; + +function randomString() { + let result = ""; + for (let i = 0; i < 5; i++) { + result += String.fromCharCode(97 + Math.floor(Math.random() * 26)); + } + return result; +} diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-no-fetch-handler-sw.js b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-no-fetch-handler-sw.js new file mode 100644 index 0000000000..1ba5fd7d46 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-no-fetch-handler-sw.js @@ -0,0 +1,35 @@ +'use strict'; + +import {routerRules} from './router-rules.js'; +import { + recordError, + getRecords, + resetRecords } from './static-router-sw.sub.js'; + +self.addEventListener('install', async e => { + e.waitUntil(caches.open('v1').then( + cache => {cache.put('cache.txt', new Response('From cache'))})); + + const params = new URLSearchParams(location.search); + const key = params.get('key'); + try { + await e.addRoutes(routerRules[key]); + } catch (e) { + recordError(e); + } + self.skipWaiting(); +}); + +self.addEventListener('activate', e => { + e.waitUntil(clients.claim()); +}); + +self.addEventListener('message', function(event) { + if (event.data.reset) { + resetRecords(); + } + if (event.data.port) { + const {requests, errors} = getRecords(); + event.data.port.postMessage({requests, errors}); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-race-network-and-fetch-handler-sw.js b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-race-network-and-fetch-handler-sw.js new file mode 100644 index 0000000000..468116eea8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-race-network-and-fetch-handler-sw.js @@ -0,0 +1,57 @@ +'use strict'; + +import {routerRules} from './router-rules.js'; +import { + recordRequest, + recordError, + getRecords, + resetRecords } from './static-router-sw.sub.js'; + +import './imported-sw.js'; + +self.addEventListener('install', async e => { + e.waitUntil(caches.open('v1').then( + cache => {cache.put('cache.txt', new Response('From cache'))})); + + const params = new URLSearchParams(location.search); + const key = params.get('key'); + try { + await e.addRoutes(routerRules[key]); + } catch (e) { + recordError(e); + } + self.skipWaiting(); +}); + +self.addEventListener('activate', e => { + e.waitUntil(clients.claim()); +}); + +self.addEventListener('fetch', function(event) { + recordRequest(event.request); + const url = new URL(event.request.url); + + + // Force slow response + if (url.searchParams.has('sw_slow')) { + const start = Date.now(); + while (true) { + if (Date.now() - start > 200) { + break; + } + } + } + + const nonce = url.searchParams.get('nonce'); + event.respondWith(new Response(nonce)); +}); + +self.addEventListener('message', function(event) { + if (event.data.reset) { + resetRecords(); + } + if (event.data.port) { + const {requests, errors} = getRecords(); + event.data.port.postMessage({requests, errors}); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js new file mode 100644 index 0000000000..07409ec42c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js @@ -0,0 +1,45 @@ +'use strict'; + +import {routerRules} from './router-rules.js'; +import { + recordRequest, + recordError, + getRecords, + resetRecords } from './static-router-sw.sub.js'; + +import './imported-sw.js'; + +self.addEventListener('install', async e => { + e.waitUntil(caches.open('v1').then( + cache => {cache.put('cache.txt', new Response('From cache'))})); + + const params = new URLSearchParams(location.search); + const key = params.get('key'); + try { + await e.addRoutes(routerRules[key]); + } catch (e) { + recordError(e); + } + self.skipWaiting(); +}); + +self.addEventListener('activate', e => { + e.waitUntil(clients.claim()); +}); + +self.addEventListener('fetch', function(event) { + recordRequest(event.request); + const url = new URL(event.request.url); + const nonce = url.searchParams.get('nonce'); + event.respondWith(new Response(nonce)); +}); + +self.addEventListener('message', function(event) { + if (event.data.reset) { + resetRecords(); + } + if (event.data.port) { + const {requests, errors} = getRecords(); + event.data.port.postMessage({requests, errors}); + } +}); diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.sub.js b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.sub.js new file mode 100644 index 0000000000..04f9c5533a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.sub.js @@ -0,0 +1,29 @@ +let requests = []; +let errors = []; + +const recordRequest = req => { + requests.push({url: req.url, mode: req.mode, destination: req.destination}); +}; + +const recordError = (error) => { + errors.push(error); +}; + +const getRecords = () => { + return { + requests, + errors + }; +} + +const resetRecords = () => { + requests = []; + errors = []; +} + +export { + recordRequest, + recordError, + getRecords, + resetRecords +}; diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js new file mode 100644 index 0000000000..64a7f7d24f --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js @@ -0,0 +1,303 @@ +// Copied from +// service-workers/service-worker/resources/testharness-helpers.js to be used under tentative. + +// Adapter for testharness.js-style tests with Service Workers + +/** + * @param options an object that represents RegistrationOptions except for scope. + * @param options.type a WorkerType. + * @param options.updateViaCache a ServiceWorkerUpdateViaCache. + * @see https://w3c.github.io/ServiceWorker/#dictdef-registrationoptions + */ +function service_worker_unregister_and_register(test, url, scope, options) { + if (!scope || scope.length == 0) + return Promise.reject(new Error('tests must define a scope')); + + if (options && options.scope) + return Promise.reject(new Error('scope must not be passed in options')); + + options = Object.assign({ scope: scope }, options); + return service_worker_unregister(test, scope) + .then(function() { + return navigator.serviceWorker.register(url, options); + }) + .catch(unreached_rejection(test, + 'unregister and register should not fail')); +} + +// This unregisters the registration that precisely matches scope. Use this +// when unregistering by scope. If no registration is found, it just resolves. +function service_worker_unregister(test, scope) { + var absoluteScope = (new URL(scope, window.location).href); + return navigator.serviceWorker.getRegistration(scope) + .then(function(registration) { + if (registration && registration.scope === absoluteScope) + return registration.unregister(); + }) + .catch(unreached_rejection(test, 'unregister should not fail')); +} + +function service_worker_unregister_and_done(test, scope) { + return service_worker_unregister(test, scope) + .then(test.done.bind(test)); +} + +function unreached_fulfillment(test, prefix) { + return test.step_func(function(result) { + var error_prefix = prefix || 'unexpected fulfillment'; + assert_unreached(error_prefix + ': ' + result); + }); +} + +// Rejection-specific helper that provides more details +function unreached_rejection(test, prefix) { + return test.step_func(function(error) { + var reason = error.message || error.name || error; + var error_prefix = prefix || 'unexpected rejection'; + assert_unreached(error_prefix + ': ' + reason); + }); +} + +/** + * Adds an iframe to the document and returns a promise that resolves to the + * iframe when it finishes loading. The caller is responsible for removing the + * iframe later if needed. + * + * @param {string} url + * @returns {HTMLIFrameElement} + */ +function with_iframe(url) { + return new Promise(function(resolve) { + var frame = document.createElement('iframe'); + frame.className = 'test-iframe'; + frame.src = url; + frame.onload = function() { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +function normalizeURL(url) { + return new URL(url, self.location).toString().replace(/#.*$/, ''); +} + +function wait_for_update(test, registration) { + if (!registration || registration.unregister == undefined) { + return Promise.reject(new Error( + 'wait_for_update must be passed a ServiceWorkerRegistration')); + } + + return new Promise(test.step_func(function(resolve) { + var handler = test.step_func(function() { + registration.removeEventListener('updatefound', handler); + resolve(registration.installing); + }); + registration.addEventListener('updatefound', handler); + })); +} + +// Return true if |state_a| is more advanced than |state_b|. +function is_state_advanced(state_a, state_b) { + if (state_b === 'installing') { + switch (state_a) { + case 'installed': + case 'activating': + case 'activated': + case 'redundant': + return true; + } + } + + if (state_b === 'installed') { + switch (state_a) { + case 'activating': + case 'activated': + case 'redundant': + return true; + } + } + + if (state_b === 'activating') { + switch (state_a) { + case 'activated': + case 'redundant': + return true; + } + } + + if (state_b === 'activated') { + switch (state_a) { + case 'redundant': + return true; + } + } + return false; +} + +function wait_for_state(test, worker, state) { + if (!worker || worker.state == undefined) { + return Promise.reject(new Error( + 'wait_for_state needs a ServiceWorker object to be passed.')); + } + if (worker.state === state) + return Promise.resolve(state); + + if (is_state_advanced(worker.state, state)) { + return Promise.reject(new Error( + `Waiting for ${state} but the worker is already ${worker.state}.`)); + } + return new Promise(test.step_func(function(resolve, reject) { + worker.addEventListener('statechange', test.step_func(function() { + if (worker.state === state) + resolve(state); + + if (is_state_advanced(worker.state, state)) { + reject(new Error( + `The state of the worker becomes ${worker.state} while waiting` + + `for ${state}.`)); + } + })); + })); +} + +// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url| +// is the service worker script URL. This function: +// - Instantiates a new test with the description specified in |description|. +// The test will succeed if the specified service worker can be successfully +// registered and installed. +// - Creates a new ServiceWorker registration with a scope unique to the current +// document URL. Note that this doesn't allow more than one +// service_worker_test() to be run from the same document. +// - Waits for the new worker to begin installing. +// - Imports tests results from tests running inside the ServiceWorker. +function service_worker_test(url, description) { + // If the document URL is https://example.com/document and the script URL is + // https://example.com/script/worker.js, then the scope would be + // https://example.com/script/scope/document. + var scope = new URL('scope' + window.location.pathname, + new URL(url, window.location)).toString(); + promise_test(function(test) { + return service_worker_unregister_and_register(test, url, scope) + .then(function(registration) { + add_completion_callback(function() { + registration.unregister(); + }); + return wait_for_update(test, registration) + .then(function(worker) { + return fetch_tests_from_worker(worker); + }); + }); + }, description); +} + +function base_path() { + return location.pathname.replace(/\/[^\/]*$/, '/'); +} + +function test_login(test, origin, username, password, cookie) { + return new Promise(function(resolve, reject) { + with_iframe( + origin + base_path() + + 'resources/fetch-access-control-login.html') + .then(test.step_func(function(frame) { + var channel = new MessageChannel(); + channel.port1.onmessage = test.step_func(function() { + frame.remove(); + resolve(); + }); + frame.contentWindow.postMessage( + {username: username, password: password, cookie: cookie}, + origin, [channel.port2]); + })); + }); +} + +function test_websocket(test, frame, url) { + return new Promise(function(resolve, reject) { + var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']); + var openCalled = false; + ws.addEventListener('open', test.step_func(function(e) { + assert_equals(ws.readyState, 1, "The WebSocket should be open"); + openCalled = true; + ws.close(); + }), true); + + ws.addEventListener('close', test.step_func(function(e) { + assert_true(openCalled, "The WebSocket should be closed after being opened"); + resolve(); + }), true); + + ws.addEventListener('error', reject); + }); +} + +function login_https(test) { + var host_info = get_host_info(); + return test_login(test, host_info.HTTPS_REMOTE_ORIGIN, + 'username1s', 'password1s', 'cookie1') + .then(function() { + return test_login(test, host_info.HTTPS_ORIGIN, + 'username2s', 'password2s', 'cookie2'); + }); +} + +function websocket(test, frame) { + return test_websocket(test, frame, get_websocket_url()); +} + +function get_websocket_url() { + return 'wss://{{host}}:{{ports[wss][0]}}/echo'; +} + +// The navigator.serviceWorker.register() method guarantees that the newly +// installing worker is available as registration.installing when its promise +// resolves. However some tests test installation using a <link> element where +// it is possible for the installing worker to have already become the waiting +// or active worker. So this method is used to get the newest worker when these +// tests need access to the ServiceWorker itself. +function get_newest_worker(registration) { + if (registration.installing) + return registration.installing; + if (registration.waiting) + return registration.waiting; + if (registration.active) + return registration.active; +} + +function register_using_link(script, options) { + var scope = options.scope; + var link = document.createElement('link'); + link.setAttribute('rel', 'serviceworker'); + link.setAttribute('href', script); + link.setAttribute('scope', scope); + document.getElementsByTagName('head')[0].appendChild(link); + return new Promise(function(resolve, reject) { + link.onload = resolve; + link.onerror = reject; + }) + .then(() => navigator.serviceWorker.getRegistration(scope)); +} + +function with_sandboxed_iframe(url, sandbox) { + return new Promise(function(resolve) { + var frame = document.createElement('iframe'); + frame.sandbox = sandbox; + frame.src = url; + frame.onload = function() { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +// Registers, waits for activation, then unregisters on a sample scope. +// +// This can be used to wait for a period of time needed to register, +// activate, and then unregister a service worker. When checking that +// certain behavior does *NOT* happen, this is preferable to using an +// arbitrary delay. +async function wait_for_activation_on_sample_scope(t, window_or_workerglobalscope) { + const script = '/service-workers/service-worker/resources/empty-worker.js'; + const scope = 'resources/there/is/no/there/there?' + Date.now(); + let registration = await window_or_workerglobalscope.navigator.serviceWorker.register(script, { scope }); + await wait_for_state(t, registration.installing, 'activated'); + await registration.unregister(); +} + diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-fetch-event.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-fetch-event.https.html new file mode 100644 index 0000000000..167e3af009 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-fetch-event.https.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title> + Static Router: routers are evaluated with the fetch-event source. +</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"> +</script> +<script src="resources/static-router-helpers.sub.js"> +</script> +<body> +<script> +const ROUTER_KEY = 'condition-urlpattern-string-source-fetch-event'; +const REGISTERED_ROUTE = 'resources/simple.html'; + +promise_test(async t => { + const worker = await registerAndActivate(t, ROUTER_KEY); + const rnd = randomString(); + const iframe = await createIframe(t, `${REGISTERED_ROUTE}?nonce=${rnd}`); + const {errors, requests} = await get_info_from_worker(worker); + + assert_equals(errors.length, 0); + assert_equals(requests.length, 1); + assert_equals(iframe.contentWindow.document.body.innerText, rnd); +}, 'Main resource matched the rule with fetch-event source'); + +iframeTest(REGISTERED_ROUTE, ROUTER_KEY, async (t, iwin, worker) => { + const rnd = randomString(); + const response = await iwin.fetch(`?nonce=${rnd}`); + assert_equals(response.status, 200); + assert_equals(await response.text(), rnd); + const {requests} = await get_info_from_worker(worker); + // Main resource request + subreosurce request = 2. + assert_equals(requests.length, 2); +}, 'Subresource load matched the rule fetch-event source'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-invalid-rules.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-invalid-rules.https.html new file mode 100644 index 0000000000..4ed4ad4b3c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-invalid-rules.https.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title> + Static Router: registration of invalid rules should raise. +</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"> +</script> +<script src="resources/static-router-helpers.sub.js"> +</script> +<body> +<script> + +const ROUTER_RULE_KEY_INVALID_REQUEST_METHOD = + 'condition-invalid-request-method'; +const ROUTER_RULE_KEY_SOURCE_FETCH_EVENT = + 'condition-request-source-fetch-event'; + +promise_test(async t => { + const worker = await registerAndActivate(t, ROUTER_RULE_KEY_INVALID_REQUEST_METHOD); + t.add_cleanup(() => {reset_info_in_worker(worker)}); + const {errors} = await get_info_from_worker(worker); + assert_equals(errors.length, 1); +}, 'addRoutes should raise for invalid request method.'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html new file mode 100644 index 0000000000..fc93a4f7c9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title> + Static Router: simply skip fetch handler for main resource if pattern matches +</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="resources/static-router-helpers.sub.js"></script> +<body> +<script> +const ROUTER_RULE_KEY = 'condition-urlpattern-constructed-source-network'; +const ROUTER_RULE_KEY_IGNORE_CASE = + 'condition-urlpattern-constructed-ignore-case-source-network'; +const ROUTER_RULE_KEY_RESPECT_CASE = + 'condition-urlpattern-constructed-respect-case-source-network'; +const ROUTER_RULE_KEY_URLPATTERN_CACHE = + 'condition-urlpattern-string-source-cache'; +const ROUTER_RULE_KEY_REQUEST_CACHE = 'condition-request-navigate-source-cache'; +const REGISTERED_ROUTE = 'resources/direct.txt'; +const CACHED_ROUTE = 'resources/cache.txt'; +const NON_REGISTERED_ROUTE = 'resources/simple.html'; +const host_info = get_host_info(); +const path = new URL(".", window.location).pathname; + +iframeTest(REGISTERED_ROUTE, ROUTER_RULE_KEY, async (t, iwin, worker) => { + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 0); + assert_equals(iwin.document.body.innerText, "Network\n"); +}, 'Main resource load matched with the condition'); + +iframeTest(REGISTERED_ROUTE, ROUTER_RULE_KEY_IGNORE_CASE, async (t, iwin, worker) => { + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 0); + assert_equals(iwin.document.body.innerText, "Network\n"); +}, 'Main resource load matched with the ignore case condition'); + +iframeTest(REGISTERED_ROUTE, ROUTER_RULE_KEY_RESPECT_CASE, async (t, iwin, worker) => { + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 1); +}, 'Main resource load matched without the ignore case condition'); + +iframeTest(NON_REGISTERED_ROUTE, ROUTER_RULE_KEY, async (t, iwin, worker) => { + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 1); + assert_equals( + requests[0].url, + `${host_info['HTTPS_ORIGIN']}${path}${NON_REGISTERED_ROUTE}`); + assert_equals(requests[0].mode, 'navigate'); +}, 'Main resource load not matched with the condition'); + +iframeTest(CACHED_ROUTE, ROUTER_RULE_KEY_URLPATTERN_CACHE, async (t, iwin, worker) => { + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 0); + assert_equals(iwin.document.body.innerText, "From cache"); +}, 'Main resource load matched with the cache source'); + +iframeTest(NON_REGISTERED_ROUTE, ROUTER_RULE_KEY_REQUEST_CACHE, async (t, iwin, worker) => { + const {requests} = await get_info_from_worker(worker); + // When the request matched to the rule with the "cache" source but failed to + // get the cache entry, the fetch handler is not involved and the network + // fallback is triggered instead. + assert_equals(requests.length, 0); + assert_equals(iwin.document.body.innerText, "Here's a simple html file."); +}, 'Main resource fallback to the network when there is no cache entry'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-multiple-router-registrations.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-multiple-router-registrations.https.html new file mode 100644 index 0000000000..dd7ff1e4e9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-multiple-router-registrations.https.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title> + Static Router: routers are registered multiple times. +</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"> +</script> +<script src="resources/static-router-helpers.sub.js"> +</script> +<body> +<script> +const ROUTER_KEY_MULTIPLE_RULES = 'multiple-router-rules'; +const ROUTER_KEY_1 = 'condition-urlpattern-string-source-network'; +const ROUTER_KEY_2 = 'condition-request-method-post-network'; + +const REGISTERED_ROUTE_1 = 'resources/direct.txt'; +const REGISTERED_ROUTE_2 = 'resources/direct.html'; +const REGISTERED_ROUTE_3 = 'direct.py'; + +promise_test(async t => { + const worker = await registerAndActivate(t, ROUTER_KEY_MULTIPLE_RULES); + + // Matched with the first rule. + const iframe = await createIframe(t, REGISTERED_ROUTE_1); + assert_equals(iframe.contentWindow.document.body.innerText, "Network\n"); + + // Matched with the second rule. + const second_iframe = await createIframe(t, REGISTERED_ROUTE_2); + assert_equals(second_iframe.contentWindow.document.body.innerText, "Here's a direct html from network."); + + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 0); +}, 'Main reosurce load matched with the service worker having multiple rules'); + +promise_test(async t => { + const worker = await registerAndActivate(t, + `${ROUTER_KEY_1}&imported-sw-router-key=${ROUTER_KEY_2}`); + + // Matched with the first rule. + const iframe = await createIframe(t, REGISTERED_ROUTE_1); + assert_equals(iframe.contentWindow.document.body.innerText, "Network\n"); + let info = await get_info_from_worker(worker); + assert_equals(info.requests.length, 0); + + // Matched with the second rule, which is registered in the service worker + // loaded via `import` by the main script. + const response = await iframe.contentWindow.fetch(REGISTERED_ROUTE_3, {method: 'POST'}); + assert_equals(response.status, 200); + assert_equals(await response.text(), "Network with POST request"); + info = await get_info_from_worker(worker); + assert_equals(info.requests.length, 0); +}, 'Resource load matched with the rule registered in the imported service worker'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-no-fetch-handler.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-no-fetch-handler.https.html new file mode 100644 index 0000000000..e98dd08a90 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-no-fetch-handler.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title> + Static Router: routers are evaluated when there is no fetch handler. +</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"> +</script> +<script src="resources/static-router-helpers.sub.js"> +</script> +<body> +<script> +const RULE_KEY_SOURCE_CACHE = 'condition-urlpattern-string-source-cache'; +const RULE_KEY_SOURCE_FETCH_EVENT = + 'condition-request-source-fetch-event'; +const SW_SRC = 'resources/static-router-no-fetch-handler-sw.js'; +const CACHED_FILE = 'cache.txt'; + +promise_test(async t => { + const worker = await registerAndActivate(t, RULE_KEY_SOURCE_CACHE, SW_SRC); + + // Matched with the main reosurce load. + const {contentWindow} = await createIframe(t, `resources/${CACHED_FILE}`); + assert_equals(contentWindow.document.body.innerText, "From cache"); + + // Matched with the subreosurce load. + const response = await contentWindow.fetch(CACHED_FILE); + assert_equals(response.status, 200); + assert_equals(await response.text(), "From cache"); + + // Both requests are served from cache. + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 0); +}, 'The router rule is evaluated without fetch handlers in service worker'); + +promise_test(async t => { + const worker = + await registerAndActivate(t, RULE_KEY_SOURCE_FETCH_EVENT, SW_SRC); + t.add_cleanup(() => {reset_info_in_worker(worker)}); + + const {errors} = await get_info_from_worker(worker); + assert_equals(errors.length, 1); +}, 'addRoutes should raise if the fetch-event source is used without onfetch') + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-race-network-and-fetch-handler.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-race-network-and-fetch-handler.https.html new file mode 100644 index 0000000000..0dad64ad99 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-race-network-and-fetch-handler.https.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title> + Static Router: routers are evaluated with the request desitnation condition. +</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"> +</script> +<script src="resources/static-router-helpers.sub.js"> +</script> +<body> +<script> +const ROUTER_KEY = 'condition-urlpattern-string-source-race-network-and-fetch-handler'; +const SW_SRC = 'resources/static-router-race-network-and-fetch-handler-sw.js'; +const FRAME_SRC = 'resources/direct.py'; + +promise_test(async t => { + const rnd = randomString(); + const worker = await registerAndActivate(t, ROUTER_KEY, SW_SRC); + const iframe = await createIframe(t, `${FRAME_SRC}?nonce=${rnd}&server_slow`); + // Expect the response from the fetch handler. + assert_equals(iframe.contentWindow.document.body.innerText, rnd); + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 1); +}, 'Main resource load matched the rule with race-network-and-fetch-handler source, and the fetch handler response is faster than the server response'); + +promise_test(async t => { + const rnd = randomString(); + const worker = await registerAndActivate(t, ROUTER_KEY, SW_SRC); + const iframe = await createIframe(t, `${FRAME_SRC}?nonce=${rnd}&sw_slow`); + // Expect the response from the netowrk request. + assert_equals(iframe.contentWindow.document.body.innerText, "Network with GET request"); + // Ensure the fetch handler is also executed. + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 1); +}, 'Main resource load matched the rule with race-network-and-fetch-handler source, and the server reseponse is faster than the fetch handler'); + +promise_test(async t => { + const rnd = randomString(); + const worker = await registerAndActivate(t, ROUTER_KEY, SW_SRC); + const iframe = await createIframe(t, FRAME_SRC); + // Expect the response from the fetch handler. + const response = await iframe.contentWindow.fetch(`?nonce=${rnd}&server_slow`); + assert_equals(response.status, 200); + assert_equals(await response.text(), rnd); + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 2); +}, 'Subresource load matched the rule with race-network-and-fetch-handler source, and the fetch handler response is faster than the server response'); + +promise_test(async t => { + const rnd = randomString(); + const worker = await registerAndActivate(t, ROUTER_KEY, SW_SRC); + const iframe = await createIframe(t, FRAME_SRC); + // Expect the response from the network request. + const response = await iframe.contentWindow.fetch(`?nonce=${rnd}&sw_slow`); + assert_equals(response.status, 200); + assert_equals(await response.text(), "Network with GET request"); + // Ensure the fetch handler is also executed. + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 2); +}, 'Subresource load matched the rule with race-network-and-fetch-handler source, and the server reseponse is faster than the fetch handler'); + +promise_test(async t => { + const rnd = randomString(); + const worker = await registerAndActivate(t, ROUTER_KEY, SW_SRC); + const iframe = await createIframe(t, FRAME_SRC); + // Expect the response from the network request. + const response = await iframe.contentWindow.fetch(`?nonce=${rnd}&sw_slow&server_no_content`); + assert_equals(response.status, 204); + // Ensure the fetch handler is also executed. + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 2); +}, 'Subresource load matched the rule with race-network-and-fetch-handler source, and the server reseponse with 204 response is faster than the fetch handler'); + + +promise_test(async t => { + const rnd = randomString(); + const worker = await registerAndActivate(t, ROUTER_KEY, SW_SRC); + const iframe = await createIframe(t, FRAME_SRC); + const response = await iframe.contentWindow.fetch(`?nonce=${rnd}&sw_slow&server_not_found`); + // Expect the response from the network request was faster, but the result was 404. + // So, the fetch handler result will be used instead. + assert_equals(response.status, 200); + assert_equals(await response.text(), rnd); + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 2); +}, 'Subresource load matched the rule with race-network-and-fetch-handler source, and the server reseponse is faster than the fetch handler, but not found'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-request-destination.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-request-destination.https.html new file mode 100644 index 0000000000..55ad6da31a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-request-destination.https.html @@ -0,0 +1,77 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title> + Static Router: routers are evaluated with the request desitnation condition. +</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"> +</script> +<script src="resources/static-router-helpers.sub.js"> +</script> +<body> +<script> +const ROUTER_KEY_DESTINATION_SCRIPT = 'condition-request-destination-script-network'; +const FRAME_SRC = 'resources/direct.html'; +const SCRIPT_SRC = 'direct.js'; +const STYLE_SRC = 'direct.txt'; + +const appendScript = async (iwin, src) => { + const promise = new Promise(resolve => { + const script = iwin.document.createElement('script'); + script.src = src; + iwin.document.body.appendChild(script); + script.onload = () => { + resolve(script); + }; + }); + + return promise; +}; + +const appendCSS = async (iwin, src) => { + const promise = new Promise(resolve => { + const link = iwin.document.createElement('link'); + link.rel = 'stylesheet'; + link.href = src; + iwin.document.head.appendChild(link); + link.onload = () => { + resolve(link); + }; + }); + + return promise; +}; + +promise_test(async t => { + const worker = await registerAndActivate(t, ROUTER_KEY_DESTINATION_SCRIPT); + const iframe = await createIframe(t, FRAME_SRC); + + let info = await get_info_from_worker(worker); + assert_equals(info.requests.length, 1); + assert_equals(info.requests[0].destination, 'iframe'); + + // JavaScript is not handled by fetch handler. + await appendScript(iframe.contentWindow, SCRIPT_SRC); + assert_equals(iframe.contentWindow.router_condition_request_destination_script, true); + info = await get_info_from_worker(worker); + assert_equals(info.requests.length, 1); +}, 'Subreosurce load matched with the requestMethod script condition'); + +promise_test(async t => { + const worker = await registerAndActivate(t, ROUTER_KEY_DESTINATION_SCRIPT); + const iframe = await createIframe(t, FRAME_SRC); + + let info = await get_info_from_worker(worker); + assert_equals(info.requests.length, 1); + assert_equals(info.requests[0].destination, 'iframe'); + + // Stylesheet is not handled by fetch handler. + await appendCSS(iframe.contentWindow, STYLE_SRC); + info = await get_info_from_worker(worker); + assert_equals(info.requests.length, 2); + assert_equals(info.requests[1].destination, 'style'); +}, 'Subreosurce load not matched with the requestMethod script condition'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-request-method.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-request-method.https.html new file mode 100644 index 0000000000..8e219532a3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-request-method.https.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title> + Static Router: routers are evaluated with the request method condition. +</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"> +</script> +<script src="resources/static-router-helpers.sub.js"> +</script> +<body> +<script> +const ROUTER_KEY_MEHOD_GET = 'condition-request-method-get-network'; +const ROUTER_KEY_MEHOD_POST = 'condition-request-method-post-network'; +const ROUTER_KEY_MEHOD_PUT = 'condition-request-method-put-network'; +const ROUTER_KEY_MEHOD_DELETE = 'condition-request-method-delete-network'; +const REQUET_SRC = 'resources/direct.py'; + +iframeTest(REQUET_SRC, ROUTER_KEY_MEHOD_GET, async (t, iwin, worker) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd); + assert_equals(await response.text(), "Network with GET request"); + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 0); +}, 'Subresource load matched with the requestMethod GET condition'); + +iframeTest(REQUET_SRC, ROUTER_KEY_MEHOD_POST, async (t, iwin, worker) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd, {method: 'POST'}); + assert_equals(await response.text(), "Network with POST request"); + const {requests} = await get_info_from_worker(worker); + // The navigation request is the only request handled by fetch handler. + assert_equals(requests.length, 1); + assert_equals(requests[0].mode, 'navigate'); +}, 'Subresource load matched with the requestMethod POST condition'); + +iframeTest(REQUET_SRC, ROUTER_KEY_MEHOD_PUT, async (t, iwin, worker) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd, {method: 'PUT'}); + assert_equals(await response.text(), "Network with PUT request"); + const {requests} = await get_info_from_worker(worker); + // The navigation request is the only request handled by fetch handler. + assert_equals(requests.length, 1); + assert_equals(requests[0].mode, 'navigate'); +}, 'Subresource load matched with the requestMethod PUT condition'); + +iframeTest(REQUET_SRC, ROUTER_KEY_MEHOD_DELETE, async (t, iwin, worker) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd, {method: 'DELETE'}); + assert_equals(await response.text(), "Network with DELETE request"); + const {requests} = await get_info_from_worker(worker); + // The navigation request is the only request handled by fetch handler. + assert_equals(requests.length, 1); + assert_equals(requests[0].mode, 'navigate'); +}, 'Subresource load matched with the requestMethod DELETE condition'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html new file mode 100644 index 0000000000..3f7902a872 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html @@ -0,0 +1,154 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Static Router: simply skip fetch handler if pattern matches</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/static-router-helpers.sub.js"></script> +<body> +<script> +const SCRIPT = 'resources/static-router-sw.js'; +const ROUTER_RULE_KEY_URL_PATTERN_CONSTRUCTED = + 'condition-urlpattern-constructed-source-network'; +const ROUTER_RULE_KEY_URL_PATTERN_CONSTRUCTED_IGNORE_CASE = + 'condition-urlpattern-constructed-ignore-case-source-network'; +const ROUTER_RULE_KEY_URL_PATTERN_CONSTRUCTED_RESPECT_CASE = + 'condition-urlpattern-constructed-respect-case-source-network'; +const ROUTER_RULE_KEY_URL_PATTERN_URLPATTERNINIT = + 'condition-urlpattern-urlpatterninit-source-network'; +const ROUTER_RULE_KEY_URL_PATTERN_STRING = + 'condition-urlpattern-string-source-network'; +const ROUTER_RULE_KEY_REQUEST = 'condition-request-source-network' +const ROUTER_RULE_KEY_URL_PATTERN_STRING_CACHE = + 'condition-urlpattern-string-source-cache'; +const ROUTER_RULE_KEY_OR = 'condition-or-source-network' +const SCOPE = 'resources/'; +const HTML_FILE = 'resources/simple.html'; +const TXT_FILE = 'resources/direct.txt'; +const CSV_FILE = 'resources/simple.csv'; +// Warning: please prepare the corresponding `*.text.headers` files, otherwise +// iframeTest() fails to load the following files due to MIME mismatches. +const OR_TEST_FILES = [ + 'resources/or-test/direct1.text', + 'resources/or-test/direct2.text', + 'resources/or-test/does-not-exist.text', +]; + +iframeTest(HTML_FILE, ROUTER_RULE_KEY_URL_PATTERN_CONSTRUCTED, async (t, iwin) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd); + assert_equals(await response.text(), rnd); +}, 'Subresource load not matched with URLPattern condition'); + +iframeTest(TXT_FILE, ROUTER_RULE_KEY_URL_PATTERN_CONSTRUCTED, async (t, iwin) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd); + assert_equals(await response.text(), "Network\n"); +}, 'Subresource load matched with URLPattern condition'); + +iframeTest(TXT_FILE, ROUTER_RULE_KEY_URL_PATTERN_CONSTRUCTED, async (t, iwin, worker) => { + const rnd = randomString(); + // Confirm that the given URLPatternInit has a wildcard pattern for the + // hostname. Also, if |urlPattern| is a consutructed URLPattern object, + // baseURL won't be set while adding router rules, thus it matches the cross + // origin request as far as other components matches. So expecting the direct + // network request and the fetch handler doesn't capture the response. + // The response is going to be a opaque. + const origin = get_host_info().HTTPS_REMOTE_ORIGIN; + const response = await iwin.fetch( + `${origin}/${TXT_FILE}?nonce=${rnd}`, {mode: 'no-cors'}); + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 0); + assert_equals(response.type, 'opaque'); +}, 'Subresource cross origin load matched with URLPattern condition via constructed object'); + +iframeTest(TXT_FILE, ROUTER_RULE_KEY_URL_PATTERN_CONSTRUCTED_IGNORE_CASE, async (t, iwin) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd); + assert_equals(await response.text(), "Network\n"); +}, 'Subresource load matched with ignoreCase URLPattern condition'); + +iframeTest(TXT_FILE, ROUTER_RULE_KEY_URL_PATTERN_CONSTRUCTED_RESPECT_CASE, async (t, iwin) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd); + assert_equals(await response.text(), rnd); +}, 'Subresource load matched without ignoreCase URLPattern condition'); + +iframeTest(TXT_FILE, ROUTER_RULE_KEY_URL_PATTERN_URLPATTERNINIT, async (t, iwin) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd); + assert_equals(await response.text(), "Network\n"); +}, 'Subresource load matched with URLPattern condition via URLPatternInit'); + +iframeTest(TXT_FILE, ROUTER_RULE_KEY_URL_PATTERN_URLPATTERNINIT, async (t, iwin, worker) => { + // The SW script URL is added as a baseURL when |urlPattern| is passed via + // URLPatternInit, and there is not |baseURL| in it. Cross origin request will + // go through the fetch handler because |baseURL| info complements hostname + // with the hostname of the SW script. + const rnd = randomString(); + const origin = get_host_info().HTTPS_REMOTE_ORIGIN; + const response = await iwin.fetch(`${origin}/${TXT_FILE}?nonce=${rnd}`); + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 1); + assert_equals(await response.text(), rnd); +}, 'Subresource cross origin load not matched with URLPattern condition via URLPatternInit'); + +iframeTest(TXT_FILE, ROUTER_RULE_KEY_URL_PATTERN_STRING, async (t, iwin) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd); + assert_equals(await response.text(), "Network\n"); +}, 'Subresource load matched with URLPattern condition via string'); + +iframeTest(TXT_FILE, ROUTER_RULE_KEY_URL_PATTERN_STRING, async (t, iwin, worker) => { + // The SW script URL is added as a baseURL when |urlPattern| is passed via + // string, and there is not |baseURL| in it. Cross origin request will go + // through the fetch handler because |baseURL| info complements hostname with + // the hostname of the SW script. + const rnd = randomString(); + const origin = get_host_info().HTTPS_REMOTE_ORIGIN; + const response = await iwin.fetch(`${origin}/${TXT_FILE}?nonce=${rnd}`); + const {requests} = await get_info_from_worker(worker); + assert_equals(requests.length, 1); + assert_equals(await response.text(), rnd); +}, 'Subresource cross origin load not matched with URLPattern condition via string'); + +iframeTest(CSV_FILE, ROUTER_RULE_KEY_REQUEST, async (t, iwin) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd, { mode: 'no-cors' }); + assert_equals(await response.text(), "matched,with,non-url,conditions\n"); +}, 'Subresource load matched with RequestMode condition'); + +iframeTest(OR_TEST_FILES[0], ROUTER_RULE_KEY_OR, async (t, iwin) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd); + assert_equals(await response.text(), "Network\n"); +}, 'Subresource load matched with the nested `or` condition'); + +iframeTest(OR_TEST_FILES[1], ROUTER_RULE_KEY_OR, async (t, iwin) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd); + assert_equals(await response.text(), "Network\n"); +}, 'Subresource load matched with the next `or` condition'); + +iframeTest(OR_TEST_FILES[2], ROUTER_RULE_KEY_OR, async (t, iwin) => { + const rnd = randomString(); + const response = await iwin.fetch('?nonce=' + rnd); + assert_equals(await response.text(), rnd); +}, 'Subresource load not matched with `or` condition'); + +iframeTest(HTML_FILE, ROUTER_RULE_KEY_URL_PATTERN_STRING_CACHE, async (t, iwin) => { + // No need to set `resources/` because the request is dispatched from iframe. + const CACHED_FILE = 'cache.txt'; + const response = await iwin.fetch(CACHED_FILE); + assert_equals(response.status, 200); + assert_equals(await response.text(), "From cache"); + + // This doesn't match because the cache key is wrong. + const rnd = randomString(); + const response_with_param = await iwin.fetch(`${CACHED_FILE}?nonce=${rnd}`); + assert_equals(response_with_param.status, 404); +}, 'Subresource load matched with the cache source rule'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/uncontrolled-page.https.html b/testing/web-platform/tests/service-workers/service-worker/uncontrolled-page.https.html new file mode 100644 index 0000000000..e22ca8f0a9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/uncontrolled-page.https.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<title>Service Worker: Registration</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<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(); + }); +} +var worker = 'resources/fail-on-fetch-worker.js'; + +promise_test(function(t) { + var scope = 'resources/scope/uncontrolled-page/'; + return service_worker_unregister_and_register(t, worker, scope) + .then(function(reg) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, reg.installing, 'activated'); + }) + .then(function() { + return fetch_url('resources/simple.txt'); + }) + .then(function(text) { + assert_equals(text, 'a simple text file\n'); + }); + }, 'Fetch events should not go through uncontrolled page.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-controller.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-controller.https.html new file mode 100644 index 0000000000..3bf4cff720 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/unregister-controller.https.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var worker_url = 'resources/simple-intercept-worker.js'; + +async_test(function(t) { + var scope = + 'resources/unregister-controller-page.html?load-before-unregister'; + var frame_window; + var controller; + var registration; + var frame; + + service_worker_unregister_and_register(t, worker_url, scope) + .then(function(r) { + registration = r; + return wait_for_state(t, r.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(f) { + frame = f; + frame_window = frame.contentWindow; + controller = frame_window.navigator.serviceWorker.controller; + assert_true(controller instanceof frame_window.ServiceWorker, + 'document should load with a controller'); + return registration.unregister(); + }) + .then(function() { + assert_equals(frame_window.navigator.serviceWorker.controller, + controller, + 'unregistration should not modify controller'); + return frame_window.fetch_url('simple.txt'); + }) + .then(function(response) { + assert_equals(response, 'intercepted by service worker', + 'controller should intercept requests'); + frame.remove(); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Unregister does not affect existing controller'); + +async_test(function(t) { + var scope = + 'resources/unregister-controller-page.html?load-after-unregister'; + var registration; + var frame; + + service_worker_unregister_and_register(t, worker_url, scope) + .then(function(r) { + registration = r; + return wait_for_state(t, r.installing, 'activated'); + }) + .then(function() { + return registration.unregister(); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(f) { + frame = f; + var frame_window = frame.contentWindow; + assert_equals(frame_window.navigator.serviceWorker.controller, null, + 'document should not have a controller'); + return frame_window.fetch_url('simple.txt'); + }) + .then(function(response) { + assert_equals(response, 'a simple text file\n', + 'requests should not be intercepted'); + frame.remove(); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Unregister prevents control of subsequent navigations'); + +async_test(function(t) { + var scope = + 'resources/scope/no-new-controllee-even-if-registration-is-still-used'; + var registration; + + service_worker_unregister_and_register(t, worker_url, scope) + .then(function(r) { + registration = r; + return wait_for_state(t, r.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(frame) { + return registration.unregister(); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(frame) { + assert_equals(frame.contentWindow.navigator.serviceWorker.controller, + null, + 'document should not have a controller'); + frame.remove(); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Unregister prevents new controllee even if registration is still in use'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html new file mode 100644 index 0000000000..79cdaf062d --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html @@ -0,0 +1,57 @@ +<!doctype html> +<meta charset=utf-8> +<title>Use Clear-Site-Data to immediately unregister service workers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="resources/unregister-immediately-helpers.js"></script> +<body> +<script> +'use strict'; + +// These tests use the Clear-Site-Data network response header to immediately +// unregister a service worker registration with a worker whose state is +// 'installing' or 'parsed'. Clear-Site-Data must delete the registration, +// abort the installation and then clear the registration by setting the +// worker's state to 'redundant'. + +promise_test(async test => { + // This test keeps the the service worker in the 'parsed' state by using a + // script with an infinite loop. + const script_url = 'resources/onparse-infiniteloop-worker.js'; + const scope_url = + 'resources/scope-for-unregister-immediately-with-parsed-worker'; + + await service_worker_unregister(test, /*scope=*/script_url); + + // Clear-Site-Data must cause register() to fail. + const register_promise = promise_rejects_dom(test, 'AbortError', + navigator.serviceWorker.register(script_url, { scope: scope_url}));; + + await Promise.all([clear_site_data(), register_promise]); + + await assert_no_registrations_exist(); + }, 'Clear-Site-Data must abort service worker registration.'); + +promise_test(async test => { + // This test keeps the the service worker in the 'installing' state by using a + // script with an install event waitUntil() promise that never resolves. + const script_url = 'resources/oninstall-waituntil-forever.js'; + const scope_url = + 'resources/scope-for-unregister-immediately-with-installing-worker'; + + const registration = await service_worker_unregister_and_register( + test, script_url, scope_url); + const service_worker = registration.installing; + + // Clear-Site-Data must cause install to fail. + await Promise.all([ + clear_site_data(), + wait_for_state(test, service_worker, 'redundant')]); + + await assert_no_registrations_exist(); + }, 'Clear-Site-Data must unregister a registration with a worker ' + + 'in the "installing" state.'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html new file mode 100644 index 0000000000..6ba87a7ce8 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html @@ -0,0 +1,50 @@ +<!doctype html> +<meta charset=utf-8> +<title>Use Clear-Site-Data to immediately unregister service workers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="resources/unregister-immediately-helpers.js"></script> +<body> +<script> +'use strict'; + +// These tests use the Clear-Site-Data network response header to immediately +// unregister a service worker registration with a worker that has pending +// extendable events. Clear-Site-Data must delete the registration, +// abort all pending extendable events and then clear the registration by +// setting the worker's state to 'redundant' + +promise_test(async test => { + // Use a service worker script that can produce fetch events with pending + // respondWith() promises that never resolve. + const script_url = 'resources/onfetch-waituntil-forever.js'; + const scope_url = + 'resources/blank.html?unregister-immediately-with-fetch-event'; + + const registration = await service_worker_unregister_and_register( + test, script_url, scope_url); + + await wait_for_state(test, registration.installing, 'activated'); + + const frame = await add_controlled_iframe(test, scope_url); + + // Clear-Site-Data must cause the pending fetch promise to reject. + const fetch_promise = promise_rejects_js( + test, TypeError, frame.contentWindow.fetch('waituntil-forever')); + + const event_watcher = new EventWatcher( + test, frame.contentWindow.navigator.serviceWorker, 'controllerchange'); + + await Promise.all([ + clear_site_data(), + fetch_promise, + event_watcher.wait_for('controllerchange'), + wait_for_state(test, registration.active, 'redundant'),]); + + assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null); + await assert_no_registrations_exist(); +}, 'Clear-Site-Data must fail pending subresource fetch events.'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-immediately.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately.https.html new file mode 100644 index 0000000000..54be40a545 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately.https.html @@ -0,0 +1,134 @@ +<!doctype html> +<meta charset=utf-8> +<title>Use Clear-Site-Data to immediately unregister service workers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="resources/unregister-immediately-helpers.js"></script> +<body> +<script> +'use strict'; + +// These tests use the Clear-Site-Data network response header to immediately +// unregister a service worker registration with a worker whose state is +// 'installed', 'waiting', 'activating' or 'activated'. Immediately +// unregistering runs the "Clear Registration" algorithm without waiting for the +// active worker's controlled clients to unload. + +promise_test(async test => { + // This test keeps the the service worker in the 'activating' state by using a + // script with an activate event waitUntil() promise that never resolves. + const script_url = 'resources/onactivate-waituntil-forever.js'; + const scope_url = + 'resources/scope-for-unregister-immediately-with-waiting-worker'; + + const registration = await service_worker_unregister_and_register( + test, script_url, scope_url); + const service_worker = registration.installing; + + await wait_for_state(test, service_worker, 'activating'); + + // Clear-Site-Data must cause activation to fail. + await Promise.all([ + clear_site_data(), + wait_for_state(test, service_worker, 'redundant')]); + + await assert_no_registrations_exist(); + }, 'Clear-Site-Data must unregister a registration with a worker ' + + 'in the "activating" state.'); + +promise_test(async test => { + // Create an registration with two service workers: one activated and one + // installed. + const script_url = 'resources/update_shell.py'; + const scope_url = + 'resources/scope-for-unregister-immediately-with-with-update'; + + const registration = await service_worker_unregister_and_register( + test, script_url, scope_url); + const first_service_worker = registration.installing; + + await wait_for_state(test, first_service_worker, 'activated'); + registration.update(); + + const event_watcher = new EventWatcher(test, registration, 'updatefound'); + await event_watcher.wait_for('updatefound'); + + const second_service_worker = registration.installing; + await wait_for_state(test, second_service_worker, 'installed'); + + // Clear-Site-Data must clear both workers from the registration. + await Promise.all([ + clear_site_data(), + wait_for_state(test, first_service_worker, 'redundant'), + wait_for_state(test, second_service_worker, 'redundant')]); + + await assert_no_registrations_exist(); +}, 'Clear-Site-Data must unregister an activated registration with ' + + 'an update waiting.'); + +promise_test(async test => { + const script_url = 'resources/empty.js'; + const scope_url = + 'resources/blank.html?unregister-immediately-with-controlled-client'; + + const registration = await service_worker_unregister_and_register( + test, script_url, scope_url); + const service_worker = registration.installing; + + await wait_for_state(test, service_worker, 'activated'); + const frame = await add_controlled_iframe(test, scope_url); + const frame_registration = + await frame.contentWindow.navigator.serviceWorker.ready; + + const event_watcher = new EventWatcher( + test, frame.contentWindow.navigator.serviceWorker, 'controllerchange'); + + // Clear-Site-Data must remove the iframe's controller. + await Promise.all([ + clear_site_data(), + event_watcher.wait_for('controllerchange'), + wait_for_state(test, service_worker, 'redundant')]); + + assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null); + await assert_no_registrations_exist(); + + // The ready promise must continue to resolve with the unregistered + // registration. + assert_equals(frame_registration, + await frame.contentWindow.navigator.serviceWorker.ready); +}, 'Clear-Site-Data must unregister an activated registration with controlled ' + + 'clients.'); + +promise_test(async test => { + const script_url = 'resources/empty.js'; + const scope_url = + 'resources/blank.html?unregister-immediately-while-waiting-to-clear'; + + const registration = await service_worker_unregister_and_register( + test, script_url, scope_url); + const service_worker = registration.installing; + + await wait_for_state(test, service_worker, 'activated'); + const frame = await add_controlled_iframe(test, scope_url); + + const event_watcher = new EventWatcher( + test, frame.contentWindow.navigator.serviceWorker, 'controllerchange'); + + // Unregister waits to clear the registration until no controlled clients + // exist. + await registration.unregister(); + + // Clear-Site-Data must clear the unregistered registration immediately. + await Promise.all([ + clear_site_data(), + event_watcher.wait_for('controllerchange'), + wait_for_state(test, service_worker, 'redundant')]); + + assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null); + await assert_no_registrations_exist(); +}, 'Clear-Site-Data must clear an unregistered registration waiting for ' + + ' controlled clients to unload.'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-then-register-new-script.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-then-register-new-script.https.html new file mode 100644 index 0000000000..d046423e0c --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/unregister-then-register-new-script.https.html @@ -0,0 +1,136 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var worker_url = 'resources/empty-worker.js'; + +promise_test(async function(t) { + const scope = 'resources/scope/unregister-then-register-new-script-that-exists'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + + const newWorkerURL = worker_url + '?new'; + await wait_for_state(t, registration.installing, 'activated'); + + const iframe = await with_iframe(scope); + t.add_cleanup(() => iframe.remove()); + + await registration.unregister(); + + const newRegistration = await navigator.serviceWorker.register(newWorkerURL, { scope }); + t.add_cleanup(() => newRegistration.unregister()); + + assert_equals( + registration.installing, + null, + 'before activated registration.installing' + ); + assert_equals( + registration.waiting, + null, + 'before activated registration.waiting' + ); + assert_equals( + registration.active.scriptURL, + normalizeURL(worker_url), + 'before activated registration.active' + ); + assert_equals( + newRegistration.installing.scriptURL, + normalizeURL(newWorkerURL), + 'before activated newRegistration.installing' + ); + assert_equals( + newRegistration.waiting, + null, + 'before activated newRegistration.waiting' + ); + assert_equals( + newRegistration.active, + null, + 'before activated newRegistration.active' + ); + iframe.remove(); + + await wait_for_state(t, newRegistration.installing, 'activated'); + + assert_equals( + newRegistration.installing, + null, + 'after activated newRegistration.installing' + ); + assert_equals( + newRegistration.waiting, + null, + 'after activated newRegistration.waiting' + ); + assert_equals( + newRegistration.active.scriptURL, + normalizeURL(newWorkerURL), + 'after activated newRegistration.active' + ); + + const newIframe = await with_iframe(scope); + t.add_cleanup(() => newIframe.remove()); + + assert_equals( + newIframe.contentWindow.navigator.serviceWorker.controller.scriptURL, + normalizeURL(newWorkerURL), + 'the new worker should control a new document' + ); +}, 'Registering a new script URL while an unregistered registration is in use'); + +promise_test(async function(t) { + const scope = 'resources/scope/unregister-then-register-new-script-that-404s'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activated'); + + const iframe = await with_iframe(scope); + t.add_cleanup(() => iframe.remove()); + + await registration.unregister(); + + await promise_rejects_js( + t, TypeError, + navigator.serviceWorker.register('this-will-404', { scope }) + ); + + assert_equals(registration.installing, null, 'registration.installing'); + assert_equals(registration.waiting, null, 'registration.waiting'); + assert_equals(registration.active.scriptURL, normalizeURL(worker_url), 'registration.active'); + + const newIframe = await with_iframe(scope); + t.add_cleanup(() => newIframe.remove()); + + assert_equals(newIframe.contentWindow.navigator.serviceWorker.controller, null, 'Document should not be controlled'); +}, 'Registering a new script URL that 404s does not resurrect unregistered registration'); + +promise_test(async function(t) { + const scope = 'resources/scope/unregister-then-register-reject-install-worker'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activated'); + + const iframe = await with_iframe(scope); + t.add_cleanup(() => iframe.remove()); + + await registration.unregister(); + + const newRegistration = await navigator.serviceWorker.register( + 'resources/reject-install-worker.js', { scope } + ); + t.add_cleanup(() => newRegistration.unregister()); + + await wait_for_state(t, newRegistration.installing, 'redundant'); + + assert_equals(registration.installing, null, 'registration.installing'); + assert_equals(registration.waiting, null, 'registration.waiting'); + assert_equals(registration.active.scriptURL, normalizeURL(worker_url), + 'registration.active'); + assert_not_equals(registration, newRegistration, 'New registration is different'); +}, 'Registering a new script URL that fails to install does not resurrect unregistered registration'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-then-register.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-then-register.https.html new file mode 100644 index 0000000000..b61608c841 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/unregister-then-register.https.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +var worker_url = 'resources/empty-worker.js'; + +promise_test(async function(t) { + const scope = 'resources/scope/re-register-resolves-to-new-value'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activated'); + await registration.unregister(); + const newRegistration = await navigator.serviceWorker.register(worker_url, { scope }); + t.add_cleanup(() => newRegistration.unregister()); + + assert_not_equals( + registration, newRegistration, + 'register should resolve to a new value' + ); + }, 'Unregister then register resolves to a new value'); + +promise_test(async function(t) { + const scope = 'resources/scope/re-register-while-old-registration-in-use'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + await registration.unregister(); + const newRegistration = await navigator.serviceWorker.register(worker_url, { scope }); + t.add_cleanup(() => newRegistration.unregister()); + + assert_not_equals( + registration, newRegistration, + 'Unregister and register should always create a new registration' + ); +}, 'Unregister then register does not resolve to the original value even if the registration is in use.'); + +promise_test(function(t) { + var scope = 'resources/scope/re-register-does-not-affect-existing-controllee'; + var iframe; + var registration; + var controller; + + return service_worker_unregister_and_register(t, worker_url, scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + return wait_for_state(t, r.installing, 'activated'); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(frame) { + iframe = frame; + controller = iframe.contentWindow.navigator.serviceWorker.controller; + return registration.unregister(); + }) + .then(function() { + return navigator.serviceWorker.register(worker_url, { scope: scope }); + }) + .then(function(newRegistration) { + assert_equals(registration.installing, null, + 'installing version is null'); + assert_equals(registration.waiting, null, 'waiting version is null'); + assert_equals( + iframe.contentWindow.navigator.serviceWorker.controller, + controller, + 'the worker from the first registration is the controller'); + iframe.remove(); + }); + }, 'Unregister then register does not affect existing controllee'); + +promise_test(async function(t) { + const scope = 'resources/scope/resurrection'; + const altWorkerURL = worker_url + '?alt'; + const registration = await service_worker_unregister_and_register(t, worker_url, scope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activating'); + const iframe = await with_iframe(scope); + t.add_cleanup(() => iframe.remove()); + + await registration.unregister(); + const newRegistration = await navigator.serviceWorker.register(altWorkerURL, { scope }); + t.add_cleanup(() => newRegistration.unregister()); + + assert_equals(newRegistration.active, null, 'Registration is new'); + + await wait_for_state(t, newRegistration.installing, 'activating'); + + const newIframe = await with_iframe(scope); + t.add_cleanup(() => newIframe.remove()); + + const iframeController = iframe.contentWindow.navigator.serviceWorker.controller; + const newIframeController = newIframe.contentWindow.navigator.serviceWorker.controller; + + assert_not_equals(iframeController, newIframeController, 'iframes have different controllers'); +}, 'Unregister then register does not resurrect the registration'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister.https.html new file mode 100644 index 0000000000..492aecb21a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/unregister.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +async_test(function(t) { + var scope = 'resources/scope/unregister-twice'; + var registration; + navigator.serviceWorker.register('resources/empty-worker.js', + {scope: scope}) + .then(function(r) { + registration = r; + return registration.unregister(); + }) + .then(function() { + return registration.unregister(); + }) + .then(function(value) { + assert_equals(value, false, + 'unregistering twice should resolve with false'); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Unregister twice'); + +async_test(function(t) { + var scope = 'resources/scope/successful-unregister/'; + navigator.serviceWorker.register('resources/empty-worker.js', + {scope: scope}) + .then(function(registration) { + return registration.unregister(); + }) + .then(function(value) { + assert_equals(value, true, + 'unregistration should resolve with true'); + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Register then unregister'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html b/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html new file mode 100644 index 0000000000..ff51f7f902 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<meta name=timeout content=long> +<title>Service Worker: Update should be triggered after a navigation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +async function cleanup(frame, registration) { + if (frame) + frame.remove(); + if (registration) + await registration.unregister(); +} + +promise_test(async t => { + const script = 'resources/update_shell.py?filename=empty.js'; + const scope = 'resources/scope/update'; + let registration; + let frame; + + async function run() { + registration = await service_worker_unregister_and_register( + t, script, scope); + await wait_for_state(t, registration.installing, 'activated'); + + // Navigation should trigger update. + frame = await with_iframe(scope); + await wait_for_update(t, registration); + } + + try { + await run(); + } finally { + await cleanup(frame, registration); + } +}, 'Update should be triggered after a navigation (no fetch event worker).'); + +promise_test(async t => { + const script = 'resources/update_shell.py?filename=simple-intercept-worker.js'; + const scope = 'resources/scope/update'; + let registration; + let frame; + + async function run() { + registration = await service_worker_unregister_and_register( + t, script, scope); + await wait_for_state(t, registration.installing, 'activated'); + + // Navigation should trigger update (network fallback). + frame = await with_iframe(scope + '?ignore'); + await wait_for_update(t, registration); + + // Navigation should trigger update (respondWith called). + frame.src = scope + '?string'; + await wait_for_update(t, registration); + } + + try { + await run(); + } finally { + await cleanup(frame, registration); + } +}, 'Update should be triggered after a navigation (fetch event worker).'); + +promise_test(async t => { + const script = 'resources/update_shell.py?filename=empty.js'; + const scope = 'resources/'; + let registration; + let frame; + + async function run() { + registration = await service_worker_unregister_and_register( + t, script, scope); + await wait_for_state(t, registration.installing, 'activated'); + + // Navigation should trigger update. Don't use with_iframe as it waits for + // the onload event. + frame = document.createElement('iframe'); + frame.src = 'resources/malformed-http-response.asis'; + document.body.appendChild(frame); + await wait_for_update(t, registration); + } + + try { + await run(); + } finally { + await cleanup(frame, registration); + } +}, 'Update should be triggered after a navigation (network error).'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-redirect.https.html new file mode 100644 index 0000000000..6e821fe479 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-redirect.https.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: Update should be triggered after redirects during navigation</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +promise_test(async t => { + // This test does a navigation that goes through a redirect chain. Each + // request in the chain has a service worker. Each service worker has no + // fetch event handler. The redirects are performed by redirect.py. + const script = 'resources/update-nocookie-worker.py'; + const scope1 = 'resources/redirect.py?scope1'; + const scope2 = 'resources/redirect.py?scope2'; + const scope3 = 'resources/empty.html'; + let registration1; + let registration2; + let registration3; + let frame; + + async function cleanup() { + if (frame) + frame.remove(); + if (registration1) + return registration1.unregister(); + if (registration2) + return registration2.unregister(); + if (registration3) + return registration3.unregister(); + } + + async function make_active_registration(scope) { + const registration = + await service_worker_unregister_and_register(t, script, scope); + await wait_for_state(t, registration.installing, 'activated'); + return registration; + } + + async function run() { + // Make the registrations. + registration1 = await make_active_registration(scope1); + registration2 = await make_active_registration(scope2); + registration3 = await make_active_registration(scope3); + + // Make the promises that resolve on update. + const saw_update1 = wait_for_update(t, registration1); + const saw_update2 = wait_for_update(t, registration2); + const saw_update3 = wait_for_update(t, registration3); + + // Create a URL for the redirect chain: scope1 -> scope2 -> scope3. + // Build the URL in reverse order. + let url = `${base_path()}${scope3}`; + url = `${base_path()}${scope2}&Redirect=${encodeURIComponent(url)}` + url = `${base_path()}${scope1}&Redirect=${encodeURIComponent(url)}` + + // Navigate to the URL. + frame = await with_iframe(url); + + // Each registration should update. + await saw_update1; + await saw_update2; + await saw_update3; + } + + try { + await run(); + } finally { + await cleanup(); + } +}, 'service workers are updated on redirects during navigation'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-after-oneday.https.html b/testing/web-platform/tests/service-workers/service-worker/update-after-oneday.https.html new file mode 100644 index 0000000000..e7a8aa42d3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-after-oneday.https.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- This test requires browser to treat all registrations are older than 24 hours. + Preference 'dom.serviceWorkers.testUpdateOverOneDay' should be enabled during + the execution of the test --> +<title>Service Worker: Functional events should trigger update if last update time is over 24 hours</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> + +promise_test(function(t) { + var script = 'resources/update-nocookie-worker.py'; + var scope = 'resources/update/update-after-oneday.https.html'; + var expected_url = normalizeURL(script); + var registration; + var frame; + + return service_worker_unregister_and_register(t, expected_url, scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(scope); }) + .then(function(f) { + frame = f; + return wait_for_update(t, registration); + }) + .then(function() { + assert_equals(registration.installing.scriptURL, expected_url, + 'new installing should be set after update resolves.'); + assert_equals(registration.waiting, null, + 'waiting should still be null after update resolves.'); + assert_equals(registration.active.scriptURL, expected_url, + 'active should still exist after update found.'); + return wait_for_state(t, registration.installing, 'installed'); + }) + .then(function() { + // Trigger a non-navigation fetch event + frame.contentWindow.load_image(normalizeURL('resources/update/sample')); + return wait_for_update(t, registration); + }) + .then(function() { + frame.remove(); + }) + }, 'Update should be triggered after a functional event when last update time is over 24 hours'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html b/testing/web-platform/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html new file mode 100644 index 0000000000..121a7378e3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html @@ -0,0 +1,92 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script> +// Tests of updating a service worker. This file contains cors cases only. + +/* + * @param string main + * Decide the content of the main script, where 'default' is for constant + * content while 'time' is for time-variant content. + * @param string imported + * Decide the content of the imported script, where 'default' is for constant + * content while 'time' is for time-variant content. + */ +const settings = [{main: 'default', imported: 'default'}, + {main: 'default', imported: 'time' }, + {main: 'time', imported: 'default'}, + {main: 'time', imported: 'time' }]; + +const host_info = get_host_info(); +settings.forEach(({main, imported}) => { + promise_test(async (t) => { + // Specify a cross origin path to load imported scripts from a cross origin. + const path = host_info.HTTPS_REMOTE_ORIGIN + + '/service-workers/service-worker/resources/'; + const script = 'resources/bytecheck-worker.py' + + '?main=' + main + + '&imported=' + imported + + '&path=' + path + + '&type=classic'; + const scope = 'resources/blank.html'; + + // Register a service worker. + const swr = await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => swr.unregister()); + const sw = await wait_for_update(t, swr); + await wait_for_state(t, sw, 'activated'); + assert_array_equals([swr.active, swr.waiting, swr.installing], + [sw, null, null]); + + // Update the service worker registration. + await swr.update(); + + // If there should be a new service worker. + if (main === 'time' || imported === 'time') { + return wait_for_update(t, swr); + } + // Otherwise, make sure there is no newly created service worker. + assert_array_equals([swr.active, swr.waiting, swr.installing], + [sw, null, null]); + }, `Test(main: ${main}, imported: ${imported})`); +}); + +settings.forEach(({main, imported}) => { + promise_test(async (t) => { + // Specify a cross origin path to load imported scripts from a cross origin. + const path = host_info.HTTPS_REMOTE_ORIGIN + + '/service-workers/service-worker/resources/'; + const script = 'resources/bytecheck-worker.py' + + '?main=' + main + + '&imported=' + imported + + '&path=' + path + + '&type=module'; + const scope = 'resources/blank.html'; + + // Register a service worker. + const swr = await service_worker_unregister_and_register(t, script, scope, {type: 'module'}); + t.add_cleanup(() => swr.unregister()); + const sw = await wait_for_update(t, swr); + await wait_for_state(t, sw, 'activated'); + assert_array_equals([swr.active, swr.waiting, swr.installing], + [sw, null, null]); + + // Update the service worker registration. + await swr.update(); + + // If there should be a new service worker. + if (main === 'time' || imported === 'time') { + return wait_for_update(t, swr); + } + // Otherwise, make sure there is no newly created service worker. + assert_array_equals([swr.active, swr.waiting, swr.installing], + [sw, null, null]); + }, `Test module script(main: ${main}, imported: ${imported})`); +}); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-bytecheck.https.html b/testing/web-platform/tests/service-workers/service-worker/update-bytecheck.https.html new file mode 100644 index 0000000000..3e5a28bb67 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-bytecheck.https.html @@ -0,0 +1,92 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script> +// Tests of updating a service worker. This file contains non-cors cases only. + +/* + * @param string main + * Decide the content of the main script, where 'default' is for constant + * content while 'time' is for time-variant content. + * @param string imported + * Decide the content of the imported script, where 'default' is for constant + * content while 'time' is for time-variant content. + */ +const settings = [{main: 'default', imported: 'default'}, + {main: 'default', imported: 'time' }, + {main: 'time', imported: 'default'}, + {main: 'time', imported: 'time' }]; + +const host_info = get_host_info(); +settings.forEach(({main, imported}) => { + promise_test(async (t) => { + // Empty path results in the same origin imported scripts. + const path = ''; + const script = 'resources/bytecheck-worker.py' + + '?main=' + main + + '&imported=' + imported + + '&path=' + path + + '&type=classic'; + const scope = 'resources/blank.html'; + + // Register a service worker. + const swr = await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => swr.unregister()); + const sw = await wait_for_update(t, swr); + await wait_for_state(t, sw, 'activated'); + assert_array_equals([swr.active, swr.waiting, swr.installing], + [sw, null, null]); + + // Update the service worker registration. + await swr.update(); + + // If there should be a new service worker. + if (main === 'time' || imported === 'time') { + return wait_for_update(t, swr); + } + // Otherwise, make sure there is no newly created service worker. + assert_array_equals([swr.active, swr.waiting, swr.installing], + [sw, null, null]); + }, `Test(main: ${main}, imported: ${imported})`); +}); + +settings.forEach(({main, imported}) => { + promise_test(async (t) => { + // Empty path results in the same origin imported scripts. + const path = './'; + const script = 'resources/bytecheck-worker.py' + + '?main=' + main + + '&imported=' + imported + + '&path=' + path + + '&type=module'; + const scope = 'resources/blank.html'; + + // Register a module service worker. + const swr = await service_worker_unregister_and_register(t, script, scope, + {type: 'module'}); + + t.add_cleanup(() => swr.unregister()); + const sw = await wait_for_update(t, swr); + await wait_for_state(t, sw, 'activated'); + assert_array_equals([swr.active, swr.waiting, swr.installing], + [sw, null, null]); + + // Update the service worker registration. + await swr.update(); + + // If there should be a new service worker. + if (main === 'time' || imported === 'time') { + return wait_for_update(t, swr); + } + // Otherwise, make sure there is no newly created service worker. + assert_array_equals([swr.active, swr.waiting, swr.installing], + [sw, null, null]); + }, `Test module script(main: ${main}, imported: ${imported})`); +}); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-import-scripts.https.html b/testing/web-platform/tests/service-workers/service-worker/update-import-scripts.https.html new file mode 100644 index 0000000000..a2df529e90 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-import-scripts.https.html @@ -0,0 +1,135 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Tests for importScripts: import scripts ignored error</title> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +// This file contains tests to check if imported scripts appropriately updated. + +const SCOPE = 'resources/simple.txt'; + +// Create a service worker (update-worker-from-file.py), which is initially +// |initial_worker| and |updated_worker| later. +async function prepare_ready_update_worker_from_file( + t, initial_worker, updated_worker) { + const key = token(); + const worker_url = `resources/update-worker-from-file.py?` + + `First=${initial_worker}&Second=${updated_worker}&Key=${key}`; + const expected_url = normalizeURL(worker_url); + + const registration = await service_worker_unregister_and_register( + t, worker_url, SCOPE); + await wait_for_state(t, registration.installing, 'activated'); + assert_equals(registration.installing, null, + 'prepare_ready: installing'); + assert_equals(registration.waiting, null, + 'prepare_ready: waiting'); + assert_equals(registration.active.scriptURL, expected_url, + 'prepare_ready: active'); + return [registration, expected_url]; +} + +// Create a service worker using the script under resources/. +async function prepare_ready_normal_worker(t, filename, additional_params='') { + const key = token(); + const worker_url = `resources/${filename}?Key=${key}&${additional_params}`; + const expected_url = normalizeURL(worker_url); + + const registration = await service_worker_unregister_and_register( + t, worker_url, SCOPE); + await wait_for_state(t, registration.installing, 'activated'); + assert_equals(registration.installing, null, + 'prepare_ready: installing'); + assert_equals(registration.waiting, null, + 'prepare_ready: waiting'); + assert_equals(registration.active.scriptURL, expected_url, + 'prepare_ready: active'); + return [registration, expected_url]; +} + +function assert_installing_and_active(registration, expected_url) { + assert_equals(registration.installing.scriptURL, expected_url, + 'assert_installing_and_active: installing'); + assert_equals(registration.waiting, null, + 'assert_installing_and_active: waiting'); + assert_equals(registration.active.scriptURL, expected_url, + 'assert_installing_and_active: active'); +} + +function assert_waiting_and_active(registration, expected_url) { + assert_equals(registration.installing, null, + 'assert_waiting_and_active: installing'); + assert_equals(registration.waiting.scriptURL, expected_url, + 'assert_waiting_and_active: waiting'); + assert_equals(registration.active.scriptURL, expected_url, + 'assert_waiting_and_active: active'); +} + +function assert_active_only(registration, expected_url) { + assert_equals(registration.installing, null, + 'assert_active_only: installing'); + assert_equals(registration.waiting, null, + 'assert_active_only: waiting'); + assert_equals(registration.active.scriptURL, expected_url, + 'assert_active_only: active'); +} + +promise_test(async t => { + const [registration, expected_url] = + await prepare_ready_update_worker_from_file( + t, 'empty.js', 'import-scripts-404.js'); + t.add_cleanup(() => registration.unregister()); + + await promise_rejects_js(t, TypeError, registration.update()); + assert_active_only(registration, expected_url); +}, 'update() should fail when a new worker imports an unavailable script.'); + +promise_test(async t => { + const [registration, expected_url] = + await prepare_ready_update_worker_from_file( + t, 'import-scripts-404-after-update.js', 'empty.js'); + t.add_cleanup(() => registration.unregister()); + + await Promise.all([registration.update(), wait_for_update(t, registration)]); + assert_installing_and_active(registration, expected_url); + + await wait_for_state(t, registration.installing, 'installed'); + assert_waiting_and_active(registration, expected_url); + + await wait_for_state(t, registration.waiting, 'activated'); + assert_active_only(registration, expected_url); +}, 'update() should succeed when the old imported script no longer exist but ' + + "the new worker doesn't import it."); + +promise_test(async t => { + const [registration, expected_url] = await prepare_ready_normal_worker( + t, 'import-scripts-404-after-update.js'); + t.add_cleanup(() => registration.unregister()); + + await registration.update(); + assert_active_only(registration, expected_url); +}, 'update() should treat 404 on imported scripts as no change.'); + +promise_test(async t => { + const [registration, expected_url] = await prepare_ready_normal_worker( + t, 'import-scripts-404-after-update-plus-update-worker.js', + `AdditionalKey=${token()}`); + t.add_cleanup(() => registration.unregister()); + + await promise_rejects_js(t, TypeError, registration.update()); + assert_active_only(registration, expected_url); +}, 'update() should find an update in an imported script but update() should ' + + 'result in failure due to missing the other imported script.'); + +promise_test(async t => { + const [registration, expected_url] = await prepare_ready_normal_worker( + t, 'import-scripts-cross-origin-worker.sub.js'); + t.add_cleanup(() => registration.unregister()); + await registration.update(); + assert_installing_and_active(registration, expected_url); +}, 'update() should work with cross-origin importScripts.'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-missing-import-scripts.https.html b/testing/web-platform/tests/service-workers/service-worker/update-missing-import-scripts.https.html new file mode 100644 index 0000000000..66e8bfac75 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-missing-import-scripts.https.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<title>Service Worker: update with missing importScripts</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script src="/common/utils.js"></script> +<body> +<script> +/** + * Test ServiceWorkerRegistration.update() when importScripts in a service worker + * script is no longer available (but was initially). + */ +let registration = null; + +promise_test(async (test) => { + const script = `resources/update-missing-import-scripts-main-worker.py?key=${token()}`; + const scope = 'resources/update-missing-import-scripts'; + + registration = await service_worker_unregister_and_register(test, script, scope); + + add_completion_callback(() => { registration.unregister(); }); + + await wait_for_state(test, registration.installing, 'activated'); +}, 'Initialize global state'); + +promise_test(test => { + return new Promise(resolve => { + registration.addEventListener('updatefound', resolve); + registration.update(); + }); +}, 'Update service worker with new script that\'s missing importScripts()'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-module-request-mode.https.html b/testing/web-platform/tests/service-workers/service-worker/update-module-request-mode.https.html new file mode 100644 index 0000000000..b3875d207b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-module-request-mode.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<title>Test that mode is set to same-origin for a main module</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +// Tests a main module service worker script fetch during an update check. +// The fetch should have the mode set to 'same-origin'. +// +// The test works by registering a main module service worker. It then does an +// update. The test server responds with an updated worker script that remembers +// the http request. The updated worker reports back this request to the test +// page. +promise_test(async (t) => { + const script = "resources/test-request-mode-worker.py"; + const scope = "resources/"; + + // Register the service worker. + await service_worker_unregister(t, scope); + const registration = await navigator.serviceWorker.register( + script, {scope, type: 'module'}); + await wait_for_state(t, registration.installing, 'activated'); + + // Do an update. + await registration.update(); + + // Ask the new worker what the request was. + const newWorker = registration.installing; + const sawMessage = new Promise((resolve) => { + navigator.serviceWorker.onmessage = (event) => { + resolve(event.data); + }; + }); + newWorker.postMessage('getHeaders'); + const result = await sawMessage; + + // Test the result. + assert_equals(result['sec-fetch-mode'], 'same-origin'); + assert_equals(result['origin'], undefined); + +}, 'headers of a main module script'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-no-cache-request-headers.https.html b/testing/web-platform/tests/service-workers/service-worker/update-no-cache-request-headers.https.html new file mode 100644 index 0000000000..6ebad4b7b1 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-no-cache-request-headers.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test that cache is being bypassed/validated in no-cache mode on update</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +// Tests a service worker script fetch during an update check which +// bypasses/validates the browser cache. The fetch should have the +// 'if-none-match' request header. +// +// This tests the Update step: +// "Set request’s cache mode to "no-cache" if any of the following are true..." +// https://w3c.github.io/ServiceWorker/#update-algorithm +// +// The test works by registering a service worker with |updateViaCache| +// set to "none". It then does an update. The test server responds with +// an updated worker script that remembers the http request headers. +// The updated worker reports back these headers to the test page. +promise_test(async (t) => { + const script = "resources/test-request-headers-worker.py"; + const scope = "resources/"; + + // Register the service worker. + await service_worker_unregister(t, scope); + const registration = await navigator.serviceWorker.register( + script, {scope, updateViaCache: 'none'}); + await wait_for_state(t, registration.installing, 'activated'); + + // Do an update. + await registration.update(); + + // Ask the new worker what the request headers were. + const newWorker = registration.installing; + const sawMessage = new Promise((resolve) => { + navigator.serviceWorker.onmessage = (event) => { + resolve(event.data); + }; + }); + newWorker.postMessage('getHeaders'); + const result = await sawMessage; + + // Test the result. + assert_equals(result['service-worker'], 'script'); + assert_equals(result['if-none-match'], 'etag'); +}, 'headers in no-cache mode'); + +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-not-allowed.https.html b/testing/web-platform/tests/service-workers/service-worker/update-not-allowed.https.html new file mode 100644 index 0000000000..0a54aa9350 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-not-allowed.https.html @@ -0,0 +1,140 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +function send_message_to_worker_and_wait_for_response(worker, message) { + return new Promise(resolve => { + // Use a dedicated channel for every request to avoid race conditions on + // concurrent requests. + const channel = new MessageChannel(); + worker.postMessage(channel.port1, [channel.port1]); + + let messageReceived = false; + channel.port2.onmessage = event => { + assert_false(messageReceived, 'Already received response for ' + message); + messageReceived = true; + resolve(event.data); + }; + channel.port2.postMessage(message); + }); +} + +async function ensure_install_event_fired(worker) { + const response = await send_message_to_worker_and_wait_for_response(worker, 'awaitInstallEvent'); + assert_equals('installEventFired', response); + assert_equals('installing', worker.state, 'Expected worker to be installing.'); +} + +async function finish_install(worker) { + await ensure_install_event_fired(worker); + const response = await send_message_to_worker_and_wait_for_response(worker, 'finishInstall'); + assert_equals('installFinished', response); +} + +async function activate_service_worker(t, worker) { + await finish_install(worker); + // By waiting for both states at the same time, the test fails + // quickly if the installation fails, avoiding a timeout. + await Promise.race([wait_for_state(t, worker, 'activated'), + wait_for_state(t, worker, 'redundant')]); + assert_equals('activated', worker.state, 'Service worker should be activated.'); +} + +async function update_within_service_worker(worker) { + // This function returns a Promise that resolves when update() + // has been called but is not necessarily finished yet. + // Call finish() on the returned object to wait for update() settle. + const port = await send_message_to_worker_and_wait_for_response(worker, 'callUpdate'); + let messageReceived = false; + return { + finish: () => { + return new Promise(resolve => { + port.onmessage = event => { + assert_false(messageReceived, 'Update already finished.'); + messageReceived = true; + resolve(event.data); + }; + }); + }, + }; +} + +async function update_from_client_and_await_installing_version(test, registration) { + const updatefound = wait_for_update(test, registration); + registration.update(); + await updatefound; + return registration.installing; +} + +async function spin_up_service_worker(test) { + const script = 'resources/update-during-installation-worker.py'; + const scope = 'resources/blank.html'; + + const registration = await service_worker_unregister_and_register(test, script, scope); + test.add_cleanup(async () => { + if (registration.installing) { + // If there is an installing worker, we need to finish installing it. + // Otherwise, the tests fails with an timeout because unregister() blocks + // until the install-event-handler finishes. + const worker = registration.installing; + await send_message_to_worker_and_wait_for_response(worker, 'awaitInstallEvent'); + await send_message_to_worker_and_wait_for_response(worker, 'finishInstall'); + } + return registration.unregister(); + }); + + return registration; +} + +promise_test(async t => { + const registration = await spin_up_service_worker(t); + const worker = registration.installing; + await ensure_install_event_fired(worker); + + const result = registration.update(); + await activate_service_worker(t, worker); + return result; +}, 'ServiceWorkerRegistration.update() from client succeeds while installing service worker.'); + +promise_test(async t => { + const registration = await spin_up_service_worker(t); + const worker = registration.installing; + await ensure_install_event_fired(worker); + + // Add event listener to fail the test if update() succeeds. + const updatefound = t.step_func(async () => { + registration.removeEventListener('updatefound', updatefound); + // Activate new worker so non-compliant browsers don't fail with timeout. + await activate_service_worker(t, registration.installing); + assert_unreached("update() should have failed"); + }); + registration.addEventListener('updatefound', updatefound); + + const update = await update_within_service_worker(worker); + // Activate worker to ensure update() finishes and the test doesn't timeout + // in non-compliant browsers. + await activate_service_worker(t, worker); + + const response = await update.finish(); + assert_false(response.success, 'update() should have failed.'); + assert_equals('InvalidStateError', response.exception, 'update() should have thrown InvalidStateError.'); +}, 'ServiceWorkerRegistration.update() from installing service worker throws.'); + +promise_test(async t => { + const registration = await spin_up_service_worker(t); + const worker1 = registration.installing; + await activate_service_worker(t, worker1); + + const worker2 = await update_from_client_and_await_installing_version(t, registration); + await ensure_install_event_fired(worker2); + + const update = await update_within_service_worker(worker1); + // Activate the new version so that update() finishes and the test doesn't timeout. + await activate_service_worker(t, worker2); + const response = await update.finish(); + assert_true(response.success, 'update() from active service worker should have succeeded.'); +}, 'ServiceWorkerRegistration.update() from active service worker succeeds while installing service worker.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-on-navigation.https.html b/testing/web-platform/tests/service-workers/service-worker/update-on-navigation.https.html new file mode 100644 index 0000000000..5273420b90 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-on-navigation.https.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<title>Update on navigation</title> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='resources/test-helpers.sub.js'></script> +<script> +promise_test(async (t) => { + var script = 'resources/update-fetch-worker.py'; + var scope = 'resources/trickle.py?ms=1000&count=1'; + + const registration = await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => registration.unregister()); + + if (registration.installing) + await wait_for_state(t, registration.installing, 'activated'); + + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); +}, 'The active service worker in charge of a navigation load should not be terminated as part of updating the registration'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-recovery.https.html b/testing/web-platform/tests/service-workers/service-worker/update-recovery.https.html new file mode 100644 index 0000000000..17608d2ef7 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-recovery.https.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<title>Service Worker: recovery by navigation update</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(function(t) { + var scope = 'resources/simple.txt'; + var worker_url = 'resources/update-recovery-worker.py'; + var expected_url = normalizeURL(worker_url); + var registration; + + function with_bad_iframe(url) { + return new Promise(function(resolve, reject) { + var frame = document.createElement('iframe'); + + // There is no cross-browser event to listen for to detect an + // iframe that fails to load due to a bad interception. Unfortunately + // we have to use a timeout. + var timeout = setTimeout(function() { + frame.remove(); + resolve(); + }, 5000); + + // If we do get a load event, though, we know something went wrong. + frame.addEventListener('load', function() { + clearTimeout(timeout); + frame.remove(); + reject('expected bad iframe should not fire a load event!'); + }); + + frame.src = url; + document.body.appendChild(frame); + }); + } + + function with_update(t) { + return new Promise(function(resolve, reject) { + registration.addEventListener('updatefound', function onUpdate() { + registration.removeEventListener('updatefound', onUpdate); + wait_for_state(t, registration.installing, 'activated').then(function() { + resolve(); + }); + }); + }); + } + + return service_worker_unregister_and_register(t, worker_url, scope) + .then(function(r) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + registration = r; + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { + return Promise.all([ + with_update(t), + with_bad_iframe(scope) + ]); + }) + .then(function() { + return with_iframe(scope); + }) + .then(function(frame) { + assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL, + expected_url); + frame.remove(); + }); + }, 'Recover from a bad service worker by updating after a failed navigation.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-registration-with-type.https.html b/testing/web-platform/tests/service-workers/service-worker/update-registration-with-type.https.html new file mode 100644 index 0000000000..269e61b390 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-registration-with-type.https.html @@ -0,0 +1,208 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: Update the registration with a different script type.</title> +<!-- common.js is for guid() --> +<script src="/common/security-features/resources/common.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +// The following two tests check that a registration is updated correctly +// with different script type. At first Service Worker is registered as +// classic script type, then it is re-registered as module script type, +// and vice versa. A main script is also updated at the same time. +promise_test(async t => { + const key = guid(); + const script = `resources/update-registration-with-type.py?classic_first=1&key=${key}`; + const scope = 'resources/update-registration-with-type'; + await service_worker_unregister(t, scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + + // Register with classic script type. + const firstRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'classic' + }); + const firstWorker = firstRegistration.installing; + await wait_for_state(t, firstWorker, 'activated'); + firstWorker.postMessage(' '); + let msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r); + assert_equals(msgEvent.data, 'A classic script.'); + + // Re-register with module script type. + const secondRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'module' + }); + const secondWorker = secondRegistration.installing; + secondWorker.postMessage(' '); + msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r); + assert_equals(msgEvent.data, 'A module script.'); + + assert_not_equals(firstWorker, secondWorker); + assert_equals(firstRegistration, secondRegistration); +}, 'Update the registration with a different script type (classic => module).'); + +promise_test(async t => { + const key = guid(); + const script = `resources/update-registration-with-type.py?classic_first=0&key=${key}`; + const scope = 'resources/update-registration-with-type'; + await service_worker_unregister(t, scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + + // Register with module script type. + const firstRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'module' + }); + const firstWorker = firstRegistration.installing; + await wait_for_state(t, firstWorker, 'activated'); + firstWorker.postMessage(' '); + let msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r); + assert_equals(msgEvent.data, 'A module script.'); + + // Re-register with classic script type. + const secondRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'classic' + }); + const secondWorker = secondRegistration.installing; + secondWorker.postMessage(' '); + msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r); + assert_equals(msgEvent.data, 'A classic script.'); + + assert_not_equals(firstWorker, secondWorker); + assert_equals(firstRegistration, secondRegistration); +}, 'Update the registration with a different script type (module => classic).'); + +// The following two tests change the script type while keeping +// the script identical. +promise_test(async t => { + const script = 'resources/empty-worker.js'; + const scope = 'resources/update-registration-with-type'; + await service_worker_unregister(t, scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + + // Register with classic script type. + const firstRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'classic' + }); + const firstWorker = firstRegistration.installing; + await wait_for_state(t, firstWorker, 'activated'); + + // Re-register with module script type. + const secondRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'module' + }); + const secondWorker = secondRegistration.installing; + + assert_not_equals(firstWorker, secondWorker); + assert_equals(firstRegistration, secondRegistration); +}, 'Update the registration with a different script type (classic => module) ' + + 'and with a same main script.'); + +promise_test(async t => { + const script = 'resources/empty-worker.js'; + const scope = 'resources/update-registration-with-type'; + await service_worker_unregister(t, scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + + // Register with module script type. + const firstRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'module' + }); + const firstWorker = firstRegistration.installing; + await wait_for_state(t, firstWorker, 'activated'); + + // Re-register with classic script type. + const secondRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'classic' + }); + const secondWorker = secondRegistration.installing; + + assert_not_equals(firstWorker, secondWorker); + assert_equals(firstRegistration, secondRegistration); +}, 'Update the registration with a different script type (module => classic) ' + + 'and with a same main script.'); + +// This test checks that a registration is not updated with the same script +// type and the same main script. +promise_test(async t => { + const script = 'resources/empty-worker.js'; + const scope = 'resources/update-registration-with-type'; + await service_worker_unregister(t, scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + + // Register with module script type. + const firstRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'module' + }); + await wait_for_state(t, firstRegistration.installing, 'activated'); + + // Re-register with module script type. + const secondRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'module' + }); + assert_equals(secondRegistration.installing, null); + + assert_equals(firstRegistration, secondRegistration); +}, 'Does not update the registration with the same script type and ' + + 'the same main script.'); + +// In the case (classic => module), a worker script contains importScripts() +// that is disallowed on module scripts, so the second registration is +// expected to fail script evaluation. +promise_test(async t => { + const script = 'resources/classic-worker.js'; + const scope = 'resources/update-registration-with-type'; + await service_worker_unregister(t, scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + + // Register with classic script type. + const firstRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'classic' + }); + assert_not_equals(firstRegistration.installing, null); + await wait_for_state(t, firstRegistration.installing, 'activated'); + + // Re-register with module script type and expect TypeError. + return promise_rejects_js(t, TypeError, navigator.serviceWorker.register(script, { + scope: scope, + type: 'module' + }), 'Registering with invalid evaluation should be failed.'); +}, 'Update the registration with a different script type (classic => module) ' + + 'and with a same main script. Expect evaluation failed.'); + +// In the case (module => classic), a worker script contains static-import +// that is disallowed on classic scripts, so the second registration is +// expected to fail script evaluation. +promise_test(async t => { + const script = 'resources/module-worker.js'; + const scope = 'resources/update-registration-with-type'; + await service_worker_unregister(t, scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + + // Register with module script type. + const firstRegistration = await navigator.serviceWorker.register(script, { + scope: scope, + type: 'module' + }); + assert_not_equals(firstRegistration.installing, null); + await wait_for_state(t, firstRegistration.installing, 'activated'); + + // Re-register with classic script type and expect TypeError. + return promise_rejects_js(t, TypeError, navigator.serviceWorker.register(script, { + scope: scope, + type: 'classic' + }), 'Registering with invalid evaluation should be failed.'); +}, 'Update the registration with a different script type (module => classic) ' + + 'and with a same main script. Expect evaluation failed.'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/update-result.https.html b/testing/web-platform/tests/service-workers/service-worker/update-result.https.html new file mode 100644 index 0000000000..d8ed94f776 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update-result.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<title>Service Worker: update() should resolve a ServiceWorkerRegistration</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +promise_test(async function(t) { + const script = './resources/empty.js'; + const scope = './resources/empty.html?update-result'; + + let reg = await navigator.serviceWorker.register(script, { scope }); + t.add_cleanup(async _ => await reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + let result = await reg.update(); + assert_true(result instanceof ServiceWorkerRegistration, + 'update() should resolve a ServiceWorkerRegistration'); +}, 'ServiceWorkerRegistration.update() should resolve a registration object'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/update.https.html b/testing/web-platform/tests/service-workers/service-worker/update.https.html new file mode 100644 index 0000000000..f9fded3db4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/update.https.html @@ -0,0 +1,164 @@ +<!DOCTYPE html> +<title>Service Worker: Registration update()</title> +<meta name="timeout" content="long"> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/testharness-helpers.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +const SCOPE = 'resources/simple.txt'; + +// Create a service worker (update-worker.py). The response to update() will be +// different based on the mode. +async function prepare_ready_registration_with_mode(t, mode) { + const key = token(); + const worker_url = `resources/update-worker.py?Key=${key}&Mode=${mode}`; + const expected_url = normalizeURL(worker_url); + const registration = await service_worker_unregister_and_register( + t, worker_url, SCOPE); + await wait_for_state(t, registration.installing, 'activated'); + assert_equals(registration.installing, null, + 'prepare_ready: installing'); + assert_equals(registration.waiting, null, + 'prepare_ready: waiting'); + assert_equals(registration.active.scriptURL, expected_url, + 'prepare_ready: active'); + return [registration, expected_url]; +} + +// Create a service worker (update-worker-from-file.py), which is initially +// |initial_worker| and |updated_worker| later. +async function prepare_ready_registration_with_file( + t, initial_worker, updated_worker) { + const key = token(); + const worker_url = `resources/update-worker-from-file.py?` + + `First=${initial_worker}&Second=${updated_worker}&Key=${key}`; + const expected_url = normalizeURL(worker_url); + + const registration = await service_worker_unregister_and_register( + t, worker_url, SCOPE); + await wait_for_state(t, registration.installing, 'activated'); + assert_equals(registration.installing, null, + 'prepare_ready: installing'); + assert_equals(registration.waiting, null, + 'prepare_ready: waiting'); + assert_equals(registration.active.scriptURL, expected_url, + 'prepare_ready: active'); + return [registration, expected_url]; +} + +function assert_installing_and_active(registration, expected_url) { + assert_equals(registration.installing.scriptURL, expected_url, + 'assert_installing_and_active: installing'); + assert_equals(registration.waiting, null, + 'assert_installing_and_active: waiting'); + assert_equals(registration.active.scriptURL, expected_url, + 'assert_installing_and_active: active'); +} + +function assert_waiting_and_active(registration, expected_url) { + assert_equals(registration.installing, null, + 'assert_waiting_and_active: installing'); + assert_equals(registration.waiting.scriptURL, expected_url, + 'assert_waiting_and_active: waiting'); + assert_equals(registration.active.scriptURL, expected_url, + 'assert_waiting_and_active: active'); +} + +function assert_active_only(registration, expected_url) { + assert_equals(registration.installing, null, + 'assert_active_only: installing'); + assert_equals(registration.waiting, null, + 'assert_active_only: waiting'); + assert_equals(registration.active.scriptURL, expected_url, + 'assert_active_only: active'); +} + +promise_test(async t => { + const [registration, expected_url] = + await prepare_ready_registration_with_mode(t, 'normal'); + t.add_cleanup(() => registration.unregister()); + + await Promise.all([registration.update(), wait_for_update(t, registration)]); + assert_installing_and_active(registration, expected_url); + + await wait_for_state(t, registration.installing, 'installed'); + assert_waiting_and_active(registration, expected_url); + + await wait_for_state(t, registration.waiting, 'activated'); + assert_active_only(registration, expected_url); +}, 'update() should succeed when new script is available.'); + +promise_test(async t => { + const [registration, expected_url] = + await prepare_ready_registration_with_mode(t, 'bad_mime_type'); + t.add_cleanup(() => registration.unregister()); + + await promise_rejects_dom(t, 'SecurityError', registration.update()); + assert_active_only(registration, expected_url); +}, 'update() should fail when mime type is invalid.'); + +promise_test(async t => { + const [registration, expected_url] = + await prepare_ready_registration_with_mode(t, 'redirect'); + t.add_cleanup(() => registration.unregister()); + + await promise_rejects_js(t, TypeError, registration.update()); + assert_active_only(registration, expected_url); +}, 'update() should fail when a response for the main script is redirect.'); + +promise_test(async t => { + const [registration, expected_url] = + await prepare_ready_registration_with_mode(t, 'syntax_error'); + t.add_cleanup(() => registration.unregister()); + + await promise_rejects_js(t, TypeError, registration.update()); + assert_active_only(registration, expected_url); +}, 'update() should fail when a new script contains a syntax error.'); + +promise_test(async t => { + const [registration, expected_url] = + await prepare_ready_registration_with_mode(t, 'throw_install'); + t.add_cleanup(() => registration.unregister()); + + await Promise.all([registration.update(), wait_for_update(t, registration)]); + assert_installing_and_active(registration, expected_url); +}, 'update() should resolve when the install event throws.'); + +promise_test(async t => { + const [registration, expected_url] = + await prepare_ready_registration_with_mode(t, 'normal'); + t.add_cleanup(() => registration.unregister()); + + // We need to hold a client alive so that unregister() below doesn't remove + // the registration before update() has had a chance to look at the pending + // uninstall flag. + const frame = await with_iframe(SCOPE); + t.add_cleanup(() => frame.remove()); + + await promise_rejects_js( + t, TypeError, + Promise.all([registration.unregister(), registration.update()])); +}, 'update() should fail when the pending uninstall flag is set.'); + +promise_test(async t => { + const [registration, expected_url] = + await prepare_ready_registration_with_file( + t, + 'update-smaller-body-before-update-worker.js', + 'update-smaller-body-after-update-worker.js'); + t.add_cleanup(() => registration.unregister()); + + await Promise.all([registration.update(), wait_for_update(t, registration)]); + assert_installing_and_active(registration, expected_url); + + await wait_for_state(t, registration.installing, 'installed'); + assert_waiting_and_active(registration, expected_url); + + await wait_for_state(t, registration.waiting, 'activated'); + assert_active_only(registration, expected_url); +}, 'update() should succeed when the script shrinks.'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/waiting.https.html b/testing/web-platform/tests/service-workers/service-worker/waiting.https.html new file mode 100644 index 0000000000..499e581eb3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/waiting.https.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<title>ServiceWorker: navigator.serviceWorker.waiting</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +const SCRIPT = 'resources/empty-worker.js'; +const SCOPE = 'resources/blank.html'; + +promise_test(async t => { + + t.add_cleanup(async() => { + if (frame) + frame.remove(); + if (registration) + await registration.unregister(); + }); + + await service_worker_unregister(t, SCOPE); + const frame = await with_iframe(SCOPE); + const registration = + await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE}); + await wait_for_state(t, registration.installing, 'installed'); + const controller = frame.contentWindow.navigator.serviceWorker.controller; + assert_equals(controller, null, 'controller'); + assert_equals(registration.active, null, 'registration.active'); + assert_equals(registration.waiting.state, 'installed', + 'registration.waiting'); + assert_equals(registration.installing, null, 'registration.installing'); +}, 'waiting is set after installation'); + +// Tests that the ServiceWorker objects returned from waiting attribute getter +// that represent the same service worker are the same objects. +promise_test(async t => { + const registration1 = + await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + const registration2 = await navigator.serviceWorker.getRegistration(SCOPE); + assert_equals(registration1.waiting, registration2.waiting, + 'ServiceWorkerRegistration.waiting should return the same ' + + 'object'); + await registration1.unregister(); +}, 'The ServiceWorker objects returned from waiting attribute getter that ' + + 'represent the same service worker are the same objects'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/websocket-in-service-worker.https.html b/testing/web-platform/tests/service-workers/service-worker/websocket-in-service-worker.https.html new file mode 100644 index 0000000000..cda9d6fe67 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/websocket-in-service-worker.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<title>Service Worker: WebSockets can be created in a Service Worker</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +promise_test(t => { + const SCRIPT = 'resources/websocket-worker.js?pipe=sub'; + const SCOPE = 'resources/blank.html'; + let registration; + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(r => { + add_completion_callback(() => { r.unregister(); }); + registration = r; + return wait_for_state(t, r.installing, 'activated'); + }) + .then(() => { + return new Promise(resolve => { + navigator.serviceWorker.onmessage = t.step_func(msg => { + assert_equals(msg.data, 'PASS'); + resolve(); + }); + registration.active.postMessage({}); + }); + }); + }, 'Verify WebSockets can be created in a Service Worker'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/websocket.https.html b/testing/web-platform/tests/service-workers/service-worker/websocket.https.html new file mode 100644 index 0000000000..cbfed456a9 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/websocket.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<title>Service Worker: WebSocket handshake channel is not intercepted</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> + +promise_test(function(t) { + var path = new URL(".", window.location).pathname + var url = 'resources/websocket.js'; + var scope = 'resources/blank.html?websocket'; + var host_info = get_host_info(); + var frameURL = host_info['HTTPS_ORIGIN'] + path + scope; + var frame; + + return service_worker_unregister_and_register(t, url, scope) + .then(function(registration) { + t.add_cleanup(function() { + return service_worker_unregister(t, scope); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(frameURL); }) + .then(function(f) { + frame = f; + return websocket(t, frame); + }) + .then(function() { + var channel = new MessageChannel(); + return new Promise(function(resolve) { + channel.port1.onmessage = resolve; + frame.contentWindow.navigator.serviceWorker.controller.postMessage({port: channel.port2}, [channel.port2]); + }); + }) + .then(function(e) { + for (var url in e.data.urls) { + assert_equals(url.indexOf(get_websocket_url()), -1, + "Observed an unexpected FetchEvent for the WebSocket handshake"); + } + frame.remove(); + }); + }, 'Verify WebSocket handshake channel does not get intercepted'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/webvtt-cross-origin.https.html b/testing/web-platform/tests/service-workers/service-worker/webvtt-cross-origin.https.html new file mode 100644 index 0000000000..9394ff75c4 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/webvtt-cross-origin.https.html @@ -0,0 +1,175 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>cross-origin webvtt returned by service worker is detected</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<body> +<script> +// This file tests responses for WebVTT text track from a service worker. It +// creates an iframe with a <track> element, controlled by a service worker. +// Each test tries to load a text track, the service worker intercepts the +// requests and responds with opaque or non-opaque responses. As the +// crossorigin attribute is not set, request's mode is always "same-origin", +// and as specified in https://fetch.spec.whatwg.org/#http-fetch, +// a response from a service worker whose type is neither "basic" nor +// "default" is rejected. + +const host_info = get_host_info(); +const kScript = 'resources/fetch-rewrite-worker.js'; +// Add '?ignore' so the service worker falls back for the navigation. +const kScope = 'resources/vtt-frame.html?ignore'; +let frame; + +function load_track(url) { + const track = frame.contentDocument.querySelector('track'); + const result = new Promise((resolve, reject) => { + track.onload = (e => { + resolve('load event'); + }); + track.onerror = (e => { + resolve('error event'); + }); + }); + + track.src = url; + // Setting mode to hidden seems needed, or else the text track requests don't + // occur. + track.track.mode = 'hidden'; + return result; +} + +promise_test(t => { + return service_worker_unregister_and_register(t, kScript, kScope) + .then(registration => { + promise_test(() => { + frame.remove(); + return registration.unregister(); + }, 'restore global state'); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { + return with_iframe(kScope); + }) + .then(f => { + frame = f; + }) + }, 'initialize global state'); + +promise_test(t => { + let url = '/media/foo.vtt'; + // Add '?url' and tell the service worker to fetch a same-origin URL. + url += '?url=' + host_info.HTTPS_ORIGIN + '/media/foo.vtt'; + return load_track(url) + .then(result => { + assert_equals(result, 'load event'); + }); + }, 'same-origin text track should load'); + +promise_test(t => { + let url = '/media/foo.vtt'; + // Add '?url' and tell the service worker to fetch a cross-origin URL. + url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + '/media/foo.vtt'; + return load_track(url) + .then(result => { + assert_equals(result, 'error event'); + }); + }, 'cross-origin text track with no-cors request should not load'); + +promise_test(t => { + let url = '/media/foo.vtt'; + // Add '?url' and tell the service worker to fetch a cross-origin URL that + // doesn't support CORS. + url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + + '/media/foo-no-cors.vtt'; + // Add '&mode' to tell the service worker to do a CORS request. + url += '&mode=cors'; + return load_track(url) + .then(result => { + assert_equals(result, 'error event'); + }); + }, 'cross-origin text track with rejected cors request should not load'); + +promise_test(t => { + let url = '/media/foo.vtt'; + // Add '?url' and tell the service worker to fetch a cross-origin URL. + url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + '/media/foo.vtt'; + // Add '&mode' to tell the service worker to do a CORS request. + url += '&mode=cors'; + // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so + // that CORS will succeed if the service approves it. + url += '&credentials=same-origin'; + return load_track(url) + .then(result => { + assert_equals(result, 'error event'); + }); + }, 'cross-origin text track with approved cors request should not load'); + +// Redirect tests. + +promise_test(t => { + let url = '/media/foo.vtt'; + // Add '?url' and tell the service worker to fetch a same-origin URL that redirects... + redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect='; + // ... to a same-origin URL. + redirect_target = host_info.HTTPS_ORIGIN + '/media/foo.vtt'; + url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target)); + return load_track(url) + .then(result => { + assert_equals(result, 'load event'); + }); + }, 'same-origin text track that redirects same-origin should load'); + +promise_test(t => { + let url = '/media/foo.vtt'; + // Add '?url' and tell the service worker to fetch a same-origin URL that redirects... + redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect='; + // ... to a cross-origin URL. + redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo.vtt'; + url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target)); + return load_track(url) + .then(result => { + assert_equals(result, 'error event'); + }); + }, 'same-origin text track that redirects cross-origin should not load'); + + +promise_test(t => { + let url = '/media/foo.vtt'; + // Add '?url' and tell the service worker to fetch a same-origin URL that redirects... + redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect='; + // ... to a cross-origin URL. + redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo-no-cors.vtt'; + url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target)); + // Add '&mode' to tell the service worker to do a CORS request. + url += '&mode=cors'; + // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so + // that CORS will succeed if the server approves it. + url += '&credentials=same-origin'; + return load_track(url) + .then(result => { + assert_equals(result, 'error event'); + }); + }, 'same-origin text track that redirects to a cross-origin text track with rejected cors should not load'); + +promise_test(t => { + let url = '/media/foo.vtt'; + // Add '?url' and tell the service worker to fetch a same-origin URL that redirects... + redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect='; + // ... to a cross-origin URL. + redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo.vtt'; + url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target)); + // Add '&mode' to tell the service worker to do a CORS request. + url += '&mode=cors'; + // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so + // that CORS will succeed if the server approves it. + url += '&credentials=same-origin'; + return load_track(url) + .then(result => { + assert_equals(result, 'error event'); + }); + }, 'same-origin text track that redirects to a cross-origin text track with approved cors should not load'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/windowclient-navigate.https.html b/testing/web-platform/tests/service-workers/service-worker/windowclient-navigate.https.html new file mode 100644 index 0000000000..ad60f78636 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/windowclient-navigate.https.html @@ -0,0 +1,190 @@ +<!DOCTYPE html> +<title>Service Worker: WindowClient.navigate() tests</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +'use strict'; + +const SCOPE = 'resources/blank.html'; +const SCRIPT_URL = 'resources/windowclient-navigate-worker.js'; +const CROSS_ORIGIN_URL = + get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/blank.html'; + +navigateTest({ + description: 'normal', + destUrl: 'blank.html?navigate', + expected: normalizeURL(SCOPE) + '?navigate', +}); + +navigateTest({ + description: 'blank url', + destUrl: '', + expected: normalizeURL(SCRIPT_URL) +}); + +navigateTest({ + description: 'in scope but not controlled test on installing worker', + destUrl: 'blank.html?navigate', + expected: 'TypeError', + waitState: 'installing', +}); + +navigateTest({ + description: 'in scope but not controlled test on active worker', + destUrl: 'blank.html?navigate', + expected: 'TypeError', + controlled: false, +}); + +navigateTest({ + description: 'out of scope', + srcUrl: '/common/blank.html', + destUrl: 'blank.html?navigate', + expected: 'TypeError', +}); + +navigateTest({ + description: 'cross orgin url', + destUrl: CROSS_ORIGIN_URL, + expected: null +}); + +navigateTest({ + description: 'invalid url (http://[example.com])', + destUrl: 'http://[example].com', + expected: 'TypeError' +}); + +navigateTest({ + description: 'invalid url (view-source://example.com)', + destUrl: 'view-source://example.com', + expected: 'TypeError' +}); + +navigateTest({ + description: 'invalid url (file:///)', + destUrl: 'file:///', + expected: 'TypeError' +}); + +navigateTest({ + description: 'invalid url (about:blank)', + destUrl: 'about:blank', + expected: 'TypeError' +}); + +navigateTest({ + description: 'navigate on a top-level window client', + destUrl: 'blank.html?navigate', + srcUrl: 'resources/loaded.html', + scope: 'resources/loaded.html', + expected: normalizeURL(SCOPE) + '?navigate', + frameType: 'top-level' +}); + +async function createFrame(t, parameters) { + if (parameters.frameType === 'top-level') { + // Wait for window.open is completed. + await new Promise(resolve => { + const win = window.open(parameters.srcUrl); + t.add_cleanup(() => win.close()); + window.addEventListener('message', (e) => { + if (e.data.type === 'LOADED') { + resolve(); + } + }); + }); + } + + if (parameters.frameType === 'nested') { + const frame = await with_iframe(parameters.srcUrl); + t.add_cleanup(() => frame.remove()); + } +} + +function navigateTest(overrideParameters) { + // default parameters + const parameters = { + description: null, + srcUrl: SCOPE, + destUrl: null, + expected: null, + waitState: 'activated', + scope: SCOPE, + controlled: true, + // `frameType` can be 'nested' for an iframe WindowClient or 'top-level' for + // a main frame WindowClient. + frameType: 'nested' + }; + + for (const key in overrideParameters) + parameters[key] = overrideParameters[key]; + + promise_test(async function(t) { + let pausedLifecyclePort; + let scriptUrl = SCRIPT_URL; + + // For in-scope-but-not-controlled test on installing worker, + // if the waitState is "installing", then append the query to scriptUrl. + if (parameters.waitState === 'installing') { + scriptUrl += '?' + parameters.waitState; + + navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data.port) { + pausedLifecyclePort = event.data.port; + } + }); + } + + t.add_cleanup(() => { + // Some tests require that the worker remain in a given lifecycle phase. + // "Clean up" logic for these tests requires signaling the worker to + // release the hold; this allows the worker to be properly discarded + // prior to the execution of additional tests. + if (pausedLifecyclePort) { + // The value of the posted message is inconsequential. A string is + // specified here solely to aid in test debugging. + pausedLifecyclePort.postMessage('continue lifecycle'); + } + }); + + // Create a frame that is not controlled by a service worker. + if (!parameters.controlled) { + await createFrame(t, parameters); + } + + const registration = await service_worker_unregister_and_register( + t, scriptUrl, parameters.scope); + const serviceWorker = registration.installing; + await wait_for_state(t, serviceWorker, parameters.waitState); + t.add_cleanup(() => registration.unregister()); + + // Create a frame after a service worker is registered so that the frmae is + // controlled by an active service worker. + if (parameters.controlled) { + await createFrame(t, parameters); + } + + const response = await new Promise(resolve => { + const channel = new MessageChannel(); + channel.port1.onmessage = t.step_func(resolve); + serviceWorker.postMessage({ + port: channel.port2, + url: parameters.destUrl, + clientUrl: new URL(parameters.srcUrl, location).toString(), + frameType: parameters.frameType, + expected: parameters.expected, + description: parameters.description, + }, [channel.port2]); + }); + + assert_equals(response.data, null); + await fetch_tests_from_worker(serviceWorker); + }, parameters.description); +} +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/worker-client-id.https.html b/testing/web-platform/tests/service-workers/service-worker/worker-client-id.https.html new file mode 100644 index 0000000000..4e4d31660b --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/worker-client-id.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<title>Service Worker: Workers should have their own unique client Id</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +// Get the iframe client ID by calling postMessage() on its controlling +// worker. This will cause the service worker to post back the +// MessageEvent.source.id value. +function getFrameClientId(frame) { + return new Promise(resolve => { + let mc = new MessageChannel(); + frame.contentWindow.navigator.serviceWorker.controller.postMessage( + 'echo-client-id', [mc.port2]); + mc.port1.onmessage = evt => { + resolve(evt.data); + }; + }); +} + +// Get the worker client ID by creating a worker that performs an intercepted +// fetch(). The synthetic fetch() response will contain the FetchEvent.clientId +// value. This is then posted back to here. +function getWorkerClientId(frame) { + return new Promise(resolve => { + let w = new frame.contentWindow.Worker('worker-echo-client-id.js'); + w.onmessage = evt => { + resolve(evt.data); + }; + }); +} + +promise_test(async function(t) { + const script = './resources/worker-client-id-worker.js'; + const scope = './resources/worker-client-id'; + const frame = scope + '/frame.html'; + + let reg = await navigator.serviceWorker.register(script, { scope }); + t.add_cleanup(async _ => await reg.unregister()); + await wait_for_state(t, reg.installing, 'activated'); + + let f = await with_iframe(frame); + t.add_cleanup(_ => f.remove()); + + let frameClientId = await getFrameClientId(f); + assert_not_equals(frameClientId, null, 'frame client id should exist'); + + let workerClientId = await getWorkerClientId(f); + assert_not_equals(workerClientId, null, 'worker client id should exist'); + + assert_not_equals(frameClientId, workerClientId, + 'frame and worker client ids should be different'); +}, 'Verify workers have a unique client id separate from their owning documents window'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html b/testing/web-platform/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html new file mode 100644 index 0000000000..c8480bf1be --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html @@ -0,0 +1,132 @@ +<!DOCTYPE html> +<title>ServiceWorker FetchEvent issued from workers in an iframe sandboxed via CSP HTTP response header.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +let lastCallbackId = 0; +let callbacks = {}; +function doTest(frame, type) { + return new Promise(function(resolve) { + var id = ++lastCallbackId; + callbacks[id] = resolve; + frame.contentWindow.postMessage({id: id, type: type}, '*'); + }); +} + +// Asks the service worker for data about requests and clients seen. The +// worker posts a message back with |data| where: +// |data.requests|: the requests the worker received FetchEvents for +// |data.clients|: the URLs of all the worker's clients +// The worker clears its data after responding. +function getResultsFromWorker(worker) { + return new Promise(resolve => { + let channel = new MessageChannel(); + channel.port1.onmessage = msg => { + resolve(msg.data); + }; + worker.postMessage({port: channel.port2}, [channel.port2]); + }); +} + +window.onmessage = function (e) { + message = e.data; + let id = message['id']; + let callback = callbacks[id]; + delete callbacks[id]; + callback(message['result']); +}; + +const SCOPE = 'resources/sandboxed-iframe-fetch-event-iframe.py'; +const SCRIPT = 'resources/sandboxed-iframe-fetch-event-worker.js'; +const expected_base_url = new URL(SCOPE, location.href); +// A service worker controlling |SCOPE|. +let worker; +// An iframe whose response header has +// 'Content-Security-Policy: allow-scripts'. +// This should NOT be controlled by a service worker. +let sandboxed_frame_by_header; +// An iframe whose response header has +// 'Content-Security-Policy: allow-scripts allow-same-origin'. +// This should be controlled by a service worker. +let sandboxed_same_origin_frame_by_header; + +promise_test(t => { + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then(function(registration) { + add_completion_callback(() => registration.unregister()); + worker = registration.installing; + return wait_for_state(t, registration.installing, 'activated'); + }); +}, 'Prepare a service worker.'); + +promise_test(t => { + const iframe_full_url = expected_base_url + '?sandbox=allow-scripts&' + + 'sandboxed-frame-by-header'; + return with_iframe(iframe_full_url) + .then(f => { + sandboxed_frame_by_header = f; + add_completion_callback(() => f.remove()); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'Service worker should provide the response'); + assert_equals(requests[0], iframe_full_url); + assert_false(data.clients.includes(iframe_full_url), + 'Service worker should NOT control the sandboxed page'); + }); +}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts.'); + +promise_test(t => { + const iframe_full_url = + expected_base_url + '?sandbox=allow-scripts%20allow-same-origin&' + + 'sandboxed-iframe-same-origin-by-header'; + return with_iframe(iframe_full_url) + .then(f => { + sandboxed_same_origin_frame_by_header = f; + add_completion_callback(() => f.remove()); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1); + assert_equals(requests[0], iframe_full_url); + assert_true(data.clients.includes(iframe_full_url)); + }) +}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts and ' + + 'allow-same-origin.'); + +promise_test(t => { + let frame = sandboxed_frame_by_header; + return doTest(frame, 'fetch-from-worker') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + assert_equals(data.requests.length, 0, + 'The request should NOT be handled by SW.'); + }); +}, 'Fetch request from a worker in iframe sandboxed by CSP HTTP header ' + + 'allow-scripts flag'); + +promise_test(t => { + let frame = sandboxed_same_origin_frame_by_header; + return doTest(frame, 'fetch-from-worker') + .then(result => { + assert_equals(result, 'done'); + return getResultsFromWorker(worker); + }) + .then(data => { + let requests = data.requests; + assert_equals(requests.length, 1, + 'The request should be handled by SW.'); + assert_equals(requests[0], frame.src + '&test=fetch-from-worker'); + }); +}, 'Fetch request from a worker in iframe sandboxed by CSP HTTP header ' + + 'with allow-scripts and allow-same-origin flag'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/worker-interception-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/worker-interception-redirect.https.html new file mode 100644 index 0000000000..8d566b9c15 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/worker-interception-redirect.https.html @@ -0,0 +1,212 @@ +<!DOCTYPE html> +<title>Service Worker: controlling Worker/SharedWorker</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> +// This tests service worker interception for worker clients, when the request +// for the worker script goes through redirects. For example, a request can go +// through a chain of URLs like A -> B -> C -> D and each URL might fall in the +// scope of a different service worker, if any. +// The two key questions are: +// 1. Upon a redirect from A -> B, should a service worker for scope B +// intercept the request? +// 2. After the final response, which service worker controls the resulting +// client? +// +// The standard prescribes the following: +// 1. The service worker for scope B intercepts the redirect. *However*, once a +// request falls back to network (i.e., a service worker did not call +// respondWith()) and a redirect is then received from network, no service +// worker should intercept that redirect or any subsequent redirects. +// 2. The final service worker that got a fetch event (or would have, in the +// case of a non-fetch-event worker) becomes the controller of the client. +// +// The standard may change later, see: +// https://github.com/w3c/ServiceWorker/issues/1289 +// +// The basic test setup is: +// 1. Page registers service workers for scope1 and scope2. +// 2. Page requests a worker from scope1. +// 3. The request is redirected to scope2 or out-of-scope. +// 4. The worker posts message to the page describing where the final response +// was served from (service worker or network). +// 5. The worker does an importScripts() and fetch(), and posts back the +// responses, which describe where the responses where served from. + +// Globals for easier cleanup. +const scope1 = 'resources/scope1'; +const scope2 = 'resources/scope2'; +let frame; + +function get_message_from_worker(port) { + return new Promise(resolve => { + port.onmessage = evt => { + resolve(evt.data); + } + }); +} + +async function cleanup() { + if (frame) + frame.remove(); + + const reg1 = await navigator.serviceWorker.getRegistration(scope1); + if (reg1) + await reg1.unregister(); + const reg2 = await navigator.serviceWorker.getRegistration(scope2); + if (reg2) + await reg2.unregister(); +} + +// Builds the worker script URL, which encodes information about where +// to redirect to. The URL falls in sw1's scope. +// +// - |redirector| is "network" or "serviceworker". If "serviceworker", sw1 will +// respondWith() a redirect. Otherwise, it falls back to network and the server +// responds with a redirect. +// - |redirect_location| is "scope2" or "out-of-scope". If "scope2", the +// redirect ends up in sw2's scope2. Otherwise it's out of scope. +function build_worker_url(redirector, redirect_location) { + let redirect_path; + // Set path to redirect.py, a file on the server that serves + // a redirect. When sw1 sees this URL, it falls back to network. + if (redirector == 'network') + redirector_path = 'redirect.py'; + // Set path to 'sw-redirect', to tell the service worker + // to respond with redirect. + else if (redirector == 'serviceworker') + redirector_path = 'sw-redirect'; + + let redirect_to = base_path() + 'resources/'; + // Append "scope2/" to redirect_to, so the redirect falls in scope2. + // Otherwise no change is needed, as the parent "resources/" directory is + // used, and is out-of-scope. + if (redirect_location == 'scope2') + redirect_to += 'scope2/'; + // Append the name of the file which serves the worker script. + redirect_to += 'worker_interception_redirect_webworker.py'; + + return `scope1/${redirector_path}?Redirect=${redirect_to}` +} + +promise_test(async t => { + await cleanup(); + const service_worker = 'resources/worker-interception-redirect-serviceworker.js'; + const registration1 = await navigator.serviceWorker.register(service_worker, {scope: scope1}); + await wait_for_state(t, registration1.installing, 'activated'); + const registration2 = await navigator.serviceWorker.register(service_worker, {scope: scope2}); + await wait_for_state(t, registration2.installing, 'activated'); + + promise_test(t => { + return cleanup(); + }, 'cleanup global state'); +}, 'initialize global state'); + +async function worker_redirect_test(worker_request_url, + worker_expected_url, + expected_main_resource_message, + expected_import_scripts_message, + expected_fetch_message, + description) { + for (const workerType of ['DedicatedWorker', 'SharedWorker']) { + for (const type of ['classic', 'module']) { + promise_test(async t => { + // Create a frame to load the worker from. This way we can remove the + // frame to destroy the worker client when the test is done. + frame = await with_iframe('resources/blank.html'); + t.add_cleanup(() => { frame.remove(); }); + + // Start the worker. + let w; + let port; + if (workerType === 'DedicatedWorker') { + w = new frame.contentWindow.Worker(worker_request_url, {type}); + port = w; + } else { + w = new frame.contentWindow.SharedWorker(worker_request_url, {type}); + port = w.port; + w.port.start(); + } + w.onerror = t.unreached_func('Worker error'); + + // Expect a message from the worker indicating which service worker + // provided the response for the worker script request, if any. + const data = await get_message_from_worker(port); + + // The worker does an importScripts(). Expect a message from the worker + // indicating which service worker provided the response for the + // importScripts(), if any. + const import_scripts_message = await get_message_from_worker(port); + test(() => { + if (type === 'classic') { + assert_equals(import_scripts_message, + expected_import_scripts_message); + } else { + assert_equals(import_scripts_message, 'importScripts failed'); + } + }, `${description} (${type} ${workerType}, importScripts())`); + + // The worker does a fetch(). Expect a message from the worker + // indicating which service worker provided the response for the + // fetch(), if any. + const fetch_message = await get_message_from_worker(port); + test(() => { + assert_equals(fetch_message, expected_fetch_message); + }, `${description} (${type} ${workerType}, fetch())`); + + // Expect a message from the worker indicating |self.location|. + const worker_actual_url = await get_message_from_worker(port); + test(() => { + assert_equals( + worker_actual_url, + (new URL(worker_expected_url, location.href)).toString(), + 'location.href'); + }, `${description} (${type} ${workerType}, location.href)`); + + assert_equals(data, expected_main_resource_message); + + }, `${description} (${type} ${workerType})`); + } + } +} + +// request to sw1 scope gets network redirect to sw2 scope +worker_redirect_test( + build_worker_url('network', 'scope2'), + 'resources/scope2/worker_interception_redirect_webworker.py', + 'the worker script was served from network', + 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/scope2/import-scripts-echo.py', + 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/scope2/simple.txt', + 'Case #1: network scope1->scope2'); + +// request to sw1 scope gets network redirect to out-of-scope +worker_redirect_test( + build_worker_url('network', 'out-scope'), + 'resources/worker_interception_redirect_webworker.py', + 'the worker script was served from network', + 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py', + 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt', + 'Case #2: network scope1->out-scope'); + +// request to sw1 scope gets service-worker redirect to sw2 scope +worker_redirect_test( + build_worker_url('serviceworker', 'scope2'), + 'resources/subdir/worker_interception_redirect_webworker.py?greeting=sw2%20saw%20the%20request%20for%20the%20worker%20script', + 'sw2 saw the request for the worker script', + 'sw2 saw importScripts from the worker: /service-workers/service-worker/resources/subdir/import-scripts-echo.py', + 'fetch(): sw2 saw the fetch from the worker: /service-workers/service-worker/resources/subdir/simple.txt', + 'Case #3: sw scope1->scope2'); + +// request to sw1 scope gets service-worker redirect to out-of-scope +worker_redirect_test( + build_worker_url('serviceworker', 'out-scope'), + 'resources/worker_interception_redirect_webworker.py', + 'the worker script was served from network', + 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py', + 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt', + 'Case #4: sw scope1->out-scope'); +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/worker-interception.https.html b/testing/web-platform/tests/service-workers/service-worker/worker-interception.https.html new file mode 100644 index 0000000000..27983d8352 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/worker-interception.https.html @@ -0,0 +1,244 @@ +<!DOCTYPE html> +<title>Service Worker: intercepting Worker script loads</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<body> +<script> + +// ========== Worker main resource interception tests ========== + +async function setup_service_worker(t, service_worker_url, scope) { + const r = await service_worker_unregister_and_register( + t, service_worker_url, scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + await wait_for_state(t, r.installing, 'activated'); + return r.active; +} + +promise_test(async t => { + const worker_url = 'resources/sample-synthesized-worker.js?dedicated'; + const service_worker_url = 'resources/sample-worker-interceptor.js'; + const scope = worker_url; + + const serviceWorker = await setup_service_worker(t, service_worker_url, scope); + + const channels = new MessageChannel(); + serviceWorker.postMessage({port: channels.port1}, [channels.port1]); + + const clientId = await new Promise(resolve => channels.port2.onmessage = (e) => resolve(e.data.id)); + + const resultPromise = new Promise(resolve => channels.port2.onmessage = (e) => resolve(e.data)); + + const w = new Worker(worker_url); + const data = await new Promise((resolve, reject) => { + w.onmessage = e => resolve(e.data); + w.onerror = e => reject(e.message); + }); + assert_equals(data, 'worker loading intercepted by service worker'); + + const results = await resultPromise; + assert_equals(results.clientId, clientId); + assert_true(!!results.resultingClientId.length); + + channels.port2.postMessage("done"); +}, `Verify a dedicated worker script request gets correct client Ids`); + +promise_test(async t => { + const worker_url = 'resources/sample-synthesized-worker.js?dedicated'; + const service_worker_url = 'resources/sample-worker-interceptor.js'; + const scope = worker_url; + + await setup_service_worker(t, service_worker_url, scope); + const w = new Worker(worker_url); + const data = await new Promise((resolve, reject) => { + w.onmessage = e => resolve(e.data); + w.onerror = e => reject(e.message); + }); + assert_equals(data, 'worker loading intercepted by service worker'); +}, `Verify a dedicated worker script request issued from a uncontrolled ` + + `document is intercepted by worker's own service worker.`); + +promise_test(async t => { + const frame_url = 'resources/create-out-of-scope-worker.html'; + const service_worker_url = 'resources/sample-worker-interceptor.js'; + const scope = frame_url; + + const registration = await service_worker_unregister_and_register( + t, service_worker_url, scope); + t.add_cleanup(() => service_worker_unregister(t, scope)); + await wait_for_state(t, registration.installing, 'activated'); + + const frame = await with_iframe(frame_url); + t.add_cleanup(_ => frame.remove()); + + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller.scriptURL, + get_newest_worker(registration).scriptURL, + 'the frame should be controlled by a service worker' + ); + + const result = await frame.contentWindow.getWorkerPromise(); + + assert_equals(result, + 'worker loading was not intercepted by service worker'); +}, `Verify an out-of-scope dedicated worker script request issued from a ` + + `controlled document should not be intercepted by document's service ` + + `worker.`); + +promise_test(async t => { + const worker_url = 'resources/sample-synthesized-worker.js?shared'; + const service_worker_url = 'resources/sample-worker-interceptor.js'; + const scope = worker_url; + + await setup_service_worker(t, service_worker_url, scope); + const w = new SharedWorker(worker_url); + const data = await new Promise((resolve, reject) => { + w.port.onmessage = e => resolve(e.data); + w.onerror = e => reject(e.message); + }); + assert_equals(data, 'worker loading intercepted by service worker'); +}, `Verify a shared worker script request issued from a uncontrolled ` + + `document is intercepted by worker's own service worker.`); + +promise_test(async t => { + const worker_url = 'resources/sample-same-origin-worker.js?dedicated'; + const service_worker_url = 'resources/sample-worker-interceptor.js'; + const scope = worker_url; + + await setup_service_worker(t, service_worker_url, scope); + const w = new Worker(worker_url); + const data = await new Promise((resolve, reject) => { + w.onmessage = e => resolve(e.data); + w.onerror = e => reject(e.message); + }); + assert_equals(data, 'dedicated worker script loaded'); +}, 'Verify a same-origin worker script served by a service worker succeeds ' + + 'in starting a dedicated worker.'); + +promise_test(async t => { + const worker_url = 'resources/sample-same-origin-worker.js?shared'; + const service_worker_url = 'resources/sample-worker-interceptor.js'; + const scope = worker_url; + + await setup_service_worker(t, service_worker_url, scope); + const w = new SharedWorker(worker_url); + const data = await new Promise((resolve, reject) => { + w.port.onmessage = e => resolve(e.data); + w.onerror = e => reject(e.message); + }); + assert_equals(data, 'shared worker script loaded'); +}, 'Verify a same-origin worker script served by a service worker succeeds ' + + 'in starting a shared worker.'); + +promise_test(async t => { + const worker_url = 'resources/sample-cors-worker.js?dedicated'; + const service_worker_url = 'resources/sample-worker-interceptor.js'; + const scope = worker_url; + + await setup_service_worker(t, service_worker_url, scope); + const w = new Worker(worker_url); + const watcher = new EventWatcher(t, w, ['message', 'error']); + await watcher.wait_for('error'); +}, 'Verify a cors worker script served by a service worker fails dedicated ' + + 'worker start.'); + +promise_test(async t => { + const worker_url = 'resources/sample-cors-worker.js?shared'; + const service_worker_url = 'resources/sample-worker-interceptor.js'; + const scope = worker_url; + + await setup_service_worker(t, service_worker_url, scope); + const w = new SharedWorker(worker_url); + const watcher = new EventWatcher(t, w, ['message', 'error']); + await watcher.wait_for('error'); +}, 'Verify a cors worker script served by a service worker fails shared ' + + 'worker start.'); + +promise_test(async t => { + const worker_url = 'resources/sample-no-cors-worker.js?dedicated'; + const service_worker_url = 'resources/sample-worker-interceptor.js'; + const scope = worker_url; + + await setup_service_worker(t, service_worker_url, scope); + const w = new Worker(worker_url); + const watcher = new EventWatcher(t, w, ['message', 'error']); + await watcher.wait_for('error'); +}, 'Verify a no-cors cross-origin worker script served by a service worker ' + + 'fails dedicated worker start.'); + +promise_test(async t => { + const worker_url = 'resources/sample-no-cors-worker.js?shared'; + const service_worker_url = 'resources/sample-worker-interceptor.js'; + const scope = worker_url; + + await setup_service_worker(t, service_worker_url, scope); + const w = new SharedWorker(worker_url); + const watcher = new EventWatcher(t, w, ['message', 'error']); + await watcher.wait_for('error'); +}, 'Verify a no-cors cross-origin worker script served by a service worker ' + + 'fails shared worker start.'); + +// ========== Worker subresource interception tests ========== + +const scope_for_subresource_interception = 'resources/load_worker.js'; + +promise_test(async t => { + const service_worker_url = 'resources/worker-load-interceptor.js'; + const r = await service_worker_unregister_and_register( + t, service_worker_url, scope_for_subresource_interception); + await wait_for_state(t, r.installing, 'activated'); +}, 'Register a service worker for worker subresource interception tests.'); + +// Do not call this function multiple times without waiting for the promise +// resolution because this sets new event handlers on |worker|. +// TODO(nhiroki): To isolate multiple function calls, use MessagePort instead of +// worker's onmessage event handler. +async function request_on_worker(worker, resource_type) { + const data = await new Promise((resolve, reject) => { + if (worker instanceof Worker) { + worker.onmessage = e => resolve(e.data); + worker.onerror = e => reject(e); + worker.postMessage(resource_type); + } else if (worker instanceof SharedWorker) { + worker.port.onmessage = e => resolve(e.data); + worker.onerror = e => reject(e); + worker.port.postMessage(resource_type); + } else { + reject('Unexpected worker type!'); + } + }); + assert_equals(data, 'This load was successfully intercepted.'); +} + +async function subresource_test(worker) { + await request_on_worker(worker, 'xhr'); + await request_on_worker(worker, 'fetch'); + await request_on_worker(worker, 'importScripts'); +} + +promise_test(async t => { + await subresource_test(new Worker('resources/load_worker.js')); +}, 'Requests on a dedicated worker controlled by a service worker.'); + +promise_test(async t => { + await subresource_test(new SharedWorker('resources/load_worker.js')); +}, 'Requests on a shared worker controlled by a service worker.'); + +promise_test(async t => { + await subresource_test(new Worker('resources/nested_load_worker.js')); +}, 'Requests on a dedicated worker nested in a dedicated worker and ' + + 'controlled by a service worker'); + +promise_test(async t => { + await subresource_test(new SharedWorker('resources/nested_load_worker.js')); +}, 'Requests on a dedicated worker nested in a shared worker and controlled ' + + 'by a service worker'); + +promise_test(async t => { + await service_worker_unregister(t, scope_for_subresource_interception); +}, 'Unregister a service worker for subresource interception tests.'); + +</script> +</body> diff --git a/testing/web-platform/tests/service-workers/service-worker/xhr-content-length.https.window.js b/testing/web-platform/tests/service-workers/service-worker/xhr-content-length.https.window.js new file mode 100644 index 0000000000..1ae320e9c3 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/xhr-content-length.https.window.js @@ -0,0 +1,55 @@ +// META: script=resources/test-helpers.sub.js + +let frame; + +promise_test(async (t) => { + const scope = "resources/empty.html"; + const script = "resources/xhr-content-length-worker.js"; + const registration = await service_worker_unregister_and_register(t, script, scope); + await wait_for_state(t, registration.installing, "activated"); + frame = await with_iframe(scope); +}, "Setup"); + +promise_test(async t => { + const xhr = new frame.contentWindow.XMLHttpRequest(); + xhr.open("GET", "test?type=no-content-length"); + xhr.send(); + const event = await new Promise(resolve => xhr.onload = resolve); + assert_equals(xhr.getResponseHeader("content-length"), null); + assert_false(event.lengthComputable); + assert_equals(event.total, 0); + assert_equals(event.loaded, xhr.responseText.length); +}, `Synthetic response without Content-Length header`); + +promise_test(async t => { + const xhr = new frame.contentWindow.XMLHttpRequest(); + xhr.open("GET", "test?type=larger-content-length"); + xhr.send(); + const event = await new Promise(resolve => xhr.onload = resolve); + assert_equals(xhr.getResponseHeader("content-length"), "10000"); + assert_true(event.lengthComputable); + assert_equals(event.total, 10000); + assert_equals(event.loaded, xhr.responseText.length); +}, `Synthetic response with Content-Length header with value larger than response body length`); + +promise_test(async t => { + const xhr = new frame.contentWindow.XMLHttpRequest(); + xhr.open("GET", "test?type=double-content-length"); + xhr.send(); + const event = await new Promise(resolve => xhr.onload = resolve); + assert_equals(xhr.getResponseHeader("content-length"), "10000, 10000"); + assert_true(event.lengthComputable); + assert_equals(event.total, 10000); + assert_equals(event.loaded, xhr.responseText.length); +}, `Synthetic response with two Content-Length headers value larger than response body length`); + +promise_test(async t => { + const xhr = new frame.contentWindow.XMLHttpRequest(); + xhr.open("GET", "test?type=bogus-content-length"); + xhr.send(); + const event = await new Promise(resolve => xhr.onload = resolve); + assert_equals(xhr.getResponseHeader("content-length"), "test"); + assert_false(event.lengthComputable); + assert_equals(event.total, 0); + assert_equals(event.loaded, xhr.responseText.length); +}, `Synthetic response with bogus Content-Length header`); diff --git a/testing/web-platform/tests/service-workers/service-worker/xhr-response-url.https.html b/testing/web-platform/tests/service-workers/service-worker/xhr-response-url.https.html new file mode 100644 index 0000000000..673ca52cc6 --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/xhr-response-url.https.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: XHR responseURL uses the response url</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js"></script> +<script> +const scope = 'resources/xhr-iframe.html'; +const script = 'resources/xhr-response-url-worker.js'; +let iframe; + +function build_url(options) { + const url = new URL('test', window.location); + const opts = options ? options : {}; + if (opts.respondWith) + url.searchParams.set('respondWith', opts.respondWith); + if (opts.url) + url.searchParams.set('url', opts.url.href); + return url.href; +} + +promise_test(async (t) => { + const registration = + await service_worker_unregister_and_register(t, script, scope); + await wait_for_state(t, registration.installing, 'activated'); + iframe = await with_iframe(scope); +}, 'global setup'); + +// Test that XMLHttpRequest.responseURL uses the response URL from the service +// worker. +promise_test(async (t) => { + // Build a URL that tells the service worker to respondWith(fetch(|target|)). + const target = new URL('resources/sample.txt', window.location); + const url = build_url({ + respondWith: 'fetch', + url: target + }); + + // Perform the XHR. + const xhr = await iframe.contentWindow.xhr(url); + assert_equals(xhr.responseURL, target.href, 'responseURL'); +}, 'XHR responseURL should be the response URL'); + +// Same as above with a generated response. +promise_test(async (t) => { + // Build a URL that tells the service worker to respondWith(new Response()). + const url = build_url({respondWith: 'string'}); + + // Perform the XHR. + const xhr = await iframe.contentWindow.xhr(url); + assert_equals(xhr.responseURL, url, 'responseURL'); +}, 'XHR responseURL should be the response URL (generated response)'); + +// Test that XMLHttpRequest.responseXML is a Document whose URL is the +// response URL from the service worker. +promise_test(async (t) => { + // Build a URL that tells the service worker to respondWith(fetch(|target|)). + const target = new URL('resources/blank.html', window.location); + const url = build_url({ + respondWith: 'fetch', + url: target + }); + + // Perform the XHR. + const xhr = await iframe.contentWindow.xhr(url, {responseType: 'document'}); + assert_equals(xhr.responseURL, target.href, 'responseURL'); + + // The document's URL uses the response URL: + // "Set |document|’s URL to |response|’s url." + // https://xhr.spec.whatwg.org/#document-response + assert_equals(xhr.responseXML.URL, target.href, 'responseXML.URL'); + + // The document's base URL falls back to the document URL: + // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url + assert_equals(xhr.responseXML.baseURI, target.href, 'responseXML.baseURI'); +}, 'XHR Document should use the response URL'); + +// Same as above with a generated response from the service worker. +promise_test(async (t) => { + // Build a URL that tells the service worker to + // respondWith(new Response()) with a document response. + const url = build_url({respondWith: 'document'}); + + // Perform the XHR. + const xhr = await iframe.contentWindow.xhr(url, {responseType: 'document'}); + assert_equals(xhr.responseURL, url, 'responseURL'); + + // The document's URL uses the response URL, which is the request URL: + // "Set |document|’s URL to |response|’s url." + // https://xhr.spec.whatwg.org/#document-response + assert_equals(xhr.responseXML.URL, url, 'responseXML.URL'); + + // The document's base URL falls back to the document URL: + // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url + assert_equals(xhr.responseXML.baseURI, url, 'responseXML.baseURI'); +}, 'XHR Document should use the response URL (generated response)'); + +promise_test(async (t) => { + if (iframe) + iframe.remove(); + await service_worker_unregister(t, scope); +}, 'global cleanup'); +</script> diff --git a/testing/web-platform/tests/service-workers/service-worker/xsl-base-url.https.html b/testing/web-platform/tests/service-workers/service-worker/xsl-base-url.https.html new file mode 100644 index 0000000000..1d3c36408a --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/xsl-base-url.https.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Service Worker: XSL's base URL must be the response URL</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/test-helpers.sub.js?pipe=sub"></script> +<script> +// This test loads an XML document which is controlled a service worker. The +// document loads a stylesheet and a service worker responds with another URL. +// The stylesheet imports a relative URL to test that the base URL is the +// response URL from the service worker. +promise_test(async (t) => { + const SCOPE = 'resources/xsl-base-url-iframe.xml'; + const SCRIPT = 'resources/xsl-base-url-worker.js'; + let worker; + let frame; + + t.add_cleanup(() => { + if (frame) + frame.remove(); + service_worker_unregister(t, SCOPE); + }); + + const registration = await service_worker_unregister_and_register( + t, SCRIPT, SCOPE); + worker = registration.installing; + await wait_for_state(t, worker, 'activated'); + + frame = await with_iframe(SCOPE); + assert_equals(frame.contentDocument.body.textContent, 'PASS'); +}, 'base URL when service worker does respondWith(fetch(responseUrl))'); +</script> |