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,