PureXML/Tests/TreeNodeTests.swift
Mihaela Mihaljevic 15de0c5344 feat(model): mutable parent-aware tree (libxml2 tree.h parity)
Add PureXML.Model.TreeNode, a reference-type document tree that knows its
parent and siblings. Navigation goes up and sideways (parent, nextSibling,
previousSibling, ancestors, root, elementChildren, stringValue) and editing
is in place (append, insert before/after, removeFromParent, replace, copy).
Children are held strongly and the parent weakly; attaching a node detaches
it from any prior parent and a node can never become its own ancestor.
parseTree builds one from XML; TreeNode.node converts back for serialization.

Closes #31
2026-06-08 03:13:59 +02:00

135 lines
4.6 KiB
Swift

@testable import PureXML
import Testing
@Suite("Mutable tree")
struct TreeNodeTests {
private typealias Tree = PureXML.Model.TreeNode
// MARK: Navigation
@Test("Parent and sibling links are wired from a parsed tree")
func test_navigation() throws {
let tree = try PureXML.parseTree("<r><a/><b/><c/></r>")
let root = tree.firstChild
let children = root?.children ?? []
#expect(children.count == 3)
#expect(children[1].parent === root)
#expect(children[0].nextSibling === children[1])
#expect(children[2].previousSibling === children[1])
#expect(children[0].previousSibling == nil)
#expect(children[2].nextSibling == nil)
}
@Test("Ancestors and root walk upward")
func test_ancestors() throws {
let tree = try PureXML.parseTree("<r><a><b/></a></r>")
let deep = tree.firstChild?.firstChild?.firstChild
#expect(deep?.name?.localName == "b")
#expect(deep?.ancestors.map { $0.name?.localName ?? "#" } == ["a", "r", "#"])
#expect(deep?.root === tree)
}
@Test("stringValue concatenates descendant text and CDATA")
func test_stringValue() throws {
let tree = try PureXML.parseTree("<r>a<x>b</x><![CDATA[c]]></r>")
#expect(tree.firstChild?.stringValue == "abc")
}
@Test("elementChildren skips non-element nodes")
func test_elementChildren() throws {
let tree = try PureXML.parseTree("<r>text<a/><!--c--><b/></r>")
#expect(tree.firstChild?.elementChildren.map { $0.name?.localName } == ["a", "b"])
}
// MARK: Mutation
@Test("append moves a node and updates its parent")
func test_append() {
let root = Tree.element("r")
let child = Tree.element("a")
root.append(child)
#expect(child.parent === root)
#expect(root.children.count == 1)
}
@Test("Attaching a node detaches it from its previous parent")
func test_reparenting() {
let first = Tree.element("first")
let second = Tree.element("second")
let child = Tree.text("x")
first.append(child)
second.append(child)
#expect(first.children.isEmpty)
#expect(second.children.count == 1)
#expect(child.parent === second)
}
@Test("insert before and after place a node correctly")
func test_insertRelative() {
let root = Tree.element("r")
let first = Tree.element("a")
let last = Tree.element("c")
root.append(first)
root.append(last)
root.insert(Tree.element("b"), after: first)
root.insert(Tree.element("start"), before: first)
#expect(root.children.map { $0.name?.localName } == ["start", "a", "b", "c"])
}
@Test("removeFromParent detaches a node")
func test_remove() {
let root = Tree.element("r")
let child = Tree.element("a")
root.append(child)
child.removeFromParent()
#expect(root.children.isEmpty)
#expect(child.parent == nil)
}
@Test("replace swaps a node in place")
func test_replace() {
let root = Tree.element("r")
let old = Tree.element("old")
root.append(old)
old.replace(with: Tree.element("new"))
#expect(root.children.map { $0.name?.localName } == ["new"])
#expect(old.parent == nil)
}
@Test("copy is a deep clone sharing no nodes")
func test_copy() {
let root = Tree.element("r", children: [Tree.element("a", children: [Tree.text("x")])])
let clone = root.copy()
clone.firstChild?.firstChild?.value = "y"
#expect(root.firstChild?.firstChild?.value == "x")
#expect(clone.parent == nil)
#expect(clone.firstChild !== root.firstChild)
}
@Test("A node cannot be made its own descendant")
func test_noCycles() {
let parent = Tree.element("p")
let child = Tree.element("c")
parent.append(child)
child.append(parent)
#expect(child.children.isEmpty)
#expect(parent.children.count == 1)
}
// MARK: Round-trip
@Test("A parsed tree round-trips back to identical XML")
func test_roundTrip() throws {
let xml = "<r a=\"1\"><b>text</b><c/></r>"
let tree = try PureXML.parseTree(xml)
#expect(PureXML.serialize(tree.node, options: .compact) == xml)
}
@Test("Edits show up in the serialized output")
func test_editThenSerialize() throws {
let tree = try PureXML.parseTree("<r><a/></r>")
tree.firstChild?.append(Tree.element("b"))
#expect(PureXML.serialize(tree.node, options: .compact) == "<r><a/><b/></r>")
}
}