fix(js): implement a real NodeIterator instead of aliasing TreeWalker (#467)

createNodeIterator returned a TreeWalker with a flag on it. The two are
different interfaces, and the alias got two things wrong.

An iterator's pointer starts *before* its root, so the first nextNode()
returns the root itself; a TreeWalker starts *at* its root and never
returns it. So createNodeIterator(el, SHOW_ELEMENT) silently skipped
exactly the element it was rooted at:

    <div id=root><a></a></div>  ->  ["A"]  (Chrome: ["DIV","A"])

It also exposed the wrong surface: no referenceNode,
pointerBeforeReferenceNode or detach (older library code still calls
detach and hit "not a function"), while leaking currentNode, firstChild,
parentNode and the rest of TreeWalker's movers.

Implemented DOM 6.2 "traverse" directly. Keeping the pointer as
(referenceNode, pointerBeforeReferenceNode) rather than a single cursor
is what makes a direction change re-yield the current node instead of
stepping over it, and a failed traversal leaves both untouched so the
iterator stays resumable either way.

The backward step is a new `DomTree::prev_in_subtree`, mirroring
`next_in_subtree`. It can live entirely in the DOM layer because a
NodeIterator prunes nothing -- unlike the TreeWalker's backward walk
(#462), where FILTER_REJECT forces the filter to be consulted at every
step of the descent.

FILTER_REJECT still behaves as FILTER_SKIP here, which the existing test
keeps covering; its expectation gains the root node the alias used to
drop. The TreeWalker's now-dead `_rejectIsSkip` branch is removed.
This commit is contained in:
Andrey Mnatsakanov committed 2026-07-24 08:34:33 +02:00
1 parent 485497393d
commit ab3ca26046
4 files changed
+224 -11

No files matched your search

+100 -1
View File
@@ -1538,7 +1538,106 @@ mod tests {
"#,
)
.unwrap();
assert_eq!(result, serde_json::json!(["P", "A"]));
// The rejected <section> is skipped but not pruned, so <p> still shows.
// The leading root is #467: an iterator yields the node it is rooted at.
assert_eq!(result, serde_json::json!(["DIV", "P", "A"]));
}
/// Issue #467: a NodeIterator starts *before* its root, so the first
/// nextNode() returns the root itself. Aliasing createTreeWalker silently
/// dropped exactly the element the iterator was rooted at.
#[test]
fn node_iterator_yields_the_root_node_first() {
let mut rt = setup_runtime(r#"<div id="root"><a></a></div>"#);
let result = rt
.evaluate(
r#"
const root = document.getElementById('root');
const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT);
const seen = [];
let node;
while ((node = it.nextNode())) seen.push(node.tagName);
return seen;
"#,
)
.unwrap();
assert_eq!(result, serde_json::json!(["DIV", "A"]));
}
/// Issue #467: the NodeIterator interface surface, and that TreeWalker-only
/// members are not exposed on it.
#[test]
fn node_iterator_exposes_its_own_interface() {
let mut rt = setup_runtime(r#"<div id="root"><a></a></div>"#);
let result = rt
.evaluate(
r#"
const root = document.getElementById('root');
const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT);
const before = [it.referenceNode === root, it.pointerBeforeReferenceNode];
it.nextNode();
return [
before,
typeof it.detach,
it.detach() === undefined,
typeof it.previousNode,
it.root === root,
it.whatToShow,
// TreeWalker-only members must not leak onto a NodeIterator.
typeof it.currentNode,
typeof it.firstChild,
typeof it.parentNode,
// The pointer advanced past the root it just returned.
[it.referenceNode.tagName, it.pointerBeforeReferenceNode],
];
"#,
)
.unwrap();
assert_eq!(
result,
serde_json::json!([
[true, true],
"function",
true,
"function",
true,
1,
"undefined",
"undefined",
"undefined",
["DIV", false]
])
);
}
/// Issue #467: previousNode() retraces the iterator, and the root is the
/// last node it yields going backwards.
#[test]
fn node_iterator_previous_node_retraces_the_walk() {
let mut rt = setup_runtime(r#"<div id="root"><a><b></b></a><c></c></div>"#);
let result = rt
.evaluate(
r#"
const root = document.getElementById('root');
const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT);
const forward = [];
let node;
while ((node = it.nextNode())) forward.push(node.tagName);
const backward = [];
while ((node = it.previousNode())) backward.push(node.tagName);
return [forward, backward];
"#,
)
.unwrap();
// Forward ends on <c>; going back re-yields <c> (the pointer sits after
// it), then the rest in reverse, root included.
assert_eq!(
result,
serde_json::json!([
["DIV", "A", "B", "C"],
["C", "B", "A", "DIV"]
])
);
}
#[test]