Prerender: upstream wpt_internal/prerender/visibility-state.html

This behavior is described in
https://jeremyroman.github.io/alternate-loading-modes/prerendering.html#interaction-with-visibility-state

Bug: 1253158
Change-Id: I70c36276c7e0a3c63d48b3adb4d8f8c857a83152
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3229327
Commit-Queue: Lingqi Chi <lingqi@chromium.org>
Reviewed-by: Hiroki Nakagawa <nhiroki@chromium.org>
Cr-Commit-Position: refs/heads/main@{#932962}
diff --git a/lint.ignore b/lint.ignore
index 8798fa3..ac67bd0 100644
--- a/lint.ignore
+++ b/lint.ignore
@@ -335,6 +335,7 @@
 SET TIMEOUT: scheduler/tentative/current-task-signal-async-abort.any.js
 SET TIMEOUT: scheduler/tentative/current-task-signal-async-priority.any.js
 SET TIMEOUT: speculation-rules/prerender/resources/activation-start.html
+SET TIMEOUT: speculation-rules/prerender/resources/deferred-promise-utils.js
 SET TIMEOUT: speculation-rules/prerender/resources/utils.js
 
 # setTimeout use in reftests
diff --git a/speculation-rules/prerender/resources/deferred-promise-utils.js b/speculation-rules/prerender/resources/deferred-promise-utils.js
new file mode 100644
index 0000000..02bd7ec
--- /dev/null
+++ b/speculation-rules/prerender/resources/deferred-promise-utils.js
@@ -0,0 +1,72 @@
+/**
+ * This file co-works with a html file and utils.js to test a promise that
+ * should be deferred during prerendering.
+ *
+ * Usage example:
+ *  Suppose the html is "prerender-promise-test.html"
+ *  On prerendering page, prerender-promise-test.html?prerendering:
+ *    const prerenderEventCollector = new PrerenderEventCollector();
+ *    const promise = {a promise that should be deferred during prerendering};
+ *    prerenderEventCollector.start(promise, {promise name});
+ *
+ *  On the initiator page, prerender-promise-test.html:
+ *   execute
+ *    `loadInitiatorPage();`
+ */
+
+// Collects events that happen relevant to a prerendering page.
+// An event is added when:
+// 1. start() is called.
+// 2. a prerenderingchange event is dispatched on this document.
+// 3. the promise passed to start() is resolved.
+// 4. addEvent() is called manually.
+class PrerenderEventCollector {
+  constructor() {
+    this.eventsSeen_ = [];
+  }
+
+  // Adds an event to `eventsSeen_` along with the prerendering state of the
+  // page.
+  addEvent(eventMessage) {
+    this.eventsSeen_.push(
+        {event: eventMessage, prerendering: document.prerendering});
+  }
+
+  // Starts collecting events until the promise resolves. Triggers activation by
+  // telling the initiator page that it is ready for activation.
+  async start(promise, promiseName) {
+    assert_true(document.prerendering);
+    this.addEvent(`started waiting ${promiseName}`);
+    promise
+        .then(
+            () => {
+              this.addEvent(`finished waiting ${promiseName}`);
+            },
+            (error) => {
+              if (error instanceof Error)
+                error = error.name;
+              this.addEvent(`${promiseName} rejected: ${error}`);
+            })
+        .finally(() => {
+          // Used to communicate with the main test page.
+          const testChannel = new BroadcastChannel('test-channel');
+          // Send the observed events back to the main test page.
+          testChannel.postMessage(this.eventsSeen_);
+          testChannel.close();
+          window.close();
+        });
+    document.addEventListener('prerenderingchange', () => {
+      this.addEvent('prerendering change');
+    });
+
+    // Post a task to give the implementation a chance to fail in case it
+    // resolves a promise without waiting for activation.
+    setTimeout(() => {
+      // Used to communicate with the initiator page.
+      const prerenderChannel = new BroadcastChannel('prerender-channel');
+      // Inform the initiator page that this page is ready to be activated.
+      prerenderChannel.postMessage('readyToActivate');
+      prerenderChannel.close();
+    }, 0);
+  }
+}
diff --git a/speculation-rules/prerender/resources/visibility-state-check.html b/speculation-rules/prerender/resources/visibility-state-check.html
new file mode 100644
index 0000000..3c1e488
--- /dev/null
+++ b/speculation-rules/prerender/resources/visibility-state-check.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="utils.js"></script>
+<script src="deferred-promise-utils.js"></script>
+<script>
+const params = new URLSearchParams(location.search);
+
+// The main test page (visibility-state.html) loads the initiator page,
+// then the initiator page will prerender itself with the `prerendering`
+// parameter.
+const isPrerendering = params.has('prerendering');
+
+if (!isPrerendering) {
+  loadInitiatorPage();
+} else {
+  const prerenderEventCollector = new PrerenderEventCollector();
+  const promise = new Promise((resolve) => {
+    prerenderEventCollector.addEvent(
+        'Initial visibilityState: ' + document.visibilityState);
+
+    document.addEventListener('prerenderingchange', () => {
+      prerenderEventCollector.addEvent(
+          'visibilityState after prerenderingchange: ' +
+          document.visibilityState);
+      resolve();
+    });
+  });
+  prerenderEventCollector.start(promise, 'visibilityState check');
+}
+
+</script>
diff --git a/speculation-rules/prerender/visibility-state.html b/speculation-rules/prerender/visibility-state.html
new file mode 100644
index 0000000..1affe54
--- /dev/null
+++ b/speculation-rules/prerender/visibility-state.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>visibilityState must be updated after prerendering</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+  const bc = new BroadcastChannel('test-channel');
+
+  const gotMessage = new Promise(resolve => {
+    bc.addEventListener('message', e => {
+      resolve(e.data);
+    }, {
+      once: true
+    });
+  });
+  const url = `resources/visibility-state-check.html`;
+  window.open(url, '_blank', 'noopener');
+
+  const result = await gotMessage;
+  const expected = [
+    {
+      event: 'Initial visibilityState: hidden',
+      prerendering: true
+    },
+    {
+      event: 'started waiting visibilityState check',
+      prerendering: true
+    },
+    {
+      event: 'visibilityState after prerenderingchange: visible',
+      prerendering: false
+    },
+    {
+      event: 'finished waiting visibilityState check',
+      prerendering: false
+    }
+  ];
+  assert_equals(result.length, expected.length);
+  for (let i = 0; i < result.length; i++) {
+    assert_equals(result[i].event, expected[i].event, `event${i}`);
+    assert_equals(result[i].prerendering, expected[i].prerendering,
+      `prerendering${i}`);
+  }
+}, 'The visibilityState must be updated after prerendering.');
+
+</script>
+</body>