| <!DOCTYPE html> |
| <title>Service Worker: controlling a SharedWorker</title> |
| <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 a fetch(), and posts back the response, which describes |
| // where the fetch response was served from. |
| // |
| // Currently this only tests shared worker but dedicated worker tests should be |
| // added in a future patch. |
| |
| // Globals for easier cleanup. |
| const scope1 = 'resources/scope1'; |
| const scope2 = 'resources/scope2'; |
| let frame; |
| |
| function get_message_from_worker(worker) { |
| return new Promise(resolve => { |
| worker.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'); |
| |
| function worker_redirect_test(worker_url, |
| expected_main_resource_message, |
| expected_subresource_message, |
| description) { |
| 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. |
| const w = new frame.contentWindow.SharedWorker(worker_url); |
| w.port.start(); |
| |
| // 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(w); |
| assert_equals(data, expected_main_resource_message); |
| |
| // The worker does a fetch() after it starts up. Expect a message from the |
| // worker indicating which service worker provided the response for the |
| // fetch(), if any. |
| // |
| // Note: for some reason, Firefox would pass all these tests if a |
| // postMessage ping/pong step is added before the fetch(). I.e., if the |
| // page does postMessage() and the worker does fetch() in response to the |
| // ping, the fetch() is properly intercepted. See |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=1452528. (Chrome can't pass |
| // the tests either way.) |
| const message = get_message_from_worker(w); |
| const data2 = await message; |
| assert_equals(data2, expected_subresource_message); |
| }, description); |
| } |
| |
| worker_redirect_test( |
| build_worker_url('network', 'scope2'), |
| 'the shared worker script was served from network', |
| 'fetch(): sw1 saw the fetch from the worker', |
| 'request to sw1 scope gets network redirect to sw2 scope'); |
| |
| worker_redirect_test( |
| build_worker_url('network', 'out-scope'), |
| 'the shared worker script was served from network', |
| 'fetch(): sw1 saw the fetch from the worker', |
| 'request to sw1 scope gets network redirect to out-of-scope'); |
| |
| worker_redirect_test( |
| build_worker_url('serviceworker', 'scope2'), |
| 'sw2 saw the request for the worker script', |
| 'fetch(): sw2 saw the fetch from the worker', |
| 'request to sw1 scope gets service-worker redirect to sw2 scope'); |
| |
| worker_redirect_test( |
| build_worker_url('serviceworker', 'out-scope'), |
| 'the shared worker script was served from network', |
| 'fetch(): sw1 saw the fetch from the worker', |
| 'request to sw1 scope gets service-worker redirect to out-of-scope'); |
| </script> |
| </body> |