PureXML/Tests/XPathAxisTests.swift
Mihaela Mihaljevic 6b7853f2d5 feat(xpath): all thirteen axes via parent-aware navigation
Rebuild the XPath engine on the parent-aware tree. Add an XPath node
abstraction (tree, attribute, namespace) with document order, and navigate
all thirteen axes: child, descendant, parent, ancestor, following-sibling,
preceding-sibling, following, preceding, attribute, namespace, self,
descendant-or-self, ancestor-or-self. Support the ./../@/// abbreviations
and processing-instruction() node tests. Reverse axes number nearest-first
for positional predicates; results return de-duplicated in document order.
The attribute axis excludes xmlns declarations; the namespace axis surfaces
in-scope namespace nodes.

Closes #40
2026-06-08 05:15:14 +02:00

103 lines
3.6 KiB
Swift

@testable import PureXML
import Testing
@Suite("XPath axes")
struct XPathAxisTests {
/// A small document with depth, siblings, attributes, and a namespace.
private func doc() throws -> PureXML.Model.Node {
try PureXML.parse(
"<root xmlns:k=\"urn:k\">"
+ "<a id=\"1\"><b><c/></b></a>"
+ "<a id=\"2\"><d/></a>"
+ "<e k:m=\"v\"/>"
+ "</root>",
)
}
private func names(_ path: String) throws -> [String] {
try PureXML.XPath.Query(path).evaluate(over: doc()).compactMap { selection in
switch selection {
case let .node(node): node.element?.name.description
case let .attribute(attribute): attribute.name.description
}
}
}
@Test("child axis selects immediate element children")
func test_child() throws {
#expect(try names("/root/child::a") == ["a", "a"])
}
@Test("descendant axis reaches any depth")
func test_descendant() throws {
#expect(try names("/root/descendant::c") == ["c"])
}
@Test("parent axis and .. climb one level")
func test_parent() throws {
#expect(try names("//c/parent::b") == ["b"])
#expect(try names("//c/..") == ["b"])
}
@Test("ancestor axis lists every enclosing element nearest first")
func test_ancestor() throws {
#expect(try names("//c/ancestor::*") == ["root", "a", "b"])
}
@Test("ancestor-or-self includes the context node")
func test_ancestorOrSelf() throws {
#expect(try names("//c/ancestor-or-self::*") == ["root", "a", "b", "c"])
}
@Test("following-sibling and preceding-sibling walk siblings")
func test_siblings() throws {
#expect(try names("/root/a[1]/following-sibling::*") == ["a", "e"])
#expect(try names("/root/e/preceding-sibling::*") == ["a", "a"])
}
@Test("following axis selects nodes after the context, excluding descendants")
func test_following() throws {
#expect(try names("//b/following::*") == ["a", "d", "e"])
}
@Test("preceding axis selects nodes before the context, excluding ancestors")
func test_preceding() throws {
#expect(try names("/root/e/preceding::*") == ["a", "b", "c", "a", "d"])
}
@Test("self axis keeps the context node")
func test_self() throws {
#expect(try names("//c/self::c") == ["c"])
}
@Test("descendant-or-self powers the // abbreviation")
func test_descendantOrSelf() throws {
#expect(try names("/root/a/descendant-or-self::*") == ["a", "b", "c", "a", "d"])
}
@Test("attribute axis selects attributes")
func test_attribute() throws {
#expect(try names("/root/a/attribute::id") == ["id", "id"])
#expect(try names("/root/a/@*") == ["id", "id"])
}
@Test("namespace declarations are not on the attribute axis")
func test_attributeExcludesNamespaceDeclaration() throws {
#expect(try names("/root/attribute::*").isEmpty)
}
@Test("namespace axis surfaces in-scope namespace nodes")
func test_namespace() throws {
let nodes = try PureXML.XPath.Query("/root/namespace::*").evaluate(over: doc())
let uris = nodes.map(\.stringValue).sorted()
#expect(uris.contains("urn:k"))
#expect(uris.contains("http://www.w3.org/XML/1998/namespace"))
}
@Test("Results come back in document order regardless of axis direction")
func test_documentOrder() throws {
// ancestor axis is reverse, but the returned node-set is document-ordered.
#expect(try names("//c/ancestor-or-self::node()") == ["root", "a", "b", "c"])
}
}