[navigation-timing]: Test fine-grained Service Worker timing (#31187)

* [navigation-timing]: Test fine-grained Service Worker timing

Mark the timing inside the service worker (and adjust by timeOrigin), to
make sure the timing reported to Navigation Timing correspond with the
milestones specified in
https://w3c.github.io/ServiceWorker/#service-worker-timing.

The four relevant milestones which should be in order:
- PerformanceNavigationTiming.workerStart
- Finish the activation event
- PerformanceNavigationTiming.fetchStart
- Handling the Fetch event in the worker

This is to ensure that the difference between workerStart and fetchStart
measures the delay caused by service worker activation.

See https://github.com/w3c/navigation-timing/issues/148

* Use step_timeout

* Remove unnecessary timeouts
diff --git a/service-workers/service-worker/navigation-timing-extended.https.html b/service-workers/service-worker/navigation-timing-extended.https.html
new file mode 100644
index 0000000..acb02c6
--- /dev/null
+++ b/service-workers/service-worker/navigation-timing-extended.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const timingEventOrder = [
+    'startTime',
+    'workerStart',
+    'fetchStart',
+    'requestStart',
+    'responseStart',
+    'responseEnd',
+];
+
+function navigate_in_frame(frame, url) {
+    frame.contentWindow.location = url;
+    return new Promise((resolve) => {
+        frame.addEventListener('load', () => {
+            const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0];
+            const {timeOrigin} = frame.contentWindow.performance;
+            resolve({
+                workerStart: timing.workerStart + timeOrigin,
+                fetchStart: timing.fetchStart + timeOrigin
+            })
+        });
+    });
+}
+
+const worker_url = 'resources/navigation-timing-worker-extended.js';
+
+promise_test(async (t) => {
+    const scope = 'resources/timings/dummy.html';
+    const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+    t.add_cleanup(() => registration.unregister());
+    await wait_for_state(t, registration.installing, 'activating');
+    const frame = await with_iframe('resources/empty.html');
+    t.add_cleanup(() => frame.remove());
+
+    const [timingFromEntry, timingFromWorker] = await Promise.all([
+        navigate_in_frame(frame, scope),
+        new Promise(resolve => {
+            window.addEventListener('message', m => {
+                resolve(m.data)
+            })
+        })])
+
+    assert_greater_than(timingFromWorker.activateWorkerEnd, timingFromEntry.workerStart,
+        'workerStart marking should not wait for worker activation to finish');
+    assert_greater_than(timingFromEntry.fetchStart, timingFromWorker.activateWorkerEnd,
+        'fetchStart should be marked once the worker is activated');
+    assert_greater_than(timingFromWorker.handleFetchEvent, timingFromEntry.fetchStart,
+        'fetchStart should be marked before the Fetch event handler is called');
+}, 'Service worker controlled navigation timing');
+</script>
diff --git a/service-workers/service-worker/resources/navigation-timing-worker-extended.js b/service-workers/service-worker/resources/navigation-timing-worker-extended.js
new file mode 100644
index 0000000..79c5408
--- /dev/null
+++ b/service-workers/service-worker/resources/navigation-timing-worker-extended.js
@@ -0,0 +1,22 @@
+importScripts("/resources/testharness.js");
+const timings = {}
+
+const DELAY_ACTIVATION = 500
+
+self.addEventListener('activate', event => {
+    event.waitUntil(new Promise(resolve => {
+        timings.activateWorkerStart = performance.now() + performance.timeOrigin;
+
+        // This gives us enough time to ensure activation would delay fetch handling
+        step_timeout(resolve, DELAY_ACTIVATION);
+    }).then(() => timings.activateWorkerEnd = performance.now() + performance.timeOrigin));
+})
+
+self.addEventListener('fetch', event => {
+    timings.handleFetchEvent = performance.now() + performance.timeOrigin;
+    event.respondWith(Promise.resolve(new Response(new Blob([`
+            <script>
+                parent.postMessage(${JSON.stringify(timings)}, "*")
+            </script>
+    `]))));
+});