mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 16:01:01 +00:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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<Value> {
|
||||
let child = r#"<!doctype html><body><script>
|
||||
const phase = new URL(location.href).searchParams.get('phase');
|
||||
const go = () => parent.navigateFrame();
|
||||
if (phase === 'parser') go();
|
||||
else if (phase === 'DOMContentLoaded') document.addEventListener(phase, go, {once: true});
|
||||
else if (phase === 'load' || phase === 'pageshow') addEventListener(phase, go, {once: true});
|
||||
else if (phase !== 'owner-load') addEventListener('load', () => setTimeout(() => {
|
||||
if (phase === 'reopen') document.open();
|
||||
go();
|
||||
}, 0), {once: true});
|
||||
</script>"#;
|
||||
let child = serde_json::to_string(child)?.replace("</script>", "<\\/script>");
|
||||
let markup = r#"<!doctype html><body><script>
|
||||
window.finished = new Promise(resolve => window.finish = resolve);
|
||||
const phase = __PHASE__;
|
||||
const api = __API__;
|
||||
const initialLength = history.length;
|
||||
const frame = document.createElement('iframe');
|
||||
const destination = new URL('/compat/child-dynamic-markup-document?label=destination&markup=' +
|
||||
encodeURIComponent('<!doctype html><body>destination'), location.href).href;
|
||||
window.navigateFrame = () => {
|
||||
window.navigationReadiness = frame.contentDocument.readyState;
|
||||
if (api === 'property') frame.src = destination;
|
||||
else frame.setAttribute('src', destination);
|
||||
};
|
||||
frame.onload = () => {
|
||||
if (frame.contentWindow.location.href === destination) setTimeout(() => {
|
||||
const win = frame.contentWindow;
|
||||
finish({delta: history.length - initialLength, readiness: navigationReadiness,
|
||||
entries: win.navigation.entries().map(entry => new URL(entry.url).searchParams.get('label')),
|
||||
index: win.navigation.currentEntry.index, activation: win.navigation.activation.navigationType});
|
||||
}, 0);
|
||||
else if (phase === 'owner-load') navigateFrame();
|
||||
};
|
||||
frame.src = '/compat/child-dynamic-markup-document?label=source&phase=' + phase +
|
||||
'&markup=' + encodeURIComponent(__CHILD__);
|
||||
document.body.append(frame);
|
||||
</script>"#
|
||||
.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(())
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user