From c6fafada408bbd361a5b78ab38f84637927f3abf Mon Sep 17 00:00:00 2001
From: Hiroshige Hayashizaki <hiroshige@chromium.org>
Date: Wed, 13 Oct 2021 18:12:42 -0700
Subject: [PATCH] [WPT] BFCache: service worker clients

This CL adds tests for BFCache +
Clients.claim(), Clients.matchAll() and unregister().

Bug: 1107415, 1204228, https://github.com/w3c/ServiceWorker/issues/1594
Change-Id: I73233cf917e31dd91b974823d5490d0190f0eade
---
 .../resources/helper.sub.js                   | 13 +++
 .../resources/service-worker.js               | 31 +++++++
 .../service-worker-clients-claim.https.html   | 86 +++++++++++++++++++
 ...service-worker-clients-matchall.https.html | 63 ++++++++++++++
 .../service-worker-unregister.https.html      | 52 +++++++++++
 5 files changed, 245 insertions(+)
 create mode 100644 html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js
 create mode 100644 html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html
 create mode 100644 html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html
 create mode 100644 html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html

diff --git a/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js b/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js
index d61e68a325f036a..836934b7c0673c9 100644
--- a/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js
+++ b/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js
@@ -128,3 +128,16 @@ function runEventTest(params, description) {
       params.expectedEvents);
   }, description);
 }
+
+// Call clients.claim() on the service worker
+async function claim(t, worker) {
+  const channel = new MessageChannel();
+  const saw_message = new Promise(function(resolve) {
+    channel.port1.onmessage = t.step_func(function(e) {
+      assert_equals(e.data, 'PASS', 'Worker call to claim() should fulfill.');
+      resolve();
+    });
+  });
+  worker.postMessage({port: channel.port2}, [channel.port2]);
+  await saw_message;
+}
diff --git a/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js b/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js
new file mode 100644
index 000000000000000..60dceb0a6a6f5f2
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js
@@ -0,0 +1,31 @@
+self.addEventListener('message', function(event) {
+    self.clients.claim()
+      .then(function(result) {
+          if (result !== undefined) {
+              event.data.port.postMessage(
+                  'FAIL: claim() should be resolved with undefined');
+              return;
+          }
+          event.data.port.postMessage('PASS');
+        })
+      .catch(function(error) {
+          event.data.port.postMessage('FAIL: exception: ' + error.name);
+        });
+  });
+
+self.addEventListener('fetch', e => {
+    if (e.request.url.match(/\/is-controlled/)) {
+      e.respondWith(new Response('controlled'));
+    }
+    else if (e.request.url.match(/\/get-clients-matchall/)) {
+      const options = { includeUncontrolled: true, type: 'all' };
+      e.respondWith(
+        self.clients.matchAll(options)
+          .then(clients => {
+            const client_urls = [];
+            clients.forEach(client => client_urls.push(client.url));
+            return new Response(JSON.stringify(client_urls));
+          })
+      );
+    }
+  });
diff --git a/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html b/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html
new file mode 100644
index 000000000000000..58748ead5f738e7
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html
@@ -0,0 +1,86 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+  const pageA = new RemoteContext(token());
+  const pageB = new RemoteContext(token());
+
+  const urlA = location.origin + executorPath + pageA.context_id + '&events=pagehide,pageshow,load';
+  const urlB = originCrossSite + executorPath + pageB.context_id;
+
+  window.open(urlA, '_blank', 'noopener');
+
+  await pageA.execute_script(waitForPageShow);
+
+  await pageA.execute_script(
+    (url) => {
+      navigator.serviceWorker.oncontrollerchange =
+          () => recordEvent('controllerchange');
+    });
+
+  // Register a service worker after `pageA` is loaded to make `pageA`
+  // uncontrolled at this time.
+  const workerUrl = 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+  const registration = await service_worker_unregister_and_register(t, workerUrl, './');
+  t.add_cleanup(_ => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+
+  await pageA.execute_script(
+    (url) => {
+      prepareNavigation(() => { location.href = url; });
+    },
+    [urlB]);
+
+  await pageB.execute_script(waitForPageShow);
+
+  // Call clients.claim() when `pageA` is in BFCache.
+  await claim(t, registration.active);
+
+  const clients1 = await (await fetch('/get-clients-matchall')).json();
+
+  await pageB.execute_script(
+    () => {
+      prepareNavigation(() => { history.back(); });
+    }
+  );
+  await pageA.execute_script(waitForPageShow);
+  await assert_bfcached(pageA);
+
+  const clients2 = await (await fetch('/get-clients-matchall')).json();
+  assert_equals(
+    await pageA.execute_script(
+        () => fetch('/is-controlled?claim-2').then(r => r.text())),
+    'controlled',
+    '2: pageA should be controlled');
+
+  assert_array_equals(
+    await pageA.execute_script(() => getRecordedEvents()),
+    [
+      'window.load',
+      'window.pageshow',
+      'window.pagehide.persisted',
+      'window.pageshow.persisted',
+      'controllerchange',
+    ]);
+
+  // Call clients.claim() again when `pageA` is not in BFCache.
+  await claim(t, registration.active);
+
+  assert_equals(
+    await pageA.execute_script(
+        () => fetch('/is-controlled?claim-3').then(r => r.text())),
+    'controlled',
+    '3: pageA should be controlled');
+  const clients3 = await (await fetch('/get-clients-matchall')).json();
+
+  assert_true(clients1.indexOf(urlA) < 0, '1: page should not be controlled when claim() is called while BFCached');
+  assert_true(clients2.indexOf(urlA) >= 0, '2: page should be controlled when claim() is called while BFCached and the page is restored');
+  assert_true(clients3.indexOf(urlA) >= 0, '3: page should be controlled when claim()ed after restored');
+}, 'Serviceworker Clients.claim()');
+</script>
diff --git a/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html b/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html
new file mode 100644
index 000000000000000..2e5bb1bac5f68e6
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+  const workerUrl = 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+  const registration = await service_worker_unregister_and_register(t, workerUrl, './');
+  t.add_cleanup(_ => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+  claim(t, registration.active);
+
+  const pageA = new RemoteContext(token());
+  const pageB = new RemoteContext(token());
+
+  const urlA = location.origin + executorPath + pageA.context_id;
+  const urlB = originCrossSite + executorPath + pageB.context_id;
+
+  window.open(urlA, '_blank', 'noopener');
+
+  await pageA.execute_script(waitForPageShow);
+
+  const clients1 = await (await fetch('/get-clients-matchall')).json();
+  const controlled1 = await pageA.execute_script(
+      () => fetch('/is-controlled?matchall-1').then(r => r.text()));
+
+  await pageA.execute_script(
+    (url) => prepareNavigation(() => {
+      location.href = url;
+    }),
+    [urlB]);
+
+  await pageB.execute_script(waitForPageShow);
+
+  const clients2 = await (await fetch('/get-clients-matchall')).json();
+
+  await pageB.execute_script(
+    () => {
+      prepareNavigation(() => { history.back(); });
+    }
+  );
+
+  await pageA.execute_script(waitForPageShow);
+  await assert_bfcached(pageA);
+
+  const clients3 = await (await fetch('/get-clients-matchall')).json();
+  const controlled3 = await pageA.execute_script(
+      () => fetch('/is-controlled?matchall-3').then(r => r.text()));
+
+  assert_true(clients1.indexOf(urlA) >= 0, '1: Before navigation');
+  assert_true(clients2.indexOf(urlA) < 0, '2: Before back navigation');
+  assert_true(clients3.indexOf(urlA) >= 0, '3: After back navigation');
+
+  assert_equals(controlled1, 'controlled',
+    'pageA should be controlled before BFCached');
+  assert_equals(controlled3, 'controlled',
+    'pageA should be controlled after restored');
+}, 'Serviceworker Clients.matchAll()');
+</script>
diff --git a/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html b/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html
new file mode 100644
index 000000000000000..a704c96c74b9dc0
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+  const workerUrl = 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+  const registration = await service_worker_unregister_and_register(t, workerUrl, './');
+  t.add_cleanup(_ => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+  await claim(t, registration.active);
+
+  const pageA = new RemoteContext(token());
+  const pageB = new RemoteContext(token());
+
+  const urlA = location.origin + executorPath + pageA.context_id;
+  const urlB = originCrossSite + executorPath + pageB.context_id;
+
+  window.open(urlA, '_blank', 'noopener');
+
+  await pageA.execute_script(waitForPageShow);
+  await pageA.execute_script(
+    (url) => prepareNavigation(() => {
+      location.href = url;
+    }),
+    [urlB]);
+
+  await pageB.execute_script(waitForPageShow);
+
+  await registration.unregister();
+
+  await pageB.execute_script(
+    () => {
+      prepareNavigation(() => { history.back(); });
+    }
+  );
+
+  await pageA.execute_script(waitForPageShow);
+  await assert_bfcached(pageA);
+
+  assert_equals(
+    await pageA.execute_script(
+        () => fetch('/is-controlled').then(r => r.text())),
+    'controlled',
+    'pageA should be still controlled');
+
+}, 'Unregister service worker while a controlled page is in BFCache should not crash');
+</script>