mirror of
https://github.com/lexmount/moli.git
synced 2026-11-02 16:07:11 +00:00
Keep submit and formdata events synchronous while navigation runs as a DOM-manipulation task. Capture the request, submitter, target and history handling; coalesce replaced submissions without extra task turns. Preserve popup POST requests, cross-origin and sandbox boundaries, and javascript URL replacement ordering. Follow form navigation that starts during document creation and cover reentrant, adopted and same-URL forms. Validation: cargo fmt --all; workspace all-target/all-feature clippy with warnings denied; cargo nextest run --no-fail-fast (20,002 passed, 16 skipped). Two additional official WPT cases pass with no passing-case regressions across the form and Navigation API suites.
195 lines
7.8 KiB
Rust
195 lines
7.8 KiB
Rust
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<Value> {
|
|
let server = FixtureServer::spawn().await?;
|
|
let browser = Browser::new(BrowserConfig::default())?;
|
|
let source = format!(
|
|
r#"<!doctype html><body><form action='/net/echo'>
|
|
<input name='q' value='submitted'></form>
|
|
<a id='go' href='javascript:run()'>go</a><script>
|
|
function run() {{
|
|
{action}
|
|
parent.scriptContinued = true;
|
|
return '<body id="completion">replacement';
|
|
}}
|
|
</script>"#
|
|
);
|
|
let parent = format!(
|
|
r#"<!doctype html><body><script>
|
|
let finish;
|
|
const loaded = new Promise(resolve => finish = resolve);
|
|
let otherLoaded = Promise.resolve(null);
|
|
if ({other_target}) {{
|
|
const other = document.createElement('iframe');
|
|
other.name = 'other';
|
|
otherLoaded = new Promise(resolve => {{
|
|
other.onload = () => {{
|
|
if (other.contentWindow.location.pathname === '/net/echo')
|
|
resolve(JSON.parse(other.contentDocument.body.textContent));
|
|
}};
|
|
}});
|
|
document.body.append(other);
|
|
}}
|
|
window.finished = Promise.all([loaded, otherLoaded]);
|
|
const frame = document.createElement('iframe');
|
|
frame.name = 'source';
|
|
frame.onload = () => {{
|
|
const originalDocument = frame.contentDocument;
|
|
frame.onload = () => finish({{
|
|
path: frame.contentWindow.location.pathname,
|
|
search: frame.contentWindow.location.search,
|
|
body: frame.contentDocument.body.textContent,
|
|
bodyId: frame.contentDocument.body.id,
|
|
documentChanged: frame.contentDocument !== originalDocument,
|
|
scriptContinued
|
|
}});
|
|
setTimeout(() => frame.contentDocument.getElementById('go').click(), 0);
|
|
}};
|
|
frame.src = {};
|
|
document.body.append(frame);
|
|
</script>"#,
|
|
serde_json::to_string(&markup_url(&server, &source))?.replace("</script>", "<\\/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:'<body id=successor>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';",
|
|
] {
|
|
let result = child_javascript_url_navigation(action, false).await?;
|
|
assert_eq!(result[0]["bodyId"], "completion", "{action}: {result}");
|
|
assert_eq!(result[0]["body"], "replacement");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn canceled_planned_form_navigation_suppresses_javascript_string_replacement() -> Result<()> {
|
|
let server = FixtureServer::spawn().await?;
|
|
let browser = Browser::new(BrowserConfig::default())?;
|
|
let source = markup_url(
|
|
&server,
|
|
r#"<!doctype html><body id=original>
|
|
<form action='/net/echo'><input name=q value=submitted></form>
|
|
<a id=go href="javascript:(document.forms[0].submit(), '<body id=completion>replacement')">go</a>"#,
|
|
);
|
|
let parent = format!(
|
|
r#"<!doctype html><body><script>
|
|
window.finished = new Promise(resolve => {{
|
|
const frame = document.createElement('iframe');
|
|
frame.onload = () => {{
|
|
const original = frame.contentDocument;
|
|
const nav = frame.contentWindow.navigation;
|
|
nav.onnavigate = event => event.preventDefault();
|
|
nav.onnavigateerror = event => resolve({{
|
|
sameDocument: frame.contentDocument === original,
|
|
bodyId: frame.contentDocument.body.id,
|
|
error: event.error.name
|
|
}});
|
|
setTimeout(() => original.getElementById('go').click(), 0);
|
|
}};
|
|
frame.src = {};
|
|
document.body.appendChild(frame);
|
|
}});
|
|
</script>"#,
|
|
serde_json::to_string(&source)?.replace("</script>", "<\\/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(JSON.stringify)", true)
|
|
.await
|
|
})
|
|
.await??;
|
|
let observed: Value = serde_json::from_str(result["value"].as_str().unwrap())?;
|
|
assert_eq!(
|
|
observed,
|
|
serde_json::json!({"sameDocument":true,"bodyId":"original","error":"AbortError"})
|
|
);
|
|
server.shutdown().await;
|
|
Ok(())
|
|
}
|