Sapling/Tests/SaplingEditorTests/EditorStateTests.swift

212 lines
7.9 KiB
Swift
Raw Normal View History

import XCTest
import SaplingCore
@testable import SaplingEditor
final class EditorStateTests: XCTestCase {
func testActiveLineFollowsSelection() {
let source = "# Heading\nRendered **line**\nRaw source line"
let document = EditorDocument(
url: URL(fileURLWithPath: "/tmp/EditorStateTests.md"),
title: "EditorStateTests",
source: source
)
var state = EditorState(document: document)
let secondLineLocation = (source as NSString).range(of: "Rendered").location
state.updateSelection(EditorSelection(location: secondLineLocation, length: 0))
XCTAssertEqual(state.activeLineIndex, 1)
XCTAssertEqual(state.lines[0].mode, .rendered)
XCTAssertEqual(state.lines[1].mode, .source)
XCTAssertEqual(state.lines[2].mode, .rendered)
}
func testSelectionIsClampedAfterDeletion() {
let document = EditorDocument(
url: URL(fileURLWithPath: "/tmp/EditorStateTests.md"),
title: "EditorStateTests",
source: "First line\nSecond line\nThird line"
)
var state = EditorState(document: document)
state.updateSelection(EditorSelection(location: 30, length: 20))
state.updateSource("Short")
XCTAssertEqual(state.selection.location, 5)
XCTAssertEqual(state.selection.length, 0)
XCTAssertEqual(state.activeLineIndex, 0)
XCTAssertEqual(state.lines.count, 1)
}
func testLineTrackerPreservesTrailingBlankLine() {
let lines = EditorActiveLineTracker.lines(from: "One\nTwo\n", activeLineIndex: 2)
XCTAssertEqual(lines.count, 3)
XCTAssertEqual(lines[2].source, "")
XCTAssertEqual(lines[2].range.location, 8)
XCTAssertEqual(lines[2].mode, .source)
}
func testUpdatingSourceTracksUnsavedChanges() {
let document = EditorDocument(
url: URL(fileURLWithPath: "/tmp/EditorStateTests.md"),
title: "EditorStateTests",
source: "Initial"
)
var state = EditorState(document: document)
state.updateSource("Initial\nChanged")
XCTAssertTrue(state.hasUnsavedChanges)
XCTAssertEqual(state.lines.count, 2)
state.markSaved()
XCTAssertFalse(state.hasUnsavedChanges)
}
2026-05-29 19:19:59 +02:00
func testActiveColumnFollowsSelectionWithinLine() {
let source = "First line\nSecond line"
let document = EditorDocument(
url: URL(fileURLWithPath: "/tmp/EditorStateTests.md"),
title: "EditorStateTests",
source: source
)
var state = EditorState(document: document)
let secondLineLocation = (source as NSString).range(of: "Second").location
state.updateSelection(EditorSelection(location: secondLineLocation + 3, length: 0))
XCTAssertEqual(state.activeLineIndex, 1)
XCTAssertEqual(state.activeColumnNumber, 4)
}
func testHybridRendererSupportsMilestoneTwoInlineElements() {
let source = "Plain **bold** and *italic* with `code`"
let line = EditorLine(
index: 0,
source: source,
range: NSRange(location: 0, length: source.utf16.count),
mode: .rendered
)
let plan = HybridMarkdownLineRenderer().renderPlan(for: line)
XCTAssertEqual(plan.kind, .paragraph)
XCTAssertTrue(plan.spans.contains { $0.kind == .bold && (source as NSString).substring(with: $0.range) == "bold" })
XCTAssertTrue(plan.spans.contains { $0.kind == .italic && (source as NSString).substring(with: $0.range) == "italic" })
XCTAssertTrue(plan.spans.contains { $0.kind == .inlineCode && (source as NSString).substring(with: $0.range) == "code" })
XCTAssertEqual(plan.spans.filter { $0.kind == .markdownDelimiter }.count, 6)
}
func testHybridRendererSupportsHeadings() {
let source = "## Architecture"
let line = EditorLine(
index: 0,
source: source,
range: NSRange(location: 12, length: source.utf16.count),
mode: .rendered
)
let plan = HybridMarkdownLineRenderer().renderPlan(for: line)
XCTAssertEqual(
plan.kind,
.heading(
level: 2,
markerRange: NSRange(location: 12, length: 2),
textRange: NSRange(location: 15, length: 12)
)
)
}
func testHybridRendererDoesNotPromoteLinksOrTasksInMilestoneTwo() {
let source = "- [ ] Task with [link](https://example.com)"
let line = EditorLine(
index: 0,
source: source,
range: NSRange(location: 0, length: source.utf16.count),
mode: .rendered
)
let plan = HybridMarkdownLineRenderer().renderPlan(for: line)
XCTAssertEqual(plan.kind, .paragraph)
XCTAssertTrue(plan.spans.isEmpty)
}
@MainActor
func testViewModelTracksActiveLineAndSelectionInstrumentation() {
let document = MarkdownDocument(
url: URL(fileURLWithPath: "/tmp/EditorStateTests.md"),
title: "EditorStateTests",
content: "One\nTwo\nThree"
)
let viewModel = HybridMarkdownEditorViewModel(document: document)
viewModel.updateSelection(EditorSelection(location: 4, length: 0))
viewModel.updateSource("One\nTwo updated\nThree")
viewModel.recordRenderPass(EditorRenderPassMetric(
reason: .activeLineChange,
durationMilliseconds: 1.5,
characterCount: 21,
lineCount: 3,
activeLineIndex: 1
))
XCTAssertEqual(viewModel.instrumentation.selectionChangeCount, 1)
XCTAssertEqual(viewModel.instrumentation.sourceChangeCount, 1)
XCTAssertEqual(viewModel.instrumentation.activeLineChangeCount, 1)
XCTAssertEqual(viewModel.instrumentation.renderPassCount, 1)
XCTAssertEqual(viewModel.instrumentation.lastRenderReason, .activeLineChange)
}
func testHybridRendererHandlesLargePrototypeDocumentShape() {
let source = Self.prototypeDocument(lineCount: 2_100)
let lines = EditorActiveLineTracker.lines(from: source, activeLineIndex: 1_000)
let renderer = HybridMarkdownLineRenderer()
let plans = lines.map(renderer.renderPlan(for:))
XCTAssertEqual(lines.count, 2_100)
XCTAssertEqual(plans.filter {
if case .heading = $0.kind {
return true
}
return false
}.count, 210)
XCTAssertEqual(plans.flatMap(\.spans).filter { $0.kind == .inlineCode }.count, 210)
}
@MainActor
func testViewModelSavesDocumentToDisk() throws {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let url = directory.appendingPathComponent("Note.md")
try "# Note\n".write(to: url, atomically: true, encoding: .utf8)
let document = try HybridMarkdownEditorViewModel.loadDocument(at: url)
let viewModel = HybridMarkdownEditorViewModel(document: document)
viewModel.updateSource("# Note\n\nUpdated")
try viewModel.save()
let saved = try String(contentsOf: url, encoding: .utf8)
XCTAssertEqual(saved, "# Note\n\nUpdated")
XCTAssertFalse(viewModel.hasUnsavedChanges)
}
private static func prototypeDocument(lineCount: Int) -> String {
(1...lineCount).map { index in
if index.isMultiple(of: 10) {
return "## Section \(index / 10)"
}
if index.isMultiple(of: 5) {
return "Line \(index) uses **bold** context and `inline code` for scanning."
}
return "Line \(index) is a realistic paragraph with *light emphasis* and enough text to wrap naturally."
}
.joined(separator: "\n")
}
}