Source code

Revision control

Other Tools

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>