AppHistory bfcache support

Currently, with appHistory and bfcache both enabled, appHistory's
entries() arrays are not updated when a navigation is served from
bfcache. It keeps the same appHistory.entries() that it had when the
document was put into bfcache, but the entries() almost certainly have
changed in the meantime.

This CL ensures that when blink is notified of the restore from
bfcache, it also receives updated appHistory.entries(). Any entries
that evicted from appHistory.entries() as a result of the restore
will have dispose events fired.

Fixed: 1241655
Change-Id: I9c49312ccbbec714f751f3b59c8d1f05edb024fd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3277336
Reviewed-by: Charles Reis <creis@chromium.org>
Reviewed-by: Mason Freed <masonf@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Reviewed-by: Domenic Denicola <domenic@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Commit-Queue: Nate Chapin <japhet@chromium.org>
Cr-Commit-Position: refs/heads/main@{#972761}
diff --git a/app-history/app-history-entry/entries-after-bfcache-in-iframe.html b/app-history/app-history-entry/entries-after-bfcache-in-iframe.html
new file mode 100644
index 0000000..5644d12
--- /dev/null
+++ b/app-history/app-history-entry/entries-after-bfcache-in-iframe.html
@@ -0,0 +1,36 @@
+<!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="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
+<script src="resources/is_uuid.js"></script>
+
+<script>
+// This test ensures that appHistory.entries() in an iframe is properly updated
+// when a page is restored from bfcache.
+// First, create an iframe and do a fragment navigation in it, so that its
+// appHistory.entries().length == 2. Then go back, so that entries()[0] is
+// current. Finally, navigate the main window (which should clobber the
+// the iframe's entries()[1]), and come back via bfcache. If the iframe's
+// entries() were updated, then its entries().length should have been reduced
+// to 1.
+runBfcacheTest({
+  targetOrigin: originSameOrigin,
+  funcBeforeNavigation: async () => {
+    window.events = [];
+    let i = document.createElement("iframe");
+    i.src = "/common/blank.html";
+    document.body.appendChild(i);
+    await new Promise(resolve => i.onload = () => setTimeout(resolve, 0));
+    await i.contentWindow.appHistory.navigate("#foo");
+    await i.contentWindow.appHistory.back();
+    window.frames[0].appHistory.entries()[1].ondispose = () => events.push("dispose");
+    window.frames[0].onpageshow = () => events.push("pageshow");
+  },
+  async funcAfterAssertion(pageA, pageB) {
+    assert_equals(await pageA.execute_script(() => window.frames[0].appHistory.entries().length), 1);
+    assert_array_equals(await pageA.execute_script(() => window.events), ["pageshow", "dispose"]);
+  }
+}, "entries() in an iframe must be updated after navigating back to a bfcached page");
+</script>
diff --git a/app-history/app-history-entry/entries-after-bfcache.html b/app-history/app-history-entry/entries-after-bfcache.html
new file mode 100644
index 0000000..3cfe039
--- /dev/null
+++ b/app-history/app-history-entry/entries-after-bfcache.html
@@ -0,0 +1,60 @@
+<!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="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
+<script src="resources/is_uuid.js"></script>
+
+<script>
+// This test ensures that appHistory.entries() is properly updated when a page
+// is restored from bfcache. Before navigating away and back, entries() contains
+// a single entry representing this document. When restored from bfcache,
+// entries() should now have two entries: [0] should still be this document, but
+// [1] should represent the document that we navigated to and back from
+// (assuming that document is same-origin to this one).
+runBfcacheTest({
+  targetOrigin: originSameOrigin,
+  funcBeforeNavigation: () => {
+    window.originalEntry0 = appHistory.entries()[0];
+  },
+  async funcAfterAssertion(pageA, pageB) {
+    const entryData = await pageA.execute_script(() => {
+      return appHistory.entries().map(e => ({
+        url: e.url,
+        key: e.key,
+        id: e.id,
+        index: e.index,
+        sameDocument: e.sameDocument
+      }));
+    });
+
+    assert_equals(entryData.length, 2);
+
+    // Ensure that [1] has the proper url, and otherwise is initialized as
+    // a cross-document AppHistoryEntry ought to be.
+    assert_equals(entryData[0].url, pageA.url);
+    assert_equals(entryData[1].url, pageB.url);
+
+    assert_true(isUUID(entryData[0].key));
+    assert_true(isUUID(entryData[1].key));
+    assert_not_equals(entryData[0].key, entryData[1].key);
+
+    assert_true(isUUID(entryData[0].id));
+    assert_true(isUUID(entryData[1].id));
+    assert_not_equals(entryData[0].id, entryData[1].id);
+
+    assert_equals(entryData[0].index, 0);
+    assert_equals(entryData[1].index, 1);
+
+    assert_true(entryData[0].sameDocument);
+    assert_false(entryData[1].sameDocument);
+
+    const currentIsZero = await pageA.execute_script(() => appHistory.current === appHistory.entries()[0]);
+    assert_true(currentIsZero);
+
+    const zeroIsSameAsOriginal = await pageA.execute_script(() => appHistory.current === window.originalEntry0);
+    assert_true(zeroIsSameAsOriginal);
+  }
+}, "entries() must contain the forward-history page after navigating back to a bfcached page");
+</script>
diff --git a/app-history/per-entry-events/dispose-after-bfcache.html b/app-history/per-entry-events/dispose-after-bfcache.html
new file mode 100644
index 0000000..db24158
--- /dev/null
+++ b/app-history/per-entry-events/dispose-after-bfcache.html
@@ -0,0 +1,32 @@
+<!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="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
+<script src="resources/is_uuid.js"></script>
+
+<script>
+// This test:
+// * Does a fragment navigation, then goes back (same-document).
+// * Navigates away, then back via bfcache.
+// When navigating away, appHistory.entries()[1] will be overwritten.
+// When returning after bfcache restore, appHistory.entries()[1] should represent
+// pageB, and the original appHistory.entries()[1] should have been disposed.
+runBfcacheTest({
+  targetOrigin: originSameOrigin,
+  funcBeforeNavigation: async () => {
+    window.events = [];
+    await appHistory.navigate("#1");
+    await appHistory.back();
+    window.originalEntry1 = appHistory.entries()[1];
+    window.originalEntry1.ondispose = () => events.push("dispose");
+    window.onpageshow = () => events.push("pageshow");
+  },
+  async funcAfterAssertion(pageA, pageB) {
+    assert_equals(await pageA.execute_script(() => appHistory.entries().length), 2);
+    assert_false(await pageA.execute_script(() => window.originalEntry1 === appHistory.entries[1]));
+    assert_array_equals(await pageA.execute_script(() => window.events), ["pageshow", "dispose"]);
+  }
+}, "entries() must contain the forward-history page after navigating back to a bfcached page");
+</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 27a0183..ff446f5 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
@@ -153,6 +153,10 @@
     const urlA = executorPath + pageA.context_id;
     const urlB = params.targetOrigin + executorPath + pageB.context_id;
 
+    // So that tests can refer to these URLs for assertions if necessary.
+    pageA.url = originSameOrigin + urlA;
+    pageB.url = urlB;
+
     params.openFunc(urlA);
 
     await pageA.execute_script(waitForPageShow);
diff --git a/lint.ignore b/lint.ignore
index 052b566..79c0c7c 100644
--- a/lint.ignore
+++ b/lint.ignore
@@ -138,6 +138,7 @@
 SET TIMEOUT: *-manual.*
 SET TIMEOUT: annotation-model/scripts/ajv.min.js
 SET TIMEOUT: apng/animated-png-timeout.html
+SET TIMEOUT: app-history/app-history-entry/entries-after-bfcache-in-iframe.html
 SET TIMEOUT: cookies/resources/testharness-helpers.js
 SET TIMEOUT: common/reftest-wait.js
 SET TIMEOUT: conformance-checkers/*