From 621afa45f4dc74ef780c33988694ed71f4e30039 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 10 Sep 2026 08:21:15 +0800 Subject: [PATCH] fix: preserve child navigations started by javascript URLs Check the navigation load as well as the Document owner before committing a javascript URL string result. A form submission or Location navigation started during script execution must keep its pending request. Add four Browser integration tests covering 13 scenarios, including GET, POST, named targets, successor javascript URLs, unrelated frames, and canceled navigation. Record the newly passing jsurl-form-submit WPT. Validation: cargo fmt --all; workspace all-targets all-features Clippy with -D warnings; cargo nextest run --no-fail-fast (17,901 passed, 13 skipped). Focused WPT: 103 cases, one new pass, no regressions. --- .../wpt-cross-current/passed-cases.txt | 1 + moli-core/tests/javascript_url_navigation.rs | 149 ++++++++++++++++++ .../child_frame_navigation/commit.rs | 7 +- 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 moli-core/tests/javascript_url_navigation.rs diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index dd1f2bbc93..9dde7258e6 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -6010,6 +6010,7 @@ html/semantics/forms/form-submission-0/form-double-submit.html html/semantics/forms/form-submission-0/form-submission-algorithm.html html/semantics/forms/form-submission-0/form-submit-iframe-then-location-navigate.html html/semantics/forms/form-submission-0/getactionurl.html +html/semantics/forms/form-submission-0/jsurl-form-submit.tentative.html html/semantics/forms/form-submission-0/jsurl-navigation-then-form-submit.html html/semantics/forms/form-submission-0/newline-normalization.html html/semantics/forms/form-submission-0/reparent-form-during-planned-navigation-task.html diff --git a/moli-core/tests/javascript_url_navigation.rs b/moli-core/tests/javascript_url_navigation.rs new file mode 100644 index 0000000000..3a1e2a71f9 --- /dev/null +++ b/moli-core/tests/javascript_url_navigation.rs @@ -0,0 +1,149 @@ +use anyhow::Result; +use moli_core::runtime::{Browser, BrowserConfig}; +use moli_test_support::FixtureServer; +use serde_json::Value; +use tokio::time::Duration; +use url::Url; + +fn markup_url(server: &FixtureServer, markup: &str) -> String { + let mut url = Url::parse(&server.url("/compat/child-dynamic-markup-document")).unwrap(); + url.query_pairs_mut().append_pair("markup", markup); + url.into() +} + +async fn child_javascript_url_navigation(action: &str, other_target: bool) -> Result { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(BrowserConfig::default())?; + let source = format!( + r#"
+
+ go"# + ); + let parent = format!( + r#""#, + serde_json::to_string(&markup_url(&server, &source))?.replace("", "<\\/script>") + ); + let result = tokio::time::timeout(Duration::from_secs(10), async { + let mut page = browser.fetch(&markup_url(&server, &parent)).await?; + page.evaluate_runtime_expression_with_await_async( + "finished.then(value => JSON.stringify(value))", + true, + ) + .await + }) + .await??; + let result: Value = serde_json::from_str(result["value"].as_str().unwrap())?; + server.shutdown().await; + assert_eq!(result[0]["documentChanged"], true, "{action}: {result}"); + assert_eq!(result[0]["scriptContinued"], true, "{action}: {result}"); + Ok(result) +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_javascript_url_string_does_not_overtake_its_form_submission() -> Result<()> { + for method in ["get", "post"] { + for target in ["", "_self", "source"] { + let action = format!( + "document.forms[0].method = {method:?}; \ + document.forms[0].target = {target:?}; document.forms[0].submit();" + ); + let result = child_javascript_url_navigation(&action, false).await?; + // Observe the first load after the javascript URL. A temporary + // replacement document would fire an extra load at the old URL. + assert_eq!(result[0]["path"], "/net/echo", "{action}: {result}"); + let response: Value = serde_json::from_str(result[0]["body"].as_str().unwrap())?; + assert_eq!(response["method"], method.to_uppercase()); + if method == "post" { + assert_eq!(result[0]["search"], ""); + } else { + assert_eq!(result[0]["search"], "?q=submitted"); + } + } + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_javascript_url_string_does_not_overtake_its_location_navigation() -> Result<()> { + let result = + child_javascript_url_navigation("location.href = '/net/echo?q=location';", false).await?; + assert_eq!(result[0]["path"], "/net/echo", "{result}"); + assert_eq!(result[0]["search"], "?q=location"); + + let result = child_javascript_url_navigation( + "location.href = \"javascript:'successor'\";", + false, + ) + .await?; + assert_eq!(result[0]["bodyId"], "successor", "{result}"); + assert_eq!(result[0]["body"], "successor"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_javascript_url_string_survives_navigation_to_another_frame() -> Result<()> { + for method in ["get", "post"] { + let action = format!( + "document.forms[0].method = {method:?}; \ + document.forms[0].target = 'other'; document.forms[0].submit();" + ); + let result = child_javascript_url_navigation(&action, true).await?; + assert_eq!(result[0]["bodyId"], "completion", "{action}: {result}"); + assert_eq!(result[0]["body"], "replacement"); + assert_eq!(result[1]["method"], method.to_uppercase()); + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_javascript_url_string_survives_without_a_new_cross_document_navigation() -> Result<()> +{ + for action in [ + "document.body.dataset.changed = 'yes';", + "location.hash = 'fragment';", + "navigation.onnavigate = event => event.preventDefault(); document.forms[0].submit();", + ] { + let result = child_javascript_url_navigation(action, false).await?; + assert_eq!(result[0]["bodyId"], "completion", "{action}: {result}"); + assert_eq!(result[0]["body"], "replacement"); + } + Ok(()) +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs index c06228952d..6ce9291890 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs @@ -643,7 +643,12 @@ impl JsContextHost { preserve_window_event_state: bool, ) -> FrameDocumentJavascriptUrlPostExecutionApplication { let attempted_script_job = true; - if !self.frame_document_task_owner_is_current(target.child_handle(), target.task_owner()) { + // The script can start another navigation without replacing its + // Document yet. Its completion must not clear that newer request. + if !self.frame_document_task_owner_is_current(target.child_handle(), target.task_owner()) + || self.current_child_navigation_load(target.child_handle()) + != Some(target.navigation_load()) + { return FrameDocumentJavascriptUrlPostExecutionApplication { attempted_script_job, failed_script_job: false,