PureXML/Tests/QuickFixTests.swift
Mihaela Mihaljevic d667b4cf1e feat(editor): structured quick-fixes from completions
Slice 4 of #94 (completes the epic). Quick-fixes are derived from the structured
schema completions, never by parsing a diagnostic message:

- TreeNode gains contentRange (the span between an element's start and end tags),
  captured by the ranged reader, so an inserted attribute or child is placed
  exactly.
- QuickFix and TextEdit model an editor edit (a replacement over a source range;
  an insertion is a zero-width range).
- QuickFixEngine turns a Completions into fixes: a required-but-absent attribute
  becomes an insertion before the start tag's >, and a still-expected required
  child becomes an insertion before the end tag. Schema.Document.quickFixes(at:in:)
  is the entry over a ranged tree.

Tests apply a fix and re-validate: adding the required id, or inserting the
required qty, yields XML the schema then accepts; a complete element offers none.

#94 (editor integration) is complete: ranged recovering read, unified lint,
schema completions, and quick-fixes.
2026-06-08 13:35:33 +02:00

57 lines
2.5 KiB
Swift

@testable import PureXML
import Testing
/// Quick-fixes derived from the structured schema completions, with precise
/// placement from content spans. The strongest check: applying a fix yields XML
/// that the same schema then accepts.
@Suite("Quick fixes")
struct QuickFixTests {
private let xsd = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="order">
<xs:complexType>
<xs:sequence><xs:element name="qty" type="xs:integer"/></xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>
"""
private func apply(_ edit: PureXML.TextEdit, to xml: String) -> String {
var characters = Array(xml)
let start = edit.range.start.offset
let end = edit.range.end.offset
characters.replaceSubrange(start ..< end, with: Array(edit.replacement))
return String(characters)
}
@Test("A missing required attribute becomes an insertion before the start tag")
func test_addAttribute() throws {
let schema = try PureXML.Schema.Document(xsd)
let (tree, _) = PureXML.readTree("<order><qty>1</qty></order>")
let fixes = schema.quickFixes(at: [.element("order")], in: tree)
let fix = try #require(fixes.first { $0.title.contains("attribute 'id'") })
let fixed = apply(fix.edits[0], to: "<order><qty>1</qty></order>")
#expect(fixed == "<order id=\"\"><qty>1</qty></order>")
// The required-attribute error is gone after applying.
#expect(try schema.validate(fixed).contains { $0.reason.contains("required attribute 'id'") } == false)
}
@Test("A still-expected required child becomes an insertion before the end tag")
func test_insertChild() throws {
let schema = try PureXML.Schema.Document(xsd)
let (tree, _) = PureXML.readTree("<order id='1'></order>")
let fixes = schema.quickFixes(at: [.element("order")], in: tree)
let fix = try #require(fixes.first { $0.title == "Insert <qty>" })
let fixed = apply(fix.edits[0], to: "<order id='1'></order>")
#expect(fixed == "<order id='1'><qty></qty></order>")
}
@Test("A complete, valid element offers no fixes")
func test_noFixes() throws {
let schema = try PureXML.Schema.Document(xsd)
let (tree, _) = PureXML.readTree("<order id='1'><qty>1</qty></order>")
#expect(schema.quickFixes(at: [.element("order")], in: tree).isEmpty)
}
}