Bug 1578919 - Add test for handling of "pending" fetch events r=asuth
authorPerry Jiang <perry@mozilla.com>
Fri, 11 Oct 2019 23:23:20 +0000
changeset 497334 0464faa83b959a5f00f2e6e7951d2818f419c6ac
parent 497333 f62280bf339a179ad3a90348e1f6ff43eb8c6540
child 497335 1c6ab26ee4cf9b1cc437efaa672b43966e15a187
push id36682
push userncsoregi@mozilla.com
push dateSat, 12 Oct 2019 09:52:03 +0000
treeherdermozilla-central@06ea2371f897 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersasuth
bugs1578919
milestone71.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1578919 - Add test for handling of "pending" fetch events r=asuth This patch factors out some of the initial test case in fetch-waits-for-activate.https.html (which only tests "pending" fetch events for a navigation request) to share with a new test case that tests "pending" fetch events for a subresource request with a request body. Both tests in the file have a high-level structure of: 1) Register a Service Worker and wait until its state is "activating" but don't let its state reach "activated". 2) Fire a fetch event that will be controlled by that Service Worker, which should wait until the Service Worker's state advances to "activated". 3) Wait for the fetch to see that the worker isn't "activated". This step isn't directly observable by content, so the test's method to determine this can have false posities (but should never cause the test to unexpectedly fail). 4) Tell the Service Worker to advance to "activated". 5) Verify the fetch that was dispatched while the Service Worker was "activating" is successfully handled. Differential Revision: https://phabricator.services.mozilla.com/D49031
testing/web-platform/tests/service-workers/service-worker/fetch-waits-for-activate.https.html
testing/web-platform/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js
--- a/testing/web-platform/tests/service-workers/service-worker/fetch-waits-for-activate.https.html
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-waits-for-activate.https.html
@@ -4,63 +4,125 @@
 <script src="/resources/testharness.js"></script>
 <script src="resources/testharness-helpers.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>
 <body>
 <script>
 
-var worker = 'resources/fetch-waits-for-activate-worker.js';
-var expected_url = normalizeURL(worker);
-var scope = 'resources/fetch-waits-for-activate/';
+const worker_url = 'resources/fetch-waits-for-activate-worker.js';
+const normalized_worker_url = normalizeURL(worker_url);
+const worker_scope = 'resources/fetch-waits-for-activate/';
+
+// Resolves with the Service Worker's registration once it's reached the
+// "activating" state. (The Service Worker should remain "activating" until
+// explicitly told advance to the "activated" state).
+async function registerAndWaitForActivating(t) {
+  const registration = await service_worker_unregister_and_register(
+      t, worker_url, worker_scope);
+  t.add_cleanup(() => service_worker_unregister(t, worker_scope));
+
+  await wait_for_state(t, registration.installing, 'activating');
+
+  return registration;
+}
 
-promise_test(function(t) {
-  var registration;
-  var frameLoadPromise;
-  var frame;
-  return service_worker_unregister_and_register(t, worker, scope).then(function(reg) {
-    t.add_cleanup(function() {
-      return service_worker_unregister(t, scope);
-    });
+// Attempts to ensure that the "Handle Fetch" algorithm has reached the step
+//
+//   "If activeWorker’s state is "activating", wait for activeWorker’s state to
+//    become "activated"."
+//
+// by waiting for some time to pass.
+//
+// WARNING: whether the algorithm has reached that step isn't directly
+// observable, so this is best effort and can race. Note that this can only
+// result in false positives (where the algorithm hasn't reached that step yet
+// and any functional events haven't actually been handled by the Service
+// Worker).
+async function ensureFunctionalEventsAreWaiting(registration) {
+  await (new Promise(resolve => { setTimeout(resolve, 1000); }));
 
-    registration = reg;
-    return wait_for_state(t, reg.installing, 'activating');
-  }).then(function() {
-    assert_equals(registration.active.scriptURL, expected_url,
-                  'active worker should be present');
-    assert_equals(registration.active.state, 'activating',
-                  'active worker should be in activating state');
+  assert_equals(registration.active.scriptURL, normalized_worker_url,
+                'active worker should be present');
+  assert_equals(registration.active.state, 'activating',
+                'active worker should be in activating state');
+}
+
+promise_test(async t => {
+  const registration = await registerAndWaitForActivating(t);
+
+  let frame = null;
+  t.add_cleanup(() => {
+    if (frame) {
+      frame.remove();
+    }
+  });
+
+  // This should block until we message the worker to tell it to complete
+  // the activate event.
+  const frameLoadPromise = with_iframe(worker_scope).then(function(f) {
+    frame = f;
+  });
+
+  await ensureFunctionalEventsAreWaiting(registration);
+  assert_equals(frame, null, 'frame should not be loaded');
 
-    // This should block until we message the worker to tell it to complete
-    // the activate event.
-    frameLoadPromise = with_iframe(scope).then(function(f) {
-      frame = f;
-    });
+  registration.active.postMessage('ACTIVATE');
+
+  await frameLoadPromise;
+  assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+                normalized_worker_url,
+                'frame should now be loaded and controlled');
+  assert_equals(registration.active.state, 'activated',
+                'active worker should be in activated state');
+}, 'Navigation fetch events should wait for the activate event to complete.');
+
+promise_test(async t => {
+  const frame = await with_iframe(worker_scope);
+  t.add_cleanup(() => { frame.remove(); });
+
+  const registration = await registerAndWaitForActivating(t);
+
+  // Make the Service Worker control the frame so the frame can perform an
+  // intercepted fetch.
+  await (new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      assert_equals(
+        frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+        normalized_worker_url, 'frame should be controlled');
+      resolve();
+    };
+
+    registration.active.postMessage('CLAIM');
+  }));
 
-    // Wait some time to allow frame loading to proceed.  It should not,
-    // however, if the fetch event is blocked on the activate.  I don't
-    // see any way to force this race without a timeout, unfortunately.
-    return new Promise(function(resolve) {
-      setTimeout(resolve, 1000);
-    });
-  }).then(function() {
-    assert_equals(frame, undefined, 'frame should not be loaded');
-    assert_equals(registration.active.scriptURL, expected_url,
-                  'active worker should be present');
-    assert_equals(registration.active.state, 'activating',
-                  'active worker should be in activating state');
+  const fetch_url = `${worker_scope}non/existent/path`;
+  const expected_fetch_result = 'Hello world';
+  let fetch_promise_settled = false;
+
+  // This should block until we message the worker to tell it to complete
+  // the activate event.
+  const fetchPromise = frame.contentWindow.fetch(fetch_url, {
+    method: 'POST',
+    body: expected_fetch_result,
+  }).then(response => {
+    fetch_promise_settled = true;
+    return response;
+  });
 
-    // This signals the activate event to complete.  The frame should now
-    // load.
-    registration.active.postMessage('GO');
-    return frameLoadPromise;
-  }).then(function() {
-    assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
-                  expected_url, 'frame should now be loaded and controlled');
-    assert_equals(registration.active.state, 'activated',
-                  'active worker should be in activated state');
-    frame.remove();
-  });
-}, 'Fetch events should wait for the activate event to complete.');
+  await ensureFunctionalEventsAreWaiting(registration);
+  assert_false(fetch_promise_settled,
+               "fetch()-ing a Service Worker-controlled scope shouldn't have " +
+               "settled yet");
+
+  registration.active.postMessage('ACTIVATE');
+
+  const response = await fetchPromise;
+  assert_equals(await response.text(), expected_fetch_result,
+                "Service Worker should have responded to request to" +
+                fetch_url)
+  assert_equals(registration.active.state, 'activated',
+                'active worker should be in activated state');
+}, 'Subresource fetch events should wait for the activate event to complete.');
 
 </script>
 </body>
--- a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js
@@ -1,17 +1,31 @@
 var activatePromiseResolve;
 
 addEventListener('activate', function(evt) {
   evt.waitUntil(new Promise(function(resolve) {
     activatePromiseResolve = resolve;
   }));
 });
 
-addEventListener('message', function(evt) {
-  if (typeof activatePromiseResolve === 'function') {
-    activatePromiseResolve();
+addEventListener('message', async function(evt) {
+  switch (evt.data) {
+    case 'CLAIM':
+      evt.waitUntil(new Promise(async resolve => {
+        await clients.claim();
+        evt.source.postMessage('CLAIMED');
+        resolve();
+      }));
+      break;
+    case 'ACTIVATE':
+      if (typeof activatePromiseResolve !== 'function') {
+        throw new Error('Not activating!');
+      }
+      activatePromiseResolve();
+      break;
+    default:
+      throw new Error('Unknown message!');
   }
 });
 
 addEventListener('fetch', function(evt) {
   evt.respondWith(new Response('Hello world'));
 });