fix(dom): keep <head> content when setting documentElement.innerHTML

Setting innerHTML on the <html> element parses the fragment in its own context
(added in a2fd4d1), which runs html5ever's "before head" insertion mode and
synthesizes both <head> and <body> under the fragment root. The importer's
helper walked into the root <html> and, if it found a <body> child, returned
that body -- so import_children_from imported only the body's children and any
assigned <head> content (<title>, <meta>, <link>) was silently dropped.

A <body> child of the fragment root only appears in this <html>-context case;
every other element context leaves parsed content directly under the root and
already fell through to returning it. Rename find_body_or_root -> fragment_root
and return the root <html> element so both head and body children import.
Behavior for all non-<html> contexts is unchanged.

Fixes #556
This commit is contained in:
Andrey Mnatsakanov committed 2026-08-04 17:41:57 +02:00
1 parent f0541f40a4
commit d0d8617ca6
3 files changed
+45 -9

No files matched your search

+1 -1
View File
@@ -431,7 +431,7 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
Some(name) => obscura_dom::parse_fragment_with_context(&arg2, name),
None => obscura_dom::parse_fragment(&arg2),
};
let import_root = fragment.find_body_or_root();
let import_root = fragment.fragment_root();
dom.import_children_from(target, &fragment, import_root);
}
"true".into()
+32
View File
@@ -2122,6 +2122,38 @@ mod tests {
);
}
/// Setting innerHTML on the <html> element parses in the "before head"
/// insertion mode, which synthesizes head and body. The importer must keep
/// both; it previously returned the synthesized body and dropped the head
/// (so a <title>/<meta> assigned this way vanished).
#[test]
fn documentelement_inner_html_keeps_head_and_body() {
let mut rt = setup_runtime("<html><head></head><body></body></html>");
let v = rt
.evaluate(
"(function(){ document.documentElement.innerHTML = '<head><title>T</title></head><body><p>hi</p></body>'; \
var t = document.querySelector('title'); var p = document.querySelector('p'); \
return (t ? t.textContent : 'no-title') + '|' + (p ? p.textContent : 'no-p'); })()",
)
.unwrap();
assert_eq!(v, serde_json::json!("T|hi"));
}
/// Regression guard: innerHTML on an ordinary element still imports the
/// parsed nodes directly (no head/body is synthesized for a div context),
/// so the fix above must not change the common case.
#[test]
fn ordinary_element_inner_html_imports_content_directly() {
let mut rt = setup_runtime("<html><body><div id=\"d\"></div></body></html>");
let v = rt
.evaluate(
"(function(){ var d=document.getElementById('d'); d.innerHTML='<span>a</span><span>b</span>'; \
return d.children.length + '|' + d.textContent; })()",
)
.unwrap();
assert_eq!(v, serde_json::json!("2|ab"));
}
/// Issue #463: the same must hold for a template that arrives via innerHTML
/// rather than the initial document parse — that is how most frameworks
/// inject templates.