fix(forms): track image form owners

This commit is contained in:
ldm0
2026-09-09 06:38:21 +08:00
parent 58c874ae70
commit ceda16aef6
9 changed files with 98 additions and 20 deletions
@@ -3282,7 +3282,6 @@ html/webappapis/dynamic-markup-insertion/html-unsafe-methods/Document-parseHTMLU
html/webappapis/dynamic-markup-insertion/html-unsafe-methods/Document-parseHTMLUnsafe.html
html/webappapis/dynamic-markup-insertion/html-unsafe-methods/setHTMLUnsafe-xml.html
html/webappapis/scripting/event-loops/microtask_before_prepare_the_script_element-01.html
html/webappapis/scripting/events/compile-event-handler-lexical-scopes-form-owner.html
html/webappapis/scripting/events/event-handler-attributes-frameset-alt.html
html/webappapis/scripting/events/event-handler-attributes-frameset-window.html
html/webappapis/scripting/events/event-handler-attributes-window-frameset.html
@@ -6658,6 +6658,7 @@ html/webappapis/scripting/event-loops/microtask_after_script.html
html/webappapis/scripting/event-loops/task_microtask_ordering.html
html/webappapis/scripting/event-loops/update-the-rendering-resize-autofocus.html
html/webappapis/scripting/events/body-onload.html
html/webappapis/scripting/events/compile-event-handler-lexical-scopes-form-owner.html
html/webappapis/scripting/events/compile-event-handler-lexical-scopes.html
html/webappapis/scripting/events/compile-event-handler-symbol-unscopables.html
html/webappapis/scripting/events/event-handler-all-global-events.html
+15
View File
@@ -49,3 +49,18 @@ fn is_html_frame_owner_candidate(local_name: &str, namespace: &str) -> bool {
.into_iter()
.any(|name| local_name.eq_ignore_ascii_case(name))
}
fn is_builtin_reassociateable_form_associated_element(element: &Element) -> bool {
element.namespace() == "http://www.w3.org/1999/xhtml"
&& matches!(
element.local_name(),
"button" | "fieldset" | "input" | "object" | "output" | "select" | "textarea"
)
}
fn is_parser_form_association_candidate(element: &Element) -> bool {
// HTMLImageElement keeps a parser/ancestor form owner for legacy
// behavior, but is neither listed nor reassociateable.
is_builtin_reassociateable_form_associated_element(element)
|| (element.namespace() == "http://www.w3.org/1999/xhtml" && element.local_name() == "img")
}
+20 -6
View File
@@ -121,13 +121,20 @@ impl DomHost {
.is_some_and(is_listed_form_control_element)
}
pub fn builtin_form_associated_owner(&self, handle: DomHandle) -> Option<DomHandle> {
let element = self.node(handle).and_then(Node::as_element)?;
if !is_parser_form_association_candidate(element) {
return None;
}
if is_builtin_reassociateable_form_associated_element(element) {
return self.form_control_owner(handle);
}
self.parser_or_ancestor_form_owner(handle)
}
pub fn form_control_owner(&self, handle: DomHandle) -> Option<DomHandle> {
let element = self.node(handle).and_then(Node::as_element)?;
if !matches!(
element.local_name(),
"button" | "fieldset" | "input" | "object" | "output" | "select" | "textarea"
) || element.namespace() != "http://www.w3.org/1999/xhtml"
{
if !is_builtin_reassociateable_form_associated_element(element) {
return None;
}
@@ -145,7 +152,14 @@ impl DomHost {
.then_some(candidate);
}
if let Some(owner) = element.parser_associated_form_owner()
self.parser_or_ancestor_form_owner(handle)
}
fn parser_or_ancestor_form_owner(&self, handle: DomHandle) -> Option<DomHandle> {
if let Some(owner) = self
.node(handle)
.and_then(Node::as_element)
.and_then(Element::parser_associated_form_owner)
&& self.is_html_element_named(owner, "form")
&& self.root_node_handle(handle) == self.root_node_handle(owner)
{
@@ -92,6 +92,13 @@ impl DomHost {
&mut self,
handle: DomHandle,
) {
if !self
.node(handle)
.and_then(Node::as_element)
.is_some_and(is_builtin_reassociateable_form_associated_element)
{
return;
}
self.clear_parser_associated_form_owner(handle);
}
+1 -6
View File
@@ -114,12 +114,7 @@ impl DomHost {
else {
return false;
};
if element.namespace() != "http://www.w3.org/1999/xhtml"
|| !matches!(
element.local_name(),
"button" | "fieldset" | "input" | "object" | "output" | "select" | "textarea"
)
{
if !is_parser_form_association_candidate(element) {
return false;
}
element.set_parser_associated_form_owner(Some(form))
+39
View File
@@ -1427,6 +1427,45 @@ mod tests {
assert_eq!(host.form_control_owner(nested_input), Some(form));
}
#[test]
fn image_form_owner_tracks_parser_and_ancestor_without_becoming_listed() {
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
host.reset_html_document_shell();
let body = host.document_body_handle().expect("document body");
let parser_form = host.create_element("form");
let ancestor_form = host.create_element("form");
let image = host.create_parser_element_without_attributes(
"img".to_owned(),
"http://www.w3.org/1999/xhtml".to_owned(),
None,
);
assert!(host.set_attribute(ancestor_form, "id", "attribute-owner"));
assert!(host.append_child(body, parser_form));
assert!(host.append_child(body, ancestor_form));
assert!(host.associate_parser_form_owner(image, parser_form));
assert!(host.append_child(body, image));
assert_eq!(host.builtin_form_associated_owner(image), Some(parser_form));
assert_eq!(host.form_control_owner(image), None);
assert!(host.form_control_elements(parser_form).is_empty());
assert!(host.set_attribute(image, "form", "attribute-owner"));
assert_eq!(host.builtin_form_associated_owner(image), Some(parser_form));
assert!(host.append_child(ancestor_form, image));
assert_eq!(
host.builtin_form_associated_owner(image),
Some(ancestor_form)
);
assert_eq!(
host.node(image)
.and_then(Node::as_element)
.and_then(Element::parser_associated_form_owner),
None
);
assert!(host.form_control_elements(ancestor_form).is_empty());
}
#[test]
fn secondary_document_form_attribute_remains_authoritative() {
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
@@ -10,12 +10,9 @@ pub(crate) fn form_associated_form_owner(
if is_form_associated_custom_element_handle(runtime, handle) {
return custom_element_form_owner(runtime, handle, element);
}
if !is_builtin_form_associated_element(element) {
return None;
}
runtime
.dom_host()
.form_control_owner(handle)
.builtin_form_associated_owner(handle)
.filter(|candidate| {
runtime
.dom_host()
@@ -59,7 +56,7 @@ fn custom_element_form_owner(
None
}
fn is_builtin_form_associated_element(element: &crate::dom::native::Element) -> bool {
fn has_reflected_builtin_form_owner(element: &crate::dom::native::Element) -> bool {
element.namespace() == "http://www.w3.org/1999/xhtml"
&& matches!(
element.local_name(),
@@ -72,7 +69,7 @@ fn form_associated_reflected_form_owner(
handle: DomHandle,
) -> Option<DomHandle> {
let element = runtime.dom_host().node(handle).and_then(Node::as_element)?;
if !is_builtin_form_associated_element(element) {
if !has_reflected_builtin_form_owner(element) {
return None;
}
if let Some(form_id) = element.attribute("form")
@@ -18,6 +18,7 @@ fn event_attribute_handlers_use_html_scope_chain_and_report_compile_errors() {
<table><tbody><tr><td id="cell"><img id="cell-inner"></td></tr></tbody></table>
<form id="owner" onsubmit="return false">
<button id="button" type="button"><q id="button-inner"></q></button>
<img id="form-image" alt="form image">
</form>
<a id="error-inner"></a>
`;
@@ -63,6 +64,14 @@ fn event_attribute_handlers_use_html_scope_chain_and_report_compile_errors() {
`);
buttonInner.click();
const formImage = document.getElementById("form-image");
globalThis.elements = "global-elements";
formImage.setAttribute(
"onclick",
`globalThis.__imageFormScope = elements;`
);
formImage.click();
globalThis.__windowScope = null;
globalThis.__compileErrorEvents = 0;
document.body.bodyOwn = true;
@@ -84,6 +93,8 @@ fn event_attribute_handlers_use_html_scope_chain_and_report_compile_errors() {
return JSON.stringify({
cell: globalThis.__cellScope,
form: globalThis.__formScope,
imageForm: globalThis.__imageFormScope === form.elements,
imageIsNotListed: !Array.from(form.elements).includes(formImage),
window: globalThis.__windowScope,
errors: globalThis.__compileErrorEvents,
});
@@ -94,7 +105,7 @@ fn event_attribute_handlers_use_html_scope_chain_and_report_compile_errors() {
assert_eq!(
result,
r#"{"cell":["number","string","function","boolean","undefined","object","updated"],"form":["boolean","object","string","string","boolean","boolean","undefined","object"],"window":["undefined","function","undefined","string"],"errors":1}"#,
r#"{"cell":["number","string","function","boolean","undefined","object","updated"],"form":["boolean","object","string","string","boolean","boolean","undefined","object"],"imageForm":true,"imageIsNotListed":true,"window":["undefined","function","undefined","string"],"errors":1}"#,
);
}