diff --git a/moli-core/tests/fixtures/runtime/history_back_forward_same_turn_coalesces.html b/moli-core/tests/fixtures/runtime/history_back_forward_same_turn_traverses_in_order.html
similarity index 69%
rename from moli-core/tests/fixtures/runtime/history_back_forward_same_turn_coalesces.html
rename to moli-core/tests/fixtures/runtime/history_back_forward_same_turn_traverses_in_order.html
index c4dc87946c..e9f6c58cdf 100644
--- a/moli-core/tests/fixtures/runtime/history_back_forward_same_turn_coalesces.html
+++ b/moli-core/tests/fixtures/runtime/history_back_forward_same_turn_traverses_in_order.html
@@ -1,6 +1,6 @@
-
+
diff --git a/moli-core/tests/history.rs b/moli-core/tests/history.rs
index d5f5fc8d80..d828d1b955 100644
--- a/moli-core/tests/history.rs
+++ b/moli-core/tests/history.rs
@@ -1062,14 +1062,14 @@ async fn history_back_traversal_is_not_synchronous() -> Result<()> {
}
#[tokio::test]
-async fn history_same_turn_back_then_forward_coalesces_without_popstate() -> Result<()> {
+async fn history_same_turn_back_then_forward_preserves_both_popstates() -> Result<()> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let mut page = browser
- .fetch(&server.url("/compat/history-back-forward-same-turn-coalesces"))
+ .fetch(&server.url("/compat/history-back-forward-same-turn-traverses-in-order"))
.await?;
- wait_for_body_attribute(&browser, &mut page, "data-timeout-state", "2").await?;
+ wait_for_body_attribute(&browser, &mut page, "data-popstate-log", "1,2").await?;
assert!(
page.serialize_html_async()
@@ -1083,7 +1083,7 @@ async fn history_same_turn_back_then_forward_coalesces_without_popstate() -> Res
page.serialize_html_async()
.await
.unwrap()
- .contains("data-timeout-state=\"2\""),
+ .contains("data-async-state=\"2\""),
"{}",
page.serialize_html_async().await.unwrap()
);
@@ -1091,7 +1091,7 @@ async fn history_same_turn_back_then_forward_coalesces_without_popstate() -> Res
page.serialize_html_async()
.await
.unwrap()
- .contains("data-popstate-log=\"\""),
+ .contains("data-popstate-log=\"1,2\""),
"{}",
page.serialize_html_async().await.unwrap()
);
diff --git a/moli-protocol/src/domains/page/tests/navigation.rs b/moli-protocol/src/domains/page/tests/navigation.rs
index ba4642f346..b31a6abf90 100644
--- a/moli-protocol/src/domains/page/tests/navigation.rs
+++ b/moli-protocol/src/domains/page/tests/navigation.rs
@@ -599,6 +599,115 @@ async fn joint_history_traversal_cross_document_before_same_document() {
assert_joint_history_multi_frame_traversal("a", false, false).await;
}
+#[tokio::test(flavor = "multi_thread")]
+async fn queued_history_back_preserves_same_document_before_cross_document() {
+ assert_queued_history_same_then_cross_document(false).await;
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn queued_history_forward_preserves_same_document_before_cross_document() {
+ assert_queued_history_same_then_cross_document(true).await;
+}
+
+async fn assert_queued_history_same_then_cross_document(forward: bool) {
+ let app = axum::Router::new()
+ .fallback(|| async { axum::response::Html("history fixture") });
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let addr = listener.local_addr().unwrap();
+ let server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
+ let mut ctx = TestContext::new();
+ load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank");
+ ctx.process_async(json!({
+ "id": 9350, "method": "Page.navigate", "sessionId": "SID-1",
+ "params": {"url": format!("http://{addr}/top")}
+ }))
+ .await;
+ assert!(take_response_by_id(&mut ctx, 9350)["error"].is_null());
+ wait_until_message(&mut ctx, Some("SID-1"), "history fixture load", |message| {
+ message["method"] == "Page.domContentEventFired"
+ })
+ .await;
+ joint_history_test_evaluate(
+ &mut ctx,
+ r#"
+ globalThis.frame = document.createElement('iframe');
+ globalThis.withFrameLoad = action => new Promise(resolve => {
+ frame.onload = () => resolve(true);
+ action();
+ });
+ globalThis.withFrameHash = action => new Promise(resolve => {
+ frame.contentWindow.addEventListener('hashchange', () => resolve(true), {once:true});
+ action();
+ });
+ frame.src = '/child?first';
+ withFrameLoad(() => document.body.append(frame));
+ "#,
+ )
+ .await;
+ // Separate protocol commands start the next navigation outside the
+ // preceding load handler, preserving push rather than replace semantics.
+ let hash = "withFrameHash(() => frame.contentWindow.location.hash = '#same')";
+ let load = "withFrameLoad(() => frame.contentWindow.location.search = '?second')";
+ for script in if forward { [hash, load] } else { [load, hash] } {
+ joint_history_test_evaluate(&mut ctx, script).await;
+ }
+ if forward {
+ joint_history_test_evaluate(
+ &mut ctx,
+ "withFrameLoad(() => frame.contentWindow.history.back())",
+ )
+ .await;
+ joint_history_test_evaluate(
+ &mut ctx,
+ "withFrameHash(() => frame.contentWindow.history.back())",
+ )
+ .await;
+ }
+ let method = if forward { "forward" } else { "back" };
+ let synchronous = joint_history_test_evaluate(
+ &mut ctx,
+ &format!(
+ r#"
+ globalThis.traversalEvents = [];
+ const snapshot = kind => [kind, frame.contentWindow.location.search,
+ frame.contentWindow.location.hash];
+ frame.contentWindow.addEventListener('hashchange', event => {{
+ const destination = new URL(event.newURL);
+ traversalEvents.push(['hash', destination.search, destination.hash]);
+ }}, {{once:true}});
+ globalThis.traversalsFinished = new Promise(resolve => {{
+ frame.onload = () => {{
+ traversalEvents.push(snapshot('load'));
+ resolve(traversalEvents);
+ }};
+ }});
+ frame.contentWindow.history.{method}();
+ frame.contentWindow.history.{method}();
+ [frame.contentWindow.location.search, frame.contentWindow.location.hash];
+ "#
+ ),
+ )
+ .await;
+ assert_eq!(
+ synchronous,
+ if forward {
+ json!(["?first", ""])
+ } else {
+ json!(["?second", "#same"])
+ }
+ );
+ let events = joint_history_test_evaluate(&mut ctx, "traversalsFinished").await;
+ assert_eq!(
+ events,
+ if forward {
+ json!([["hash", "?first", "#same"], ["load", "?second", "#same"]])
+ } else {
+ json!([["hash", "?second", ""], ["load", "?first", ""]])
+ }
+ );
+ server.abort();
+}
+
#[tokio::test(flavor = "multi_thread")]
async fn joint_history_traversal_same_document_before_cross_document() {
assert_joint_history_multi_frame_traversal("b", false, false).await;
diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs
index c1d10c4842..c548248036 100644
--- a/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs
+++ b/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs
@@ -589,7 +589,13 @@ fn history_traversal_target_window<'s>(
) -> Option> {
match target.dispatch_scope() {
crate::native_bridge::OwnerDispatchScope::Top => {
- Some(scope.get_current_context().global(scope))
+ // An isolated realm may initiate this task, but the Page's
+ // default Window owns the history being traversed.
+ Some(
+ host.page_default_context(scope)
+ .unwrap_or_else(|| scope.get_current_context())
+ .global(scope),
+ )
}
crate::native_bridge::OwnerDispatchScope::Child(child_handle) => {
host.child_browsing_context_window_wrapper(scope, child_handle)
diff --git a/moli-renderer-v8/src/native_bridge/history_queue.rs b/moli-renderer-v8/src/native_bridge/history_queue.rs
index 7a2f3691cb..b54c12ba9a 100644
--- a/moli-renderer-v8/src/native_bridge/history_queue.rs
+++ b/moli-renderer-v8/src/native_bridge/history_queue.rs
@@ -118,6 +118,7 @@ impl HistoryQueueState {
fn pending_history_traversal_target_index(&self, target: WindowTaskTarget) -> Option {
self.pending_history_traversal_tasks
.iter()
+ .rev()
.find_map(|queued| match &queued.action {
PendingHistoryTraversalAction::SameDocument(pending)
if pending.target == target =>
@@ -140,18 +141,22 @@ impl HistoryQueueState {
info: Option>,
result: Option,
) -> Option {
- if let Some(pending) = self
- .pending_history_traversal_tasks
- .iter_mut()
- .find_map(|queued| match &mut queued.action {
- PendingHistoryTraversalAction::SameDocument(pending)
- if pending.target == target =>
- {
- Some(pending)
- }
- PendingHistoryTraversalAction::SameDocument(_)
- | PendingHistoryTraversalAction::ChildCrossDocument(_) => None,
- })
+ // History API requests are ordered steps, including a same-document
+ // traversal followed by a cross-document traversal. Only Navigation
+ // API requests with result promises can share a pending task.
+ if result.is_some()
+ && let Some(pending) =
+ self.pending_history_traversal_tasks
+ .iter_mut()
+ .find_map(|queued| match &mut queued.action {
+ PendingHistoryTraversalAction::SameDocument(pending)
+ if pending.target == target && !pending.results.is_empty() =>
+ {
+ Some(pending)
+ }
+ PendingHistoryTraversalAction::SameDocument(_)
+ | PendingHistoryTraversalAction::ChildCrossDocument(_) => None,
+ })
{
pending.target_index = target_index;
pending.joint_step = joint_step;
diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs b/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs
index 969938113b..93a92f0a89 100644
--- a/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs
@@ -72,9 +72,10 @@ history.back();
}
#[tokio::test(flavor = "current_thread")]
-async fn history_back_coalesces_into_one_typed_turn_and_never_enters_page_timer() {
+async fn history_back_preserves_each_typed_turn_and_never_enters_page_timer() {
run_page_vm_async_test(async move {
- let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
+ let loader =
+ crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
let document_url = Url::parse("https://example.com/history-typed-turn").unwrap();
let (mut page_vm, _resource_source, _owner_wake_rx) =
page_vm_with_bound_task_sources_and_owner_wake(&loader, document_url);
@@ -112,28 +113,45 @@ location.hash
!page_vm.vm().has_ready_timeout(),
"history traversal admission must not manufacture a PageTimer descriptor"
);
- assert_eq!(
- page_vm.vm().ms_to_next_timeout(),
- None
- );
+ assert_eq!(page_vm.vm().ms_to_next_timeout(), None);
assert!(
page_vm
- .run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::HistoryTraversal, &loader)
+ .run_exact_selected_page_task_for_test(
+ PageSelectedTaskTestSelector::HistoryTraversal,
+ &loader
+ )
.await?,
- "the coalesced traversal should consume one production selected task"
+ "the first traversal should consume one production selected task"
+ );
+ assert_eq!(page_vm.vm_mut().eval("location.hash")?, "#one");
+ assert_eq!(
+ page_vm.vm_mut().eval("__historyTurnLog.join('|')")?,
+ "popstate:#one|microtask:#one",
+ "the selected traversal must checkpoint its event microtasks before the next turn"
+ );
+ assert!(
+ page_vm
+ .run_exact_selected_page_task_for_test(
+ PageSelectedTaskTestSelector::HistoryTraversal,
+ &loader
+ )
+ .await?,
+ "the second traversal should retain its own production selected task"
);
assert_eq!(page_vm.vm_mut().eval("location.hash")?, "");
assert_eq!(
page_vm.vm_mut().eval("__historyTurnLog.join('|')")?,
- "popstate:|microtask:",
- "the selected traversal must checkpoint its event microtasks before the next turn"
+ "popstate:#one|microtask:#one|popstate:|microtask:"
);
assert!(
!page_vm
- .run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::HistoryTraversal, &loader)
+ .run_exact_selected_page_task_for_test(
+ PageSelectedTaskTestSelector::HistoryTraversal,
+ &loader
+ )
.await?,
- "two pending history.back() calls for one LocalWindow must coalesce into one source position"
+ "the history source should drain after both ordered traversal tasks"
);
Ok::<_, anyhow::Error>(())
})
diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs
index 911dd2f21b..2743b0a4e3 100644
--- a/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs
+++ b/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs
@@ -1345,7 +1345,7 @@ result.finished.catch(error => __lmClosedHistoryRoute.push("finished:" + error.n
}
#[tokio::test]
-async fn history_back_calls_from_default_and_isolated_world_coalesce_per_window() {
+async fn history_back_calls_from_default_and_isolated_world_preserve_each_step() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm =
new_storage_page_task_executor_test_vm_with_loader("https://example.com/base", &loader);
@@ -1379,9 +1379,24 @@ history.pushState(null, "", "#two");
.await
.expect("default realm traversal should run")
);
+ assert_eq!(
+ vm.eval("location.hash")
+ .expect("first queued traversal should preserve its destination"),
+ "#one"
+ );
+ assert!(
+ vm.run_one_history_traversal_executor_turn(&loader)
+ .await
+ .expect("isolated realm traversal should run after the default realm traversal")
+ );
+ assert_eq!(
+ vm.eval("location.hash")
+ .expect("second queued traversal should reach the initial entry"),
+ ""
+ );
assert!(
!vm.run_one_history_traversal_executor_turn(&loader)
.await
- .expect("history source should be drained after one Window traversal position")
+ .expect("history source should be drained after both traversals")
);
}
diff --git a/moli-test-support/src/lib.rs b/moli-test-support/src/lib.rs
index e5278d8eac..dd5c80db96 100644
--- a/moli-test-support/src/lib.rs
+++ b/moli-test-support/src/lib.rs
@@ -172,8 +172,8 @@ const HISTORY_BACK_SAME_TURN_TRAVERSES_ASYNCHRONOUSLY_HTML: &str = include_str!(
const HISTORY_BACK_IGNORES_PAGE_TAMPERED_QUEUE_MICROTASK_HTML: &str = include_str!(
"../../moli-core/tests/fixtures/runtime/history_back_ignores_page_tampered_queue_microtask.html"
);
-const HISTORY_BACK_FORWARD_SAME_TURN_COALESCES_HTML: &str = include_str!(
- "../../moli-core/tests/fixtures/runtime/history_back_forward_same_turn_coalesces.html"
+const HISTORY_BACK_FORWARD_SAME_TURN_TRAVERSES_IN_ORDER_HTML: &str = include_str!(
+ "../../moli-core/tests/fixtures/runtime/history_back_forward_same_turn_traverses_in_order.html"
);
const HISTORY_STATE_MUTATION_DOES_NOT_MUTATE_STORED_SNAPSHOT_HTML: &str = include_str!(
"../../moli-core/tests/fixtures/runtime/history_state_mutation_does_not_mutate_stored_snapshot.html"
diff --git a/moli-test-support/src/routes_core.rs b/moli-test-support/src/routes_core.rs
index 34f9f902de..3bea85c3d3 100644
--- a/moli-test-support/src/routes_core.rs
+++ b/moli-test-support/src/routes_core.rs
@@ -776,8 +776,8 @@ pub(super) async fn history_back_ignores_page_tampered_queue_microtask_page() ->
Html(HISTORY_BACK_IGNORES_PAGE_TAMPERED_QUEUE_MICROTASK_HTML)
}
-pub(super) async fn history_back_forward_same_turn_coalesces_page() -> Html<&'static str> {
- Html(HISTORY_BACK_FORWARD_SAME_TURN_COALESCES_HTML)
+pub(super) async fn history_back_forward_same_turn_traverses_in_order_page() -> Html<&'static str> {
+ Html(HISTORY_BACK_FORWARD_SAME_TURN_TRAVERSES_IN_ORDER_HTML)
}
pub(super) async fn history_state_mutation_does_not_mutate_stored_snapshot_page()
diff --git a/moli-test-support/src/server_routes.rs b/moli-test-support/src/server_routes.rs
index 68e4fdc3f6..dad73c6095 100644
--- a/moli-test-support/src/server_routes.rs
+++ b/moli-test-support/src/server_routes.rs
@@ -235,8 +235,8 @@ pub(super) fn build_router() -> Router {
get(history_back_ignores_page_tampered_queue_microtask_page),
)
.route(
- "/compat/history-back-forward-same-turn-coalesces",
- get(history_back_forward_same_turn_coalesces_page),
+ "/compat/history-back-forward-same-turn-traverses-in-order",
+ get(history_back_forward_same_turn_traverses_in_order_page),
)
.route(
"/compat/history-state-mutation-does-not-mutate-stored-snapshot",