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
index cb4ed30a37fd..04e98266b4f1 100644
--- 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
@@ -1,4 +1,5 @@
+
@@ -56,21 +57,25 @@ function msg_event_test(scope, test) {
}
promise_test(msg_event_test.bind(this, 'no-current-extension-different-task'),
- 'Test calling waitUntil in a different task without an existing extension throws');
+ '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 different microtask without an existing extension throws');
+ '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 with an existing extension succeeds');
+ 'Test calling waitUntil in a different task an existing extension succeeds');
-promise_test(msg_event_test.bind(this, 'current-extension-expired-same-microtask-turn'),
- 'Test calling waitUntil with an existing extension promise handler 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)');
-// The promise handler will queue a new microtask after the check for new
-// extensions was performed.
-promise_test(msg_event_test.bind(this, 'current-extension-expired-same-microtask-turn-extra'),
- 'Test calling waitUntil at the end of the microtask turn throws');
+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');
@@ -80,24 +85,36 @@ promise_test(msg_event_test.bind(this, 'script-extendable-event'),
promise_test(function(t) {
var testBody = function(worker) {
- return with_iframe('./resources/pending-respondwith-async-waituntil/dummy.html');
+ 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/respondwith-microtask-sync-waituntil/dummy.html');
+ return with_iframe('./resources/during-event-dispatch-respondwith-microtask-sync-waituntil');
}
- return runTest(t, 'respondwith-microtask-sync-waituntil', testBody);
- }, 'Test calling waitUntil synchronously inside microtask of respondWith promise.');
+ 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/respondwith-microtask-async-waituntil/dummy.html');
+ return with_iframe('./resources/during-event-dispatch-respondwith-microtask-async-waituntil');
}
- return runTest(t, 'respondwith-microtask-async-waituntil', testBody);
- }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise.');
+ 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).');
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
index 87fa04679832..7842a829c9b8 100644
--- 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
@@ -1,36 +1,61 @@
+
+
respondWith cannot be called asynchronously
+
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
index abf54934a3b4..8a975b0d2e9b 100644
--- 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
@@ -1,4 +1,12 @@
-// controlled by 'init'/'done' messages.
+// 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;
@@ -14,34 +22,72 @@ self.addEventListener('message', function(event) {
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('InvalidStateError'));
+ 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;
- case 'current-extension-expired-same-microtask-turn':
+
+ // 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;
- case 'current-extension-expired-same-microtask-turn-extra':
- // The promise handler queues a new microtask *after* the check for new
- // extensions was performed.
+
+ // 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;
@@ -51,25 +97,62 @@ self.addEventListener('message', function(event) {
});
self.addEventListener('fetch', function(event) {
- if (event.request.url.indexOf('pending-respondwith-async-waituntil') != -1) {
+ 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;
- let response = new Promise((res) => { resolveFetch = res; });
+ response = new Promise((res) => { resolveFetch = res; });
event.respondWith(response);
async_task_waituntil(event)
.then(reportResultExpecting('OK'))
.then(() => { resolveFetch(new Response('OK')); });
- } else if (event.request.url.indexOf('respondwith-microtask-sync-waituntil') != -1) {
+ 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'))
- } else if (event.request.url.indexOf('respondwith-microtask-async-waituntil') != -1) {
+ .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'));
@@ -118,3 +201,10 @@ function async_task_waituntil(event) {
}, 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/fetch-event-async-respond-with-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js
index 7f66d20dfc2d..3409d0a0397b 100644
--- 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
@@ -1,19 +1,56 @@
-var result;
+// This worker attempts to call respondWith() asynchronously after the
+// fetch event handler finished. It reports back to the test whether
+// an exception was thrown.
-self.addEventListener('message', function(event) {
- event.data.port.postMessage(result);
+// These get reset at the start of a test case.
+let reportResult;
+let resultPromise;
+
+// 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) => {
+ resultPromise = new Promise((resolve) => {
+ reportResult = resolve;
});
+ // Keep the worker alive until the test case finishes, and report
+ // back the result to the test page.
+ event.waitUntil(resultPromise.then(result => {
+ 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) {
- setTimeout(function() {
- try {
- event.respondWith(new Response());
- result = 'FAIL: did not throw';
- } catch (error) {
- if (error.name == 'InvalidStateError')
- result = 'PASS';
- else
- result = 'FAIL: Unexpected exception: ' + error;
- }
- }, 0);
- });
+ 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);
+ }
+});