From 0e07e58286a9500ae52c3f5b7e32205ea46f54a0 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 10 Sep 2026 11:19:22 +0800 Subject: [PATCH] fix: replace iframe src history before load completion Use the active child Document's native complete-load state when choosing whether iframe src navigation replaces the current history entry. This covers parser, DOMContentLoaded, load and pageshow callbacks despite an already complete readyState, while retaining post-load push behavior. Add Browser coverage for 14 src assignment and setAttribute flows. Update two existing history expectations for src changes from iframe load callbacks, independently confirmed with their HTML fixtures in Chrome. Validation: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (17915 passed, 13 skipped). The 222-case WPT comparison gains three passing cases and three subtests without regressions; passed ledger is 9105. --- .../wpt-cross-current/failed-cases.txt | 3 - .../wpt-cross-current/passed-cases.txt | 3 + moli-core/tests/history_child.rs | 10 +- moli-core/tests/history_iframe.rs | 109 ++++++++++++++++++ .../element/url_attributes/iframe.rs | 11 +- 5 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 moli-core/tests/history_iframe.rs diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 350c55627f..61cbcd52d7 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -2714,9 +2714,6 @@ html/browsers/browsing-the-web/navigating-across-documents/014.html html/browsers/browsing-the-web/navigating-across-documents/abort-document-load.html html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204-fragment.html html/browsers/browsing-the-web/navigating-across-documents/plugin-document.historical.html -html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-load.html -html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-pageshow.html -html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src.html html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-traversal.html html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-stop.html html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index d856c0bcd5..3b105623fb 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -5716,6 +5716,9 @@ html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/f html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate-during-load.html html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate-during-pageshow.html html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate.html +html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-load.html +html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-pageshow.html +html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src.html html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-during-load.html html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-during-pageshow.html html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign.html diff --git a/moli-core/tests/history_child.rs b/moli-core/tests/history_child.rs index 0e9efb61d7..4e791b75d2 100644 --- a/moli-core/tests/history_child.rs +++ b/moli-core/tests/history_child.rs @@ -928,7 +928,7 @@ async fn child_browsing_context_attribute_navigation_preserves_local_history() - page.serialize_html_async() .await .unwrap() - .contains("data-child-history-length=\"3\""), + .contains("data-child-history-length=\"2\""), "{}", page.serialize_html_async().await.unwrap() ); @@ -938,7 +938,7 @@ async fn child_browsing_context_attribute_navigation_preserves_local_history() - .unwrap() .contains(&format!( "data-child-location-after-back=\"{}\"", - server.url("/compat/window-child-browsing-context-target-name-b") + server.url("/compat/window-child-browsing-context-target-name-a") )), "{}", page.serialize_html_async().await.unwrap() @@ -949,7 +949,7 @@ async fn child_browsing_context_attribute_navigation_preserves_local_history() - .unwrap() .contains(&format!( "data-child-document-location-after-back=\"{}\"", - server.url("/compat/window-child-browsing-context-target-name-b") + server.url("/compat/window-child-browsing-context-target-name-a") )), "{}", page.serialize_html_async().await.unwrap() @@ -960,7 +960,7 @@ async fn child_browsing_context_attribute_navigation_preserves_local_history() - .unwrap() .contains(&format!( "data-child-current-entry-after-back=\"{}\"", - server.url("/compat/window-child-browsing-context-target-name-b") + server.url("/compat/window-child-browsing-context-target-name-a") )), "{}", page.serialize_html_async().await.unwrap() @@ -3987,7 +3987,7 @@ async fn child_browsing_context_fragment_navigation_persists_through_attribute_n page.serialize_html_async() .await .unwrap() - .contains("data-child-history-length=\"3\""), + .contains("data-child-history-length=\"2\""), "{}", page.serialize_html_async().await.unwrap() ); diff --git a/moli-core/tests/history_iframe.rs b/moli-core/tests/history_iframe.rs new file mode 100644 index 0000000000..9d916b949d --- /dev/null +++ b/moli-core/tests/history_iframe.rs @@ -0,0 +1,109 @@ +use anyhow::Result; +use moli_core::runtime::{Browser, BrowserConfig}; +use moli_test_support::FixtureServer; +use serde_json::{Value, json}; +use tokio::time::Duration; +use url::Url; + +async fn iframe_attribute_history(phase: &str, api: &str) -> Result { + let child = r#""#; + let child = serde_json::to_string(child)?.replace("", "<\\/script>"); + let markup = r#""# + .replace("__PHASE__", &serde_json::to_string(phase)?) + .replace("__API__", &serde_json::to_string(api)?) + .replace("__CHILD__", &child); + let server = FixtureServer::spawn().await?; + let browser = Browser::new(BrowserConfig::default())?; + let mut url = Url::parse(&server.url("/compat/child-dynamic-markup-document"))?; + url.query_pairs_mut().append_pair("markup", &markup); + let result = tokio::time::timeout(Duration::from_secs(10), async { + let mut page = browser.fetch(url.as_str()).await?; + page.evaluate_runtime_expression_with_await_async( + "finished.then(value => JSON.stringify(value))", + true, + ) + .await + }) + .await??; + let result = serde_json::from_str(result["value"].as_str().unwrap())?; + server.shutdown().await; + Ok(result) +} + +#[tokio::test(flavor = "multi_thread")] +async fn iframe_src_changes_replace_history_until_child_load_finishes() -> Result<()> { + for api in ["property", "setAttribute"] { + for phase in [ + "parser", + "DOMContentLoaded", + "load", + "pageshow", + "owner-load", + ] { + let result = iframe_attribute_history(phase, api).await?; + let readiness = match phase { + "parser" => "loading", + "DOMContentLoaded" => "interactive", + _ => "complete", + }; + assert_eq!( + result, + json!({"delta": 0, "readiness": readiness, "entries": ["destination"], + "index": 0, "activation": "replace"}), + "{phase}/{api}" + ); + } + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn iframe_src_changes_push_history_after_load_even_when_document_is_reopened() -> Result<()> { + for api in ["property", "setAttribute"] { + for phase in ["after-load", "reopen"] { + let result = iframe_attribute_history(phase, api).await?; + assert_eq!( + result, + json!({"delta": 1, "readiness": if phase == "reopen" { "loading" } else { "complete" }, + "entries": ["source", "destination"], "index": 1, "activation": "push"}), + "{phase}/{api}" + ); + } + } + Ok(()) +} diff --git a/moli-renderer-v8/src/native_bridge/element/url_attributes/iframe.rs b/moli-renderer-v8/src/native_bridge/element/url_attributes/iframe.rs index cbf0a95a91..516fde238f 100644 --- a/moli-renderer-v8/src/native_bridge/element/url_attributes/iframe.rs +++ b/moli-renderer-v8/src/native_bridge/element/url_attributes/iframe.rs @@ -38,8 +38,15 @@ pub(in crate::native_bridge) fn update_iframe_snapshot_navigation( handle, previous_seed_snapshot, ); - let replace_current = - runtime.child_browsing_context_is_on_initial_about_blank_entry(handle); + // Attribute navigation replaces an incompletely loaded Document, + // including inside its Window load/pageshow callbacks. readyState + // alone cannot distinguish those callbacks from a completed load. + let replace_current = runtime + .child_browsing_context_is_on_initial_about_blank_entry(handle) + || runtime + .child_browsing_context_document_handle(handle) + .and_then(|document| runtime.document_is_completely_loaded(document)) + == Some(false); if runtime.queue_child_browsing_context_navigation_from_existing_seed( handle, &navigation_target,