postmessage-to-client-message-queue.https.html (9118B)
1 <!DOCTYPE html> 2 <title>Service Worker: postMessage to Client (message queue)</title> 3 <script src="/resources/testharness.js"></script> 4 <script src="/resources/testharnessreport.js"></script> 5 <script src="/common/get-host-info.sub.js"></script> 6 <script src="resources/test-helpers.sub.js"></script> 7 <script> 8 // This function creates a message listener that captures all messages 9 // sent to this window and matches them with corresponding requests. 10 // This frees test code from having to use clunky constructs just to 11 // avoid race conditions, since the relative order of message and 12 // request arrival doesn't matter. 13 function create_message_listener(t) { 14 const listener = { 15 messages: new Set(), 16 requests: new Set(), 17 waitFor: function(predicate) { 18 for (const event of this.messages) { 19 // If a message satisfying the predicate has already 20 // arrived, it gets matched to this request. 21 if (predicate(event)) { 22 this.messages.delete(event); 23 return Promise.resolve(event); 24 } 25 } 26 27 // If no match was found, the request is stored and a 28 // promise is returned. 29 const request = { predicate }; 30 const promise = new Promise(resolve => request.resolve = resolve); 31 this.requests.add(request); 32 return promise; 33 } 34 }; 35 window.onmessage = t.step_func(event => { 36 for (const request of listener.requests) { 37 // If the new message matches a stored request's 38 // predicate, the request's promise is resolved with this 39 // message. 40 if (request.predicate(event)) { 41 listener.requests.delete(request); 42 request.resolve(event); 43 return; 44 } 45 }; 46 47 // No outstanding request for this message, store it in case 48 // it's requested later. 49 listener.messages.add(event); 50 }); 51 return listener; 52 } 53 54 async function service_worker_register_and_activate(t, script, scope) { 55 const registration = await service_worker_unregister_and_register(t, script, scope); 56 t.add_cleanup(() => registration.unregister()); 57 const worker = registration.installing; 58 await wait_for_state(t, worker, 'activated'); 59 return worker; 60 } 61 62 // Add an iframe (parent) whose document contains a nested iframe 63 // (child), then set the child's src attribute to child_url and return 64 // its Window (without waiting for it to finish loading). 65 async function with_nested_iframes(t, child_url) { 66 const parent = await with_iframe('resources/nested-iframe-parent.html?role=parent'); 67 t.add_cleanup(() => parent.remove()); 68 const child = parent.contentWindow.document.getElementById('child'); 69 child.setAttribute('src', child_url); 70 return child.contentWindow; 71 } 72 73 // Returns a predicate matching a fetch message with the specified 74 // key. 75 function fetch_message(key) { 76 return event => event.data.type === 'fetch' && event.data.key === key; 77 } 78 79 // Returns a predicate matching a ping message with the specified 80 // payload. 81 function ping_message(data) { 82 return event => event.data.type === 'ping' && event.data.data === data; 83 } 84 85 // A client message queue test is a testharness.js test with some 86 // additional setup: 87 // 1. A listener (see create_message_listener) 88 // 2. An active service worker 89 // 3. Two nested iframes 90 // 4. A state transition function that controls the order of events 91 // during the test 92 function client_message_queue_test(url, test_function, description) { 93 promise_test(async t => { 94 t.listener = create_message_listener(t); 95 96 const script = 'resources/stalling-service-worker.js'; 97 const scope = 'resources/'; 98 t.service_worker = await service_worker_register_and_activate(t, script, scope); 99 100 // We create two nested iframes such that both are controlled by 101 // the newly installed service worker. 102 const child_url = url + '?role=child'; 103 t.frame = await with_nested_iframes(t, child_url); 104 105 t.state_transition = async function(from, to, scripts) { 106 // A state transition begins with the child's parser 107 // fetching a script due to a <script> tag. The request 108 // arrives at the service worker, which notifies the 109 // parent, which in turn notifies the test. Note that the 110 // event loop keeps spinning while the parser is waiting. 111 const request = await this.listener.waitFor(fetch_message(to)); 112 113 // The test instructs the service worker to send two ping 114 // messages through the Client interface: first to the 115 // child, then to the parent. 116 this.service_worker.postMessage(from); 117 118 // When the parent receives the ping message, it forwards 119 // it to the test. Assuming that messages to both child 120 // and parent are mapped to the same task queue (this is 121 // not [yet] required by the spec), receiving this message 122 // guarantees that the child has already dispatched its 123 // message if it was allowed to do so. 124 await this.listener.waitFor(ping_message(from)); 125 126 // Finally, reply to the service worker's fetch 127 // notification with the script it should use as the fetch 128 // request's response. This is a defensive mechanism that 129 // ensures the child's parser really is blocked until the 130 // test is ready to continue. 131 request.ports[0].postMessage([`state = '${to}';`].concat(scripts)); 132 }; 133 134 await test_function(t); 135 }, description); 136 } 137 138 function client_message_queue_enable_test( 139 install_script, 140 start_script, 141 earliest_dispatch, 142 description) 143 { 144 function assert_state_less_than_equal(state1, state2, explanation) { 145 const states = ['init', 'install', 'start', 'finish', 'loaded']; 146 const index1 = states.indexOf(state1); 147 const index2 = states.indexOf(state2); 148 if (index1 > index2) 149 assert_unreached(explanation); 150 } 151 152 client_message_queue_test('enable-client-message-queue.html', async t => { 153 // While parsing the child's document, the child transitions 154 // from the 'init' state all the way to the 'finish' state. 155 // Once parsing is finished it would enter the final 'loaded' 156 // state. All but the last transition require assitance from 157 // the test. 158 await t.state_transition('init', 'install', [install_script]); 159 await t.state_transition('install', 'start', [start_script]); 160 await t.state_transition('start', 'finish', []); 161 162 // Wait for all messages to get dispatched on the child's 163 // ServiceWorkerContainer and then verify that each message 164 // was dispatched after |earliest_dispatch|. 165 const report = await t.frame.report; 166 ['init', 'install', 'start'].forEach(state => { 167 const explanation = `Message sent in state '${state}' was dispatched in '${report[state]}', should be dispatched no earlier than '${earliest_dispatch}'`; 168 assert_state_less_than_equal(earliest_dispatch, 169 report[state], 170 explanation); 171 }); 172 }, description); 173 } 174 175 const empty_script = ``; 176 177 const add_event_listener = 178 `navigator.serviceWorker.addEventListener('message', handle_message);`; 179 180 const set_onmessage = `navigator.serviceWorker.onmessage = handle_message;`; 181 182 const start_messages = `navigator.serviceWorker.startMessages();`; 183 184 client_message_queue_enable_test(add_event_listener, empty_script, 'loaded', 185 'Messages from ServiceWorker to Client only received after DOMContentLoaded event.'); 186 187 client_message_queue_enable_test(add_event_listener, start_messages, 'start', 188 'Messages from ServiceWorker to Client only received after calling startMessages().'); 189 190 client_message_queue_enable_test(set_onmessage, empty_script, 'install', 191 'Messages from ServiceWorker to Client only received after setting onmessage.'); 192 193 const resolve_manual_promise = `resolve_manual_promise();` 194 195 async function test_microtasks_when_client_message_queue_enabled(t, scripts) { 196 await t.state_transition('init', 'start', scripts.concat([resolve_manual_promise])); 197 let result = await t.frame.result; 198 assert_equals(result[0], 'microtask', 'The microtask was executed first.'); 199 assert_equals(result[1], 'message', 'The message was dispatched.'); 200 } 201 202 client_message_queue_test('message-vs-microtask.html', t => { 203 return test_microtasks_when_client_message_queue_enabled(t, [ 204 add_event_listener, 205 start_messages, 206 ]); 207 }, 'Microtasks run before dispatching messages after calling startMessages().'); 208 209 client_message_queue_test('message-vs-microtask.html', t => { 210 return test_microtasks_when_client_message_queue_enabled(t, [set_onmessage]); 211 }, 'Microtasks run before dispatching messages after setting onmessage.'); 212 </script>