fix(forms): honor submission entry-list guards

This commit is contained in:
ldm0
2026-09-27 19:28:50 +08:00
parent d3465aad03
commit 4e4a43adad
4 changed files with 101 additions and 0 deletions
@@ -1671,6 +1671,10 @@ impl JsContextHost {
true
}
pub(crate) fn is_constructing_form_data_for(&self, form_handle: DomHandle) -> bool {
self.constructing_form_data_forms.contains(&form_handle)
}
pub(crate) fn end_form_data_construction(&mut self, form_handle: DomHandle) {
if let Some(index) = self
.constructing_form_data_forms
@@ -205,6 +205,7 @@ pub(crate) fn submit_form_with_submit_event(
let form_can_dispatch_submit_event = {
let runtime = unsafe { &*runtime_ptr };
form_is_connected_or_in_detached_document(runtime, form_handle)
&& !runtime.is_constructing_form_data_for(form_handle)
};
if !form_can_dispatch_submit_event {
return false;
@@ -576,6 +577,15 @@ pub(in crate::native_bridge) fn submit_form_default_action(
submitter: Option<DomHandle>,
user_initiated: bool,
) -> bool {
let form_can_submit = {
let runtime = unsafe { &*runtime_ptr };
form_is_connected_or_in_detached_document(runtime, form_handle)
&& !runtime.is_constructing_form_data_for(form_handle)
};
if !form_can_submit {
return false;
}
let method = {
let runtime = unsafe { &*runtime_ptr };
resolve_form_submission_method(runtime, form_handle, submitter)
@@ -604,6 +614,9 @@ pub(in crate::native_bridge) fn submit_form_default_action(
else {
return false;
};
if !form_is_connected_or_in_detached_document(unsafe { &*runtime_ptr }, form_handle) {
return false;
}
let special_target = target_name
.as_deref()
@@ -2562,6 +2562,89 @@ fn url_like_href_resolution_does_not_legacy_encode_fragment_question_mark() {
"https://href-fragment-gbk.test/search#frag?q=%E5%AE%B6%E5%B1%85"
);
}
#[test]
fn form_submission_returns_while_constructing_entry_list() {
let mut vm = new_storage_test_vm("https://form-entry-list-reentrant.test/page.html");
let result = vm
.eval(
r#"
(() => {
const parent = document.body || document.documentElement || document;
const form = document.createElement('form');
form.action = '/submitted';
form.innerHTML = '<input name=n value=v>';
parent.appendChild(form);
const outcomes = [];
let submitEvents = 0;
form.addEventListener('submit', event => {
submitEvents++;
event.preventDefault();
});
form.addEventListener('formdata', () => {
for (const submit of [
() => form.submit(),
() => form.requestSubmit()
]) {
try {
submit();
outcomes.push('returned');
} catch (error) {
outcomes.push(`threw:${error.name}`);
}
}
});
form.submit();
return JSON.stringify({ outcomes, submitEvents });
})()
"#,
)
.expect("form submission entry-list reentrancy probe should evaluate");
assert_eq!(
result,
r#"{"outcomes":["returned","returned"],"submitEvents":0}"#
);
assert_eq!(
vm.take_pending_location_navigation_with_seed()
.expect("outer form submission should still queue navigation")
.url
.as_str(),
"https://form-entry-list-reentrant.test/submitted?n=v"
);
}
#[test]
fn form_submission_aborts_when_formdata_listener_disconnects_form() {
let mut vm = new_storage_test_vm("https://form-disconnected-after-entry-list.test/page.html");
let result = vm
.eval(
r#"
(() => {
const parent = document.body || document.documentElement || document;
const form = document.createElement('form');
form.action = '/submitted';
form.innerHTML = '<input name=n value=v>';
parent.appendChild(form);
form.addEventListener('formdata', () => form.remove());
form.submit();
return String(form.isConnected);
})()
"#,
)
.expect("formdata disconnect submission probe should evaluate");
assert_eq!(result, "false");
assert!(
vm.take_pending_location_navigation_with_seed().is_none(),
"a form disconnected while constructing its entry list must not navigate"
);
}
#[tokio::test(flavor = "current_thread")]
async fn formdata_event_appended_entries_are_submitted_to_named_iframe() {
let server = StaticHttpServer::spawn(1).await;
@@ -31,6 +31,7 @@
const form = document.createElement("form");
form.target = "frame2";
form.action = "resources/a-page.html";
document.body.appendChild(form);
form.submit();
document.body.setAttribute(