import XCTest @testable import SaplingEditor final class DocumentPresentationStateTests: XCTestCase { func testEveryLineHasExactlyOnePresentationState() { let source = "# Heading\n* [x] Done\nParagraph" let lineIndex = DocumentLineIndex(source: source) let presentation = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 1) XCTAssertEqual(presentation.lines.count, 3) XCTAssertEqual(presentation.lineState(at: 0), .rendered) XCTAssertEqual(presentation.lineState(at: 1), .source) XCTAssertEqual(presentation.lineState(at: 2), .rendered) XCTAssertEqual(presentation.lines.filter { $0.state == .source }.map(\.line.index), [1]) XCTAssertEqual(presentation.lines.filter { $0.state == .rendered }.map(\.line.index), [0, 2]) } func testPresentationStateIsDeterministicForSameDocumentAndActiveLine() { let source = "# Heading\nThis has **bold** and `code`.\n* [ ] Todo" let lineIndex = DocumentLineIndex(source: source) let first = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 2) let second = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 2) XCTAssertEqual(first, second) } func testDocumentRenderModelIsIndependentOfActiveLinePresentation() { let source = "# Heading\nThis has **bold** and `code`.\n* [ ] Todo" let lineIndex = DocumentLineIndex(source: source) let renderModel = DocumentRenderModel(lineIndex: lineIndex) let firstPresentation = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 0) let secondPresentation = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 2) XCTAssertEqual(renderModel.snapshot, DocumentRenderModel(lineIndex: lineIndex).snapshot) XCTAssertNotEqual(firstPresentation, secondPresentation) XCTAssertTrue(renderModel.nodes.contains { guard case .heading(let heading) = $0.element else { return false } return heading.lineIndex == 0 }) } func testInitialRenderModelContainsHeadingsWithoutInteraction() { let source = "# Title\n\n## Section\nParagraph" let renderModel = DocumentRenderModel(lineIndex: DocumentLineIndex(source: source)) let headingLevels = renderModel.nodes.compactMap { node -> Int? in guard case .heading(let heading) = node.element else { return nil } return heading.level } XCTAssertEqual(headingLevels, [1, 2]) } func testRenderSnapshotChangesOnlyWhenMarkdownChanges() { let source = "# Heading\n* [ ] Move with arrow keys." let originalModel = DocumentRenderModel(lineIndex: DocumentLineIndex(source: source)) let repeatedModel = DocumentRenderModel(lineIndex: DocumentLineIndex(source: source)) let toggledSource = (source as NSString).replacingCharacters( in: NSRange(location: (source as NSString).range(of: "[ ]").location, length: 3), with: "[x]" ) let toggledModel = DocumentRenderModel(lineIndex: DocumentLineIndex(source: toggledSource)) XCTAssertEqual(originalModel.snapshot, repeatedModel.snapshot) XCTAssertNotEqual(originalModel.snapshot, toggledModel.snapshot) } func testRenderedTaskToggleReplacementDoesNotMoveSourceRanges() { let source = "Intro\n* [ ] Move with arrow keys.\nOutro" let lineIndex = DocumentLineIndex(source: source) let task = DocumentRenderModel(lineIndex: lineIndex).nodes.compactMap { node -> RenderedTaskElement? in guard case .task(let task) = node.element else { return nil } return task }.first XCTAssertEqual(task?.toggledMarkdownCheckbox, "[x]") XCTAssertEqual(task?.checkboxRange.length, ((task?.toggledMarkdownCheckbox ?? "") as NSString).length) } func testRenderedElementsAreSemantic() { let source = "# Heading\n* [x] Done\nSee [Docs](https://example.com)" let lineIndex = DocumentLineIndex(source: source) let presentation = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 99) XCTAssertTrue(presentation.elements.contains { guard case .heading(let heading) = $0 else { return false } return heading.lineIndex == 0 && heading.level == 1 }) XCTAssertEqual(presentation.renderedTasks.map(\.checked), [true]) XCTAssertTrue(presentation.elements.contains { guard case .link(let link) = $0 else { return false } return (source as NSString).substring(with: link.titleRange) == "Docs" }) } func testCodeBlockIsRepresentedAsSemanticBlock() { let source = "Intro\n```swift\nlet value = 42\n```" let lineIndex = DocumentLineIndex(source: source) let presentation = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 0) XCTAssertEqual(presentation.renderedCodeBlocks.count, 1) XCTAssertEqual(presentation.renderedCodeBlocks[0].lineIndexes, [1, 2, 3]) XCTAssertEqual( (source as NSString).substring(with: presentation.renderedCodeBlocks[0].languageRange!), "swift" ) } func testDirtyPresentationResolvesNearbyCodeBlockContext() { let source = "Intro\n```swift\nlet value = 42\n```" let lineIndex = DocumentLineIndex(source: source) let presentation = DocumentPresentationState( lineIndex: lineIndex, activeLineIndex: 0, lineIndexes: [2] ) XCTAssertEqual(presentation.lines.count, 1) XCTAssertEqual(presentation.lines[0].state, .rendered) XCTAssertEqual(presentation.lines[0].renderPlan.kind, .codeBlockContent(language: "swift")) } func testEditableRegionMakesMultipleSelectedLinesSource() { let source = "# Heading\nParagraph\n* [ ] Task\nOutro" let lineIndex = DocumentLineIndex(source: source) let selectionStart = (source as NSString).range(of: "Paragraph").location let selectionEnd = (source as NSString).range(of: "Task").upperBound let region = EditableRegion.selection( NSRange(location: selectionStart, length: selectionEnd - selectionStart), in: lineIndex ) let presentation = DocumentPresentationState(lineIndex: lineIndex, editableRegion: region) XCTAssertEqual(presentation.lines.filter { $0.state == .source }.map(\.line.index), [1, 2]) XCTAssertEqual(presentation.lines.filter { $0.state == .rendered }.map(\.line.index), [0, 3]) } func testEditableRegionExpandsToEntireCodeBlock() { let source = "Intro\n```swift\nlet value = 42\n```\nOutro" let lineIndex = DocumentLineIndex(source: source) let cursorLocation = (source as NSString).range(of: "value").location let region = EditableRegion.selection(NSRange(location: cursorLocation, length: 0), in: lineIndex) let presentation = DocumentPresentationState(lineIndex: lineIndex, editableRegion: region) XCTAssertEqual(region.lineIndexes, [1, 2, 3]) XCTAssertEqual(presentation.lines.filter { $0.state == .source }.map(\.line.index), [1, 2, 3]) XCTAssertEqual(presentation.lines.filter { $0.state == .rendered }.map(\.line.index), [0, 4]) XCTAssertTrue(presentation.renderedCodeBlocks.isEmpty) } } private extension NSRange { var upperBound: Int { location + length } }