Merge pull request #465 from mnaza/fix/treewalker-previousnode-462
fix(js): walk reverse document order in TreeWalker.previousNode() (#462)
This commit is contained in:
4 files changed
+271
-19
No files matched your search
@@ -1378,6 +1378,169 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Issue #461: FILTER_REJECT must prune the rejected node's whole subtree,
|
||||
/// while FILTER_SKIP only skips the node and leaves descendants eligible.
|
||||
/// Collapsing both into "not accepted" let a TreeWalker yield nodes from
|
||||
/// inside a subtree the page explicitly rejected.
|
||||
#[test]
|
||||
fn tree_walker_filter_reject_prunes_the_whole_subtree() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<div id="root"><section><p>deep</p></section><a></a></div>"#,
|
||||
);
|
||||
rt.run_page_init();
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
function walk(verdict) {
|
||||
const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode(node) {
|
||||
return node.tagName === 'SECTION' ? verdict : NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
const seen = [];
|
||||
let node;
|
||||
while ((node = w.nextNode())) seen.push(node.tagName);
|
||||
return seen;
|
||||
}
|
||||
return [walk(NodeFilter.FILTER_REJECT), walk(NodeFilter.FILTER_SKIP)];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// REJECT drops <p> with its <section> parent; SKIP drops only <section>.
|
||||
assert_eq!(result, serde_json::json!([["A"], ["P", "A"]]));
|
||||
}
|
||||
|
||||
/// Issue #462: previousNode() must walk reverse document order until a node
|
||||
/// is accepted, not give up as soon as the first candidate is filtered out.
|
||||
#[test]
|
||||
fn previous_node_walks_reverse_document_order() {
|
||||
let mut rt = setup_runtime(r#"<div id="root"><a><b></b></a><c></c></div>"#);
|
||||
rt.run_page_init();
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode(node) {
|
||||
return node.tagName === 'B'
|
||||
? NodeFilter.FILTER_SKIP
|
||||
: NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
const forward = [];
|
||||
let node;
|
||||
while ((node = w.nextNode())) forward.push(node.tagName);
|
||||
const backward = [];
|
||||
while ((node = w.previousNode())) backward.push(node.tagName);
|
||||
return [forward, backward];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// From <c>, the previous sibling's deepest last child <b> is skipped, so
|
||||
// the walk must keep going up to <a> instead of returning null.
|
||||
assert_eq!(result, serde_json::json!([["A", "C"], ["A"]]));
|
||||
}
|
||||
|
||||
/// Issue #462: a backward walk must retrace a forward walk exactly, and stop
|
||||
/// at the root without ever returning it.
|
||||
#[test]
|
||||
fn previous_node_retraces_a_full_forward_walk() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<div id="root"><section><p>one</p><span></span></section><a><b></b></a></div>"#,
|
||||
);
|
||||
rt.run_page_init();
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
const forward = [];
|
||||
let node;
|
||||
while ((node = w.nextNode())) forward.push(node.tagName);
|
||||
const backward = [];
|
||||
while ((node = w.previousNode())) backward.push(node.tagName);
|
||||
backward.reverse();
|
||||
// previousNode never yields root, and never yields the node the
|
||||
// forward walk ended on, so compare against forward minus its last.
|
||||
// A failed traversal leaves currentNode untouched (DOM 6.1), so
|
||||
// it stays on the last node previousNode did return.
|
||||
return [forward, backward, w.currentNode.tagName];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!([
|
||||
["SECTION", "P", "SPAN", "A", "B"],
|
||||
["SECTION", "P", "SPAN", "A"],
|
||||
"SECTION"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/// Issue #462: FILTER_REJECT prunes a subtree in the backward direction too
|
||||
/// — the descent into a rejected node's last children must stop.
|
||||
#[test]
|
||||
fn previous_node_honours_filter_reject_subtree_pruning() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<div id="root"><a></a><section><p>deep</p></section><c></c></div>"#,
|
||||
);
|
||||
rt.run_page_init();
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode(node) {
|
||||
return node.tagName === 'SECTION'
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
while (w.nextNode()) { /* advance to the last accepted node */ }
|
||||
const backward = [];
|
||||
let node;
|
||||
while ((node = w.previousNode())) backward.push(node.tagName);
|
||||
return backward;
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// <p> lives inside the rejected <section>, so the backward walk from <c>
|
||||
// must jump straight to <a>.
|
||||
assert_eq!(result, serde_json::json!(["A"]));
|
||||
}
|
||||
|
||||
/// Issue #461: NodeIterator has no subtree pruning — DOM 6.2 says
|
||||
/// FILTER_REJECT behaves as FILTER_SKIP there. The shared walker must not
|
||||
/// leak TreeWalker's pruning into it.
|
||||
#[test]
|
||||
fn node_iterator_treats_filter_reject_as_skip() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<div id="root"><section><p>deep</p></section><a></a></div>"#,
|
||||
);
|
||||
rt.run_page_init();
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode(node) {
|
||||
return node.tagName === 'SECTION'
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
const seen = [];
|
||||
let node;
|
||||
while ((node = it.nextNode())) seen.push(node.tagName);
|
||||
return seen;
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, serde_json::json!(["P", "A"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_child_flattens_document_fragment() {
|
||||
let mut rt = setup_runtime(r#"<main id="host"></main>"#);
|
||||
|
||||
Reference in new issue
Block a user