diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 8e70cb3..ac72bba 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -2713,11 +2713,17 @@ class Document extends Node { } return null; }, + // DOM 6.1 "parentNode" (issue #475). The old version looked only at the + // immediate parent, so it couldn't climb past a skipped ancestor; it also + // excluded `root` as a result yet stepped to root's own parent when + // currentNode was root — returning a node OUTSIDE the walker's subtree. + // The loop's `node !== this.root` guard is what keeps the walk inside + // root while still allowing root itself to be returned. parentNode() { - let parent = this.currentNode.parentNode; - if (parent && parent !== this.root && this._accept(parent)) { - this.currentNode = parent; - return parent; + let node = this.currentNode; + while (node && node !== this.root) { + node = node.parentNode; + if (node && this._accept(node)) { this.currentNode = node; return node; } } return null; }, diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index ca7ade2..dfeed3b 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -1513,6 +1513,75 @@ mod tests { /// Issue #461: NodeIterator has no subtree pruning — DOM 6.2 says /// FILTER_REJECT behaves as FILTER_SKIP there. The shared walker must not + /// Issue #475: parentNode() must never surface a node above `root`. With + /// currentNode at root, the old guard stepped to root's own parent and + /// returned it — escaping the walker's subtree entirely. + #[test] + fn tree_walker_parent_node_does_not_escape_above_root() { + let mut rt = setup_runtime(r#"
"#); + let result = rt + .evaluate( + r#" + const root = document.getElementById('root'); + const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + const escaped = w.parentNode(); + return [escaped, w.currentNode.id]; + "#, + ) + .unwrap(); + // No parent within the subtree, and currentNode stays put at root. + assert_eq!(result, serde_json::json!([null, "root"])); + } + + /// Issue #475: when the accepted ancestor is `root` itself, parentNode() + /// returns it and moves currentNode there — the old `parent !== root` guard + /// wrongly excluded it. + #[test] + fn tree_walker_parent_node_can_return_the_root() { + let mut rt = setup_runtime(r#""#); + let result = rt + .evaluate( + r#" + const root = document.getElementById('root'); + const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + w.currentNode = root.querySelector('a'); + const p = w.parentNode(); + return [p ? p.id : null, w.currentNode === root]; + "#, + ) + .unwrap(); + assert_eq!(result, serde_json::json!(["root", true])); + } + + /// Issue #475: parentNode() climbs past a skipped ancestor to the first + /// accepted one, instead of stopping at the immediate parent. + #[test] + fn tree_walker_parent_node_climbs_past_skipped_ancestors() { + let mut rt = setup_runtime( + r#""#, + ); + let result = rt + .evaluate( + r#" + const root = document.getElementById('root'); + const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, { + acceptNode(n) { + return n.tagName === 'SECTION' + ? NodeFilter.FILTER_SKIP + : NodeFilter.FILTER_ACCEPT; + } + }); + w.currentNode = root.querySelector('a'); + const p = w.parentNode(); + return p ? p.id : null; + "#, + ) + .unwrap(); + // 's parent