fix(js): clone nodes structurally instead of via div.innerHTML
cloneNode(true) round-tripped through a throwaway <div>'s innerHTML, so the
HTML fragment parser discarded elements that are not valid children of <div>
(<tr>, <td>, <option>, <tbody>, …) — createElement('tr').cloneNode(true)
returned null — and JS-set inline styles were lost. The shallow path also
re-derived attributes by regex-scraping serialized HTML.
Clone structurally with real DOM APIs: shallow-clone each node via
createElement/createElementNS plus a getAttributeNames() attribute copy (no
parsing context, every attribute preserved), then descend into children with an
explicit stack so a deep subtree cannot overflow the JS stack. Text, Comment,
DocumentFragment and <template> contents are handled.
Fixes #490
This commit is contained in:
1 parent
e314b9b0f3
commit
491ecfb0f1
2 files changed
+104
-23
No files matched your search
@@ -1317,6 +1317,58 @@ mod tests {
|
||||
rt
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_node_deep_preserves_context_sensitive_elements() {
|
||||
// A <tr> is not a valid child of <div>, so cloning through a throwaway
|
||||
// <div>.innerHTML dropped it and returned null. A structural clone keeps it.
|
||||
let mut rt = setup_runtime("<html><body></body></html>");
|
||||
let tag = rt
|
||||
.evaluate("(document.createElement('tr').cloneNode(true) || {}).tagName || 'NULL'")
|
||||
.unwrap();
|
||||
assert_eq!(tag, serde_json::json!("TR"));
|
||||
let td = rt
|
||||
.evaluate("(document.createElement('td').cloneNode(true) || {}).tagName || 'NULL'")
|
||||
.unwrap();
|
||||
assert_eq!(td, serde_json::json!("TD"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_node_deep_copies_children_and_attributes() {
|
||||
let mut rt = setup_runtime(r#"<html><body><ul id="l"><li class="a">one</li><li class="b">two</li></ul></body></html>"#);
|
||||
let out = rt
|
||||
.evaluate(
|
||||
"(function(){var c=document.getElementById('l').cloneNode(true); return c.children.length + '|' + c.children[0].className + '|' + c.children[1].textContent;})()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(out, serde_json::json!("2|a|two"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_node_deep_preserves_table_rows() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<html><body><table id="t"><tbody><tr><td>1</td><td>2</td></tr></tbody></table></body></html>"#,
|
||||
);
|
||||
// Navigate the detached clone directly (querySelector does not traverse
|
||||
// detached subtrees). tbody > tr > (td, td).
|
||||
let out = rt
|
||||
.evaluate(
|
||||
"(function(){var tb=document.querySelector('#t tbody').cloneNode(true); var tr=tb.children[0]; return tr.tagName + '|' + tr.children.length + '|' + tr.children[1].textContent;})()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(out, serde_json::json!("TR|2|2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_node_shallow_copies_attributes_without_children() {
|
||||
let mut rt = setup_runtime(r#"<html><body><div id="d" data-x="7"><span>kid</span></div></body></html>"#);
|
||||
let out = rt
|
||||
.evaluate(
|
||||
"(function(){var c=document.getElementById('d').cloneNode(false); return c.getAttribute('data-x') + '|' + c.childNodes.length;})()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(out, serde_json::json!("7|0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_document_title() {
|
||||
let mut rt = setup_runtime("<html><head><title>Test</title></head><body></body></html>");
|
||||
|
||||
Reference in new issue
Block a user