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); + } +});