| <!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> |