Service Worker: Add tentative wpt test for FetchEvent WorkerTiming in frame

This patch adds tentative wpt test. Currently FetchEvent WorkerTiming is a
tentative feature. In chromium, this feature hasn't implemented completely yet,
so some tests will be failed.

Explainer : https://github.com/wanderview/fetchevent-worker-timing/blob/master/explainer.md

Bug: 900700
Change-Id: I7a43989d8833ddd65c93106de67c4ebe6bd9ed52
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1915644
Commit-Queue: Yuta Kasai <yutakasai@google.com>
Reviewed-by: Makoto Shimazu <shimazu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#720134}
diff --git a/service-workers/service-worker/fetch-event-worker-timing-frame.tentative.https.html b/service-workers/service-worker/fetch-event-worker-timing-frame.tentative.https.html
new file mode 100644
index 0000000..6ca9247
--- /dev/null
+++ b/service-workers/service-worker/fetch-event-worker-timing-frame.tentative.https.html
@@ -0,0 +1,225 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.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>
+<script>
+// This is tentative because currently this feature only has an explainer and
+// no formal spec.
+// https://github.com/wanderview/fetchevent-worker-timing/blob/master/explainer.md
+
+async function wait_for_performance_entries(url, performance, entry_type) {
+  const entries = await performance.getEntriesByName(url);
+  if (entries.length > 0) {
+    return entries;
+  }
+  return new Promise((resolve) => {
+    new PerformanceObserver((list, observer) => {
+      const entries = list.getEntriesByName(url);
+      if (entries.length > 0) {
+        observer.disconnect();
+        resolve(entries);
+      }
+    }).observe({ entryTypes: [entry_type] });
+  });
+}
+
+function load_image(target_document, url) {
+  return new Promise((resolve, reject) => {
+    const image = target_document.createElement('img');
+    image.onload = () => { resolve(image); }
+    image.onerror = () => { reject(`Failed to load: ${url}`); };
+    image.src = url;
+  });
+}
+
+promise_test(async (t) => {
+  const registration = await service_worker_unregister_and_register(
+    t, 'resources/fetch-event-worker-timing.js', 'resources/');
+  promise_test(async (t) => registration.unregister(),
+    'Unregister service worker');
+  await wait_for_state(t, registration.installing, 'activated');
+}, 'Set up an active service worker');
+
+promise_test(async (t) => {
+  const frame = await with_iframe('resources/empty.html?fallback');
+  t.add_cleanup(() => frame.remove());
+
+  const performance = frame.contentWindow.performance;
+
+  const entries = await wait_for_performance_entries(
+    frame.src, performance, 'navigation');
+  assert_equals(entries.length, 1, 'PerformanceNavigationTiming is observed');
+
+  const worker_timings = await entries[0].workerTiming;
+  const expected_worker_timings = [
+    {
+      name: "network-fallback mark 1",
+      entryType: "mark",
+      detail: { foo: 'foo' }
+    },
+    {
+      name: "network-fallback mark 2",
+      entryType: "mark",
+      detail: { bar: 'bar' }
+    },
+    {
+      name: "network-fallback measure",
+      entryType: "measure",
+      detail: { baz: 'baz' }
+    }
+  ];
+  assert_equals(worker_timings.length, expected_worker_timings.length,
+    'workerTiming is completed when PerformanceResourceTiming is observed');
+
+  for (let i = 0; i < workerTimings.length; i++) {
+    assert_equals(worker_timings[i].name,
+      expected_worker_timings[i].name, 'entry name');
+    assert_equals(worker_timings[i].entryType,
+      expected_worker_timings[i].entryType, 'entry type');
+    assert_object_equals(worker_timings[i].detail,
+      expected_worker_timings[i].detail, 'entry detail');
+  }
+}, 'workerTiming for navigation in a frame with network fallback');
+
+promise_test(async (t) => {
+  const frame= await with_iframe('resources/empty.html?fetch-event');
+  t.add_cleanup(() => frame.remove());
+
+  const performance = frame.contentWindow.performance;
+
+  const entries = await wait_for_performance_entries(
+    frame.src, performance, 'navigation');
+  assert_equals(entries.length, 1, 'PerformanceNavigationTiming is observed');
+
+  const worker_timings = await entries[0].workerTiming;
+  const expected_worker_timings = [
+    {
+      name: "fetch-event mark 1",
+      entryType: "mark",
+      detail: { foo: 'foo' }
+    },
+    {
+      name: "fetch-event mark 2",
+      entryType: "mark",
+      detail: { bar: 'bar' }
+    },
+    {
+      name: "fetch-event measure",
+      entryType: "measure",
+      detail: { baz: 'baz' }
+    }
+  ];
+  assert_equals(worker_timings.length, expected_worker_timings.length,
+    'workerTiming is completed when PerformanceResourceTiming is observed');
+
+  for (let i = 0; i < workerTimings.length; i++) {
+    assert_equals(worker_timings[i].name,
+      expected_worker_timings[i].name, 'entry name');
+    assert_equals(worker_timings[i].entryType,
+      expected_worker_timings[i].entryType, 'entry type');
+    assert_object_equals(worker_timings[i].detail,
+      expected_worker_timings[i].detail, 'entry detail');
+  }
+}, 'workerTiming for navigation in a frame for a response from a fetch ' +
+   'handler');
+
+promise_test(async (t) => {
+  const frame = await with_iframe('resources/empty.html');
+  t.add_cleanup(() => frame.remove());
+
+  const performance = frame.contentWindow.performance;
+
+  const image_path = base_path() + 'resources/square.png?fallback';
+  const image_url = get_host_info()['HTTPS_ORIGIN'] + image_path;
+
+  let entries = await performance.getEntriesByName(image_url);
+  assert_equals(entries.length, 0, 'No PerformanceResourceTiming');
+
+  const image = await load_image(frame.contentDocument,
+    'square.png?fetch-event');
+  entries = await wait_for_performance_entries(
+    image.src, performance, 'resource');
+  assert_equals(entries.length, 1, 'PerformanceResourceTiming is observed');
+
+  const worker_timings = await entries[0].workerTiming;
+  const expected_worker_timings = [
+    {
+      name: "network-fallback mark 1",
+      entryType: "mark",
+      detail: { foo: 'foo' }
+    },
+    {
+      name: "network-fallback mark 2",
+      entryType: "mark",
+      detail: { bar: 'bar' }
+    },
+    {
+      name: "network-fallback measure",
+      entryType: "measure",
+      detail: { baz: 'baz' }
+    }
+  ];
+  assert_equals(worker_timings.length, expected_worker_timings.length,
+    'workerTiming is completed when PerformanceResourceTiming is observed');
+
+  for (let i = 0; i < workerTimings.length; i++) {
+    assert_equals(worker_timings[i].name,
+      expected_worker_timings[i].name, 'entry name');
+    assert_equals(worker_timings[i].entryType,
+      expected_worker_timings[i].entryType, 'entry type');
+    assert_object_equals(worker_timings[i].detail,
+      expected_worker_timings[i].detail, 'entry detail');
+  }
+}, 'workerTiming for subresources in a frame with network fallback');
+
+promise_test(async (t) => {
+  const frame = await with_iframe('resources/empty.html');
+  t.add_cleanup(() => frame.remove());
+
+  const performance = frame.contentWindow.performance;
+
+  const image_path = base_path() + 'resources/square.png?fetch-event';
+  const image_url = get_host_info()['HTTPS_ORIGIN'] + image_path;
+
+  let entries = await performance.getEntriesByName(image_url);
+  assert_equals(entries.length, 0, 'No PerformanceResourceTiming');
+
+  const image = await load_image(frame.contentDocument,
+    'square.png?fetch-event');
+  entries = await wait_for_performance_entries(
+    image.src, performance, 'resource');
+  assert_equals(entries.length, 1, 'PerformanceResourceTiming is observed');
+
+  const worker_timings = await entries[0].workerTiming;
+  const expected_worker_timings = [
+    {
+      name: "fetch-event mark 1",
+      entryType: "mark",
+      detail: { foo: 'foo' }
+    },
+    {
+      name: "fetch-event mark 2",
+      entryType: "mark",
+      detail: { bar: 'bar' }
+    },
+    {
+      name: "fetch-event measure",
+      entryType: "measure",
+      detail: { baz: 'baz' }
+    }
+  ];
+  assert_equals(worker_timings.length, expected_worker_timings.length,
+    'workerTiming is completed when PerformanceResourceTiming is observed');
+
+  for (let i = 0; i < workerTimings.length; i++) {
+    assert_equals(worker_timings[i].name,
+      expected_worker_timings[i].name, 'entry name');
+    assert_equals(worker_timings[i].entryType,
+      expected_worker_timings[i].entryType, 'entry type');
+    assert_object_equals(worker_timings[i].detail,
+      expected_worker_timings[i].detail, 'entry detail');
+  }
+}, 'workerTiming for subresources in a frame for a response from a fetch' +
+   'handler');
+</script>
diff --git a/service-workers/service-worker/resources/fetch-event-worker-timing.js b/service-workers/service-worker/resources/fetch-event-worker-timing.js
new file mode 100644
index 0000000..7f3b201
--- /dev/null
+++ b/service-workers/service-worker/resources/fetch-event-worker-timing.js
@@ -0,0 +1,39 @@
+importScripts("/resources/testharness.js");
+
+self.addEventListener('fetch', event => {
+  if (event.request.url.indexOf('fallback') >= 0) {
+    event.addPerformanceEntry(
+      performance.mark("network-fallback mark 1",
+        { detail: { foo: 'foo' } }));
+    event.addPerformanceEntry(
+      performance.mark("network-fallback mark 2",
+        { detail: { bar: 'bar' } }));
+    event.addPerformanceEntry(performance.measure("network-fallback measure",
+      {
+        start: "network-fallback mark 1", end: "network-fallback mark 2",
+        detail: { baz: 'baz' }
+      }));
+    return;
+  } else if (event.request.url.indexOf('fetch-event') >= 0) {
+    event.respondWith((async () => {
+      event.addPerformanceEntry(performance.mark("fetch-event mark 1",
+        { detail: { foo: 'foo' } }));
+      const response = await fetch(event.request);
+
+      event.waitUntil(new Promise((resolve) => {
+        // Add performance entries after settling a promise for respondWith().
+        step_timeout(() => {
+          event.addPerformanceEntry(performance.mark("fetch-event mark 2",
+            { detail: { bar: 'bar' } }));
+          event.addPerformanceEntry(performance.measure("fetch-event measure",
+            {
+              start: "fetch-event mark 1", end: "fetch-event mark 2",
+              detail: { baz: 'baz' }
+            }));
+          resolve();
+        }, 100);
+      }));
+      return response;
+    })());
+  }
+});