diff --git a/PublicAPITests/PublicEntryPointTests.swift b/PublicAPITests/PublicEntryPointTests.swift
new file mode 100644
index 0000000..1f94c71
--- /dev/null
+++ b/PublicAPITests/PublicEntryPointTests.swift
@@ -0,0 +1,64 @@
+import PureXML
+import Testing
+
+/// Exercises the public entry points in `API.swift` that the rest of the suite
+/// reaches only through subsystem types: the SAX and incremental-source parse
+/// overloads, the streaming `events` overloads, and HTML content-model validation.
+@Suite("Public entry points")
+struct PublicEntryPointTests {
+ private let xml = #"hi"#
+
+ @Test("parse delivers SAX callbacks in document order")
+ func test_parseSax() throws {
+ var elements: [String] = []
+ var text = ""
+ let handler = PureXML.Parsing.SAXHandler(
+ startElement: { name, _ in elements.append(name.localName) },
+ characters: { text += $0 },
+ )
+ try PureXML.parse(xml, sax: handler)
+ #expect(elements == ["r", "c"])
+ #expect(text == "hi")
+ }
+
+ @Test("parse from an incremental byte source yields the same tree as the string parse")
+ func test_parsePullingBytes() throws {
+ var bytes = Array(xml.utf8)[...]
+ let streamed = try PureXML.parse(pullingBytes: { bytes.isEmpty ? nil : bytes.removeFirst() })
+ #expect(try PureXML.serialize(streamed) == PureXML.serialize(PureXML.parse(xml)))
+ }
+
+ @Test("every events overload streams the document to completion")
+ func test_eventsOverloads() throws {
+ var fromBytes = try PureXML.events(bytes: Array(xml.utf8))
+ var byteEvents = 0
+ while try fromBytes.next() != nil {
+ byteEvents += 1
+ }
+ #expect(byteEvents > 0)
+
+ var chars = Array(xml)[...]
+ var fromChars = PureXML.events(pulling: { chars.isEmpty ? nil : chars.removeFirst() })
+ var charEvents = 0
+ while try fromChars.next() != nil {
+ charEvents += 1
+ }
+ #expect(charEvents == byteEvents)
+
+ var sourceBytes = Array(xml.utf8)[...]
+ var fromPulledBytes = PureXML.events(pullingBytes: { sourceBytes.isEmpty ? nil : sourceBytes.removeFirst() })
+ var pulledByteEvents = 0
+ while try fromPulledBytes.next() != nil {
+ pulledByteEvents += 1
+ }
+ #expect(pulledByteEvents == byteEvents)
+ }
+
+ @Test("validateHTML flags a duplicate id")
+ func test_validateHTML() throws {
+ let duplicate = try PureXML.parse(#""#)
+ #expect(!PureXML.validateHTML(duplicate).isEmpty)
+ let unique = try PureXML.parse(#""#)
+ #expect(PureXML.validateHTML(unique).allSatisfy { !$0.reason.lowercased().contains("id") })
+ }
+}