[WPT] BFCache eligibility: in-flight fetch (#31227)

When there is an in-flight network request at the time of navigation,

Chrome: The page is BFCached and
        the fetch request completes successfully after restored.
Firefox: The page is not BFCached
Safari: The page is BFCached but
        the fetch request is canceled.

Bug: https://github.com/whatwg/html/issues/6699
Change-Id: I1d84d92c6396abad62eab9306f8656cbf303f8c6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3222281
Reviewed-by: Kouhei Ueno <kouhei@chromium.org>
Commit-Queue: Hiroshige Hayashizaki <hiroshige@chromium.org>
Cr-Commit-Position: refs/heads/main@{#962858}

Co-authored-by: Hiroshige Hayashizaki <hiroshige@chromium.org>
diff --git a/common/dispatcher/dispatcher.js b/common/dispatcher/dispatcher.js
index b789c6e..1bac938 100644
--- a/common/dispatcher/dispatcher.js
+++ b/common/dispatcher/dispatcher.js
@@ -106,7 +106,7 @@
   //   `execute_script()` returns a rejected Promise with the error's
   //   `message`.
   //   Note that currently the type of error (e.g. DOMException) is not
-  //   preserved.
+  //   preserved, except for `TypeError`.
   // The values should be able to be serialized by JSON.stringify().
   async execute_script(fn, args) {
     const receiver = token();
@@ -117,6 +117,9 @@
     }
 
     // exception
+    if (response.name === 'TypeError') {
+      throw new TypeError(response.value);
+    }
     throw new Error(response.value);
   }
 
@@ -180,6 +183,7 @@
       } catch(e) {
         response = JSON.stringify({
           status: 'exception',
+          name: e.name,
           value: e.message
         });
       }
diff --git a/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html
new file mode 100644
index 0000000..c04089a
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<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="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// CORS and failing fetch.
+runTest(crossSiteUrl + '?delayBeforeHeader=2000&cors=yes', false, true,
+  'CORS succeeded when in BFCache');
+runTest(crossSiteUrl + '?delayBeforeHeader=2000', false, false,
+  'CORS failed when in BFCache');
+</script>
diff --git a/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html
new file mode 100644
index 0000000..b0b49d5
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<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="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// Redirects and CSP.
+runTest(
+  '/common/slow-redirect.py?delay=2&location=' +
+  encodeURIComponent(sameOriginUrl),
+  false, true,
+  'Redirect header received when in BFCache');
+runTest(
+  '/common/slow-redirect.py?delay=2&location=' +
+  encodeURIComponent(sameOriginUrl),
+  true, true,
+  'Redirect header received when in BFCache w/ CSP passing');
+runTest(
+  '/common/slow-redirect.py?delay=2&location=' +
+  encodeURIComponent(crossSiteUrl + '?cors=yes'),
+  false, true,
+  'Cross-origin redirect header received when in BFCache');
+runTest(
+  '/common/slow-redirect.py?delay=2&location=' +
+  encodeURIComponent(crossSiteUrl + '?cors=yes'),
+  true, false,
+  'Cross-origin redirect header received when in BFCache w/ CSP failing');
+</script>
diff --git a/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch.html b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch.html
new file mode 100644
index 0000000..3ce8e99
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<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="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// Successful fetch completion with different header/body timing.
+runTest(sameOriginUrl + '?delayBeforeBody=2000', false, true,
+  'Header received before BFCache and body received when in BFCache');
+runTest(sameOriginUrl + '?delayBeforeBody=3500', false, true,
+  'Header received before BFCache and body received after BFCache');
+runTest(sameOriginUrl + '?delayBeforeHeader=2000', false, true,
+  'Header and body received when in BFCache');
+runTest(sameOriginUrl + '?delayBeforeHeader=2000&delayBeforeBody=1500',
+  false, true,
+  'Header received when in BFCache and body received after BFCache');
+runTest(sameOriginUrl + '?delayBeforeHeader=3500', false, true,
+  'Header and body received after BFCache');
+</script>
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 b2087d9..0af00f9 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
@@ -67,7 +67,8 @@
 
 // Run a test that navigates A->B->A:
 // 1. Page A is opened by `params.openFunc(url)`.
-// 2. `params.funcBeforeNavigation` is executed on page A.
+// 2. `params.funcBeforeNavigation(params.argsBeforeNavigation)` is executed
+//    on page A.
 // 3. The window is navigated to page B on `params.targetOrigin`.
 // 4. The window is back navigated to page A (expecting BFCached).
 //
@@ -104,7 +105,7 @@
   runBfcacheTest(params, description);
 }
 
-async function navigateAndThenBack(pageA, pageB, urlB) {
+async function navigateAndThenBack(pageA, pageB, urlB, funcBeforeBackNavigation) {
   await pageA.execute_script(
     (url) => {
       prepareNavigation(() => {
@@ -115,6 +116,9 @@
   );
 
   await pageB.execute_script(waitForPageShow);
+  if (funcBeforeBackNavigation) {
+    await pageB.execute_script(funcBeforeBackNavigation);
+  }
   await pageB.execute_script(
     () => {
       prepareNavigation(() => { history.back(); });
@@ -129,7 +133,9 @@
     openFunc: url => window.open(url, '_blank', 'noopener'),
     scripts: [],
     funcBeforeNavigation: () => {},
+    argsBeforeNavigation: [],
     targetOrigin: originCrossSite,
+    funcBeforeBackNavigation: () => {},
     shouldBeCached: true,
     funcAfterAssertion: () => {},
   }
@@ -156,8 +162,10 @@
       }, [src]);
     }
 
-    await pageA.execute_script(params.funcBeforeNavigation);
-    await navigateAndThenBack(pageA, pageB, urlB);
+    await pageA.execute_script(params.funcBeforeNavigation,
+                               params.argsBeforeNavigation);
+    await navigateAndThenBack(pageA, pageB, urlB,
+                              params.funcBeforeBackNavigation);
 
     if (params.shouldBeCached) {
       await assert_bfcached(pageA);
@@ -166,7 +174,7 @@
     }
 
     if (params.funcAfterAssertion) {
-      await params.funcAfterAssertion(pageA, pageB);
+      await params.funcAfterAssertion(pageA, pageB, t);
     }
   }, description);
 }
diff --git a/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js b/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
new file mode 100644
index 0000000..7832003
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
@@ -0,0 +1,47 @@
+// Delay after fetch start:
+// - 0.0 seconds: before BFCache
+// - 2.0 seconds: when in BFCache
+// - 3.5 seconds: after restored from BFCache
+function runTest(urlToFetch, hasCSP, shouldSucceed, description) {
+  runBfcacheTest({
+    funcBeforeNavigation: async (urlToFetch, hasCSP) => {
+      if (hasCSP) {
+        // Set CSP.
+        const meta = document.createElement('meta');
+        meta.setAttribute('http-equiv', 'Content-Security-Policy');
+        meta.setAttribute('content', "connect-src 'self'");
+        document.head.appendChild(meta);
+      }
+
+      // Initiate a `fetch()`.
+      window.fetchPromise = fetch(urlToFetch);
+
+      // Wait for 0.5 seconds to receive response headers for the fetch()
+      // before BFCache, if any.
+      await new Promise(resolve => setTimeout(resolve, 500));
+    },
+    argsBeforeNavigation: [urlToFetch, hasCSP],
+    funcBeforeBackNavigation: () => {
+      // Wait for 2 seconds before back navigating to pageA.
+      return new Promise(resolve => setTimeout(resolve, 2000));
+    },
+    funcAfterAssertion: async (pageA, pageB, t) => {
+      // Wait for fetch() completion and check the result.
+      const result = pageA.execute_script(
+          () => window.fetchPromise.then(r => r.text()));
+      if (shouldSucceed) {
+        assert_equals(
+          await result,
+          'Body',
+          'Fetch should complete successfully after restored from BFCache');
+      } else {
+        await promise_rejects_js(t, TypeError, result,
+          'Fetch should fail after restored from BFCache');
+      }
+    }
+  }, 'Eligibility (in-flight fetch): ' + description);
+}
+
+const url = new URL('../resources/slow.py', location);
+const sameOriginUrl = url.href;
+const crossSiteUrl = originCrossSite + url.pathname;
diff --git a/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py b/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py
new file mode 100644
index 0000000..01bb330
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py
@@ -0,0 +1,13 @@
+import time
+
+def main(request, response):
+    delay_before_header = float(request.GET.first(b"delayBeforeHeader", 0)) / 1000
+    delay_before_body = float(request.GET.first(b"delayBeforeBody", 0)) / 1000
+
+    time.sleep(delay_before_header)
+    if b"cors" in request.GET:
+        response.headers.set(b"Access-Control-Allow-Origin", b"*")
+    response.write_status_headers()
+
+    time.sleep(delay_before_body)
+    response.writer.write_content(b"Body")
diff --git a/lint.ignore b/lint.ignore
index e281f71..465b39b 100644
--- a/lint.ignore
+++ b/lint.ignore
@@ -153,6 +153,7 @@
 SET TIMEOUT: encrypted-media/polyfill/clearkey-polyfill.js
 SET TIMEOUT: encrypted-media/scripts/playback-temporary-events.js
 SET TIMEOUT: generic-sensor/resources/iframe_sensor_handler.html
+SET TIMEOUT: html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
 SET TIMEOUT: html/browsers/browsing-the-web/history-traversal/*
 SET TIMEOUT: html/browsers/browsing-the-web/navigating-across-documents/*
 SET TIMEOUT: html/browsers/browsing-the-web/scroll-to-fragid/*