service worker: Add test for FetchEvent.handled

The spec PR is: https://github.com/w3c/ServiceWorker/pull/1496
diff --git a/service-workers/service-worker/fetch-event-handled.https.html b/service-workers/service-worker/fetch-event-handled.https.html
new file mode 100644
index 0000000..2d6f6c8
--- /dev/null
+++ b/service-workers/service-worker/fetch-event-handled.https.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html>
+<title>Service Worker: FetchEvent.handled</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+let frame = null;
+let worker = null;
+const script = 'resources/fetch-event-handled-worker.js';
+const scope = 'resources/simple.html';
+
+// Wait for a message from the service worker and removes the message handler.
+function wait_for_message_from_worker() {
+  return new Promise((resolve) => {
+    const handler = (event) => {
+      frame.contentWindow.navigator.serviceWorker.removeEventListener(
+          'message', handler);
+      resolve(event.data);
+    };
+    frame.contentWindow.navigator.serviceWorker.addEventListener(
+        'message', handler);
+  });
+}
+
+// Global setup: this must be the first promise_test.
+promise_test(async (t) => {
+  const registration =
+      await service_worker_unregister_and_register(t, script, scope);
+  worker = registration.installing;
+  await wait_for_state(t, worker, 'activated');
+  frame = await with_iframe(scope);
+}, 'global setup');
+
+promise_test(async (t) => {
+  frame.contentWindow.fetch('dummy.txt?respondWith-not-called');
+  const message = await wait_for_message_from_worker();
+  assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when respondWith() is not called');
+
+promise_test(async (t) => {
+  frame.contentWindow.fetch(
+      'dummy.txt?respondWith-not-called-and-event-canceled').catch((e) => {});
+  const message = await wait_for_message_from_worker();
+  assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when respondWith() is not called and the' +
+    ' event is canceled');
+
+promise_test(async (t) => {
+  frame.contentWindow.fetch(
+      'dummy.txt?respondWith-called-and-promise-resolved');
+  const message = await wait_for_message_from_worker();
+  assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when the promise provided' +
+    ' to respondWith() is resolved');
+
+promise_test(async (t) => {
+  frame.contentWindow.fetch(
+      'dummy.txt?respondWith-called-and-promise-resolved-to-invalid-response')
+      .catch((e) => {});
+  const message = await wait_for_message_from_worker();
+  assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when the promise provided' +
+    ' to respondWith() is resolved to an invalid response');
+
+promise_test(async (t) => {
+  frame.contentWindow.fetch(
+      'dummy.txt?respondWith-called-and-promise-rejected').catch((e) => {});
+  const message = await wait_for_message_from_worker();
+  assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when the promise provided to' +
+    ' respondWith() is rejected');
+
+// Global cleanup: the final promise_test.
+promise_test(async (t) => {
+  if (frame)
+    frame.remove();
+  await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
+</html>
diff --git a/service-workers/service-worker/resources/fetch-event-handled-worker.js b/service-workers/service-worker/resources/fetch-event-handled-worker.js
new file mode 100644
index 0000000..4af58e2
--- /dev/null
+++ b/service-workers/service-worker/resources/fetch-event-handled-worker.js
@@ -0,0 +1,41 @@
+// This worker reports back the final state of FetchEvent.handled (RESOLVED or
+// REJECTED) to the test.
+
+// Send a message to the client with the client id.
+function send_message_to_client(message, clientId) {
+  clients.get(clientId).then((client) => {
+    client.postMessage(message);
+  });
+}
+
+self.addEventListener('fetch', function(event) {
+  const clientId = event.clientId;
+  try {
+    event.handled.then(() => {
+      send_message_to_client('RESOLVED', clientId);
+    }, () => {
+      send_message_to_client('REJECTED', clientId);
+    });
+  } catch (e) {
+    send_message_to_client('FAILED', clientId);
+    return;
+  }
+
+  const search = new URL(event.request.url).search;
+  switch (search) {
+    case '?respondWith-not-called':
+      break;
+    case '?respondWith-not-called-and-event-canceled':
+      event.preventDefault();
+      break;
+    case '?respondWith-called-and-promise-resolved':
+      event.respondWith(Promise.resolve(new Response('body')));
+      break;
+    case '?respondWith-called-and-promise-resolved-to-invalid-response':
+      event.respondWith(Promise.resolve('invalid response'));
+      break;
+    case '?respondWith-called-and-promise-rejected':
+      event.respondWith(Promise.reject(new Error('respondWith rejected')));
+      break;
+  }
+});