107 lines
4.4 KiB
Swift
107 lines
4.4 KiB
Swift
|
|
import XCTest
|
||
|
|
@testable import SaplingEditor
|
||
|
|
|
||
|
|
final class EditorLargeDocumentValidationTests: XCTestCase {
|
||
|
|
func testLargeDocumentOpenAndDirtyRenderPlanning() {
|
||
|
|
for lineCount in [1_000, 5_000, 10_000] {
|
||
|
|
let source = Self.prototypeDocument(lineCount: lineCount)
|
||
|
|
let activeLineIndex = lineCount / 2
|
||
|
|
|
||
|
|
let openMeasurement = elapsedTime {
|
||
|
|
_ = EditorState(
|
||
|
|
document: document(source: source, lineCount: lineCount),
|
||
|
|
activeLineIndex: activeLineIndex
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
let updatedSource = Self.replacingLine(
|
||
|
|
activeLineIndex,
|
||
|
|
in: source,
|
||
|
|
with: "Line \(activeLineIndex + 1) edited with **bold** and `inline code`."
|
||
|
|
)
|
||
|
|
let updateMeasurement = elapsedTime {
|
||
|
|
var state = EditorState(
|
||
|
|
document: document(source: source, lineCount: lineCount),
|
||
|
|
activeLineIndex: activeLineIndex
|
||
|
|
)
|
||
|
|
state.updateSource(updatedSource)
|
||
|
|
_ = EditorDirtyLineInvalidator.plan(
|
||
|
|
previousText: source,
|
||
|
|
currentText: updatedSource,
|
||
|
|
previousActiveLineIndex: activeLineIndex,
|
||
|
|
currentActiveLineIndex: activeLineIndex
|
||
|
|
)
|
||
|
|
}
|
||
|
|
let plan = EditorDirtyLineInvalidator.plan(
|
||
|
|
previousText: source,
|
||
|
|
currentText: updatedSource,
|
||
|
|
previousActiveLineIndex: activeLineIndex,
|
||
|
|
currentActiveLineIndex: activeLineIndex
|
||
|
|
)
|
||
|
|
let dirtyRenderMeasurement = elapsedTime {
|
||
|
|
let lines = EditorActiveLineTracker.lines(from: updatedSource, activeLineIndex: activeLineIndex)
|
||
|
|
let dirty = Set(plan.dirtyLineIndexes)
|
||
|
|
let renderer = HybridMarkdownLineRenderer()
|
||
|
|
_ = lines
|
||
|
|
.filter { dirty.contains($0.index) }
|
||
|
|
.map(renderer.renderPlan(for:))
|
||
|
|
}
|
||
|
|
|
||
|
|
XCTAssertEqual(EditorActiveLineTracker.lines(from: source, activeLineIndex: activeLineIndex).count, lineCount)
|
||
|
|
XCTAssertFalse(plan.isFullRender)
|
||
|
|
XCTAssertLessThanOrEqual(plan.dirtyLineCount, 3)
|
||
|
|
XCTAssertLessThan(openMeasurement, 0.25, "Opening \(lineCount) generated lines should remain interactive.")
|
||
|
|
XCTAssertLessThan(updateMeasurement, 0.50, "State update for \(lineCount) lines should remain bounded.")
|
||
|
|
XCTAssertLessThan(dirtyRenderMeasurement, 0.05, "Dirty render planning should not scale with the full document.")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func testLargeDocumentActiveLineNavigationDoesNotScheduleFullRender() {
|
||
|
|
let source = Self.prototypeDocument(lineCount: 10_000)
|
||
|
|
|
||
|
|
let plan = EditorDirtyLineInvalidator.plan(
|
||
|
|
previousText: source,
|
||
|
|
currentText: source,
|
||
|
|
previousActiveLineIndex: 250,
|
||
|
|
currentActiveLineIndex: 9_750
|
||
|
|
)
|
||
|
|
|
||
|
|
XCTAssertEqual(plan.reason, .activeLineChange)
|
||
|
|
XCTAssertFalse(plan.isFullRender)
|
||
|
|
XCTAssertEqual(plan.dirtyLineIndexes, [250, 9_750])
|
||
|
|
}
|
||
|
|
|
||
|
|
private func document(source: String, lineCount: Int) -> EditorDocument {
|
||
|
|
EditorDocument(
|
||
|
|
url: URL(fileURLWithPath: "/tmp/EditorLargeDocument-\(lineCount).md"),
|
||
|
|
title: "EditorLargeDocument-\(lineCount)",
|
||
|
|
source: source
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
private func elapsedTime(_ operation: () -> Void) -> TimeInterval {
|
||
|
|
let start = Date()
|
||
|
|
operation()
|
||
|
|
return Date().timeIntervalSince(start)
|
||
|
|
}
|
||
|
|
|
||
|
|
private static func prototypeDocument(lineCount: Int) -> String {
|
||
|
|
(1...lineCount).map { index in
|
||
|
|
if index.isMultiple(of: 25) {
|
||
|
|
return "## Section \(index / 25)"
|
||
|
|
}
|
||
|
|
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")
|
||
|
|
}
|
||
|
|
|
||
|
|
private static func replacingLine(_ lineIndex: Int, in source: String, with replacement: String) -> String {
|
||
|
|
var lines = source.components(separatedBy: "\n")
|
||
|
|
lines[lineIndex] = replacement
|
||
|
|
return lines.joined(separator: "\n")
|
||
|
|
}
|
||
|
|
}
|