2026-06-01 09:40:19 +02:00
|
|
|
import XCTest
|
|
|
|
|
@testable import SaplingEditor
|
|
|
|
|
|
|
|
|
|
#if os(macOS)
|
|
|
|
|
import AppKit
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
|
final class HybridMarkdownLiveEditorHarnessTests: XCTestCase {
|
|
|
|
|
func testLiveInitialFirstResponderDoesNotShowHeadingSourceBeforeUserInteraction() {
|
|
|
|
|
let harness = HybridMarkdownLiveEditorHarness(source: "# Heading\nParagraph")
|
|
|
|
|
|
|
|
|
|
harness.simulateLaunchFirstResponder()
|
|
|
|
|
|
|
|
|
|
XCTAssertTrue(harness.headingMarkerIsHidden())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testLiveParagraphGeometryReturnsAfterClickAndFocusAway() throws {
|
|
|
|
|
let source = """
|
|
|
|
|
# Heading
|
|
|
|
|
Paragraph with **bold**, *italic*, `code`, and [Link](https://example.com) markers.
|
|
|
|
|
Outro
|
|
|
|
|
"""
|
|
|
|
|
let harness = HybridMarkdownLiveEditorHarness(source: source)
|
|
|
|
|
harness.simulateLaunchFirstResponder()
|
|
|
|
|
let initialPoint = try XCTUnwrap(harness.point(for: "Paragraph"))
|
|
|
|
|
|
|
|
|
|
let paragraphLocation = (source as NSString).range(of: "bold").location
|
|
|
|
|
harness.setSelection(NSRange(location: paragraphLocation, length: 0))
|
|
|
|
|
harness.simulateFocusAway()
|
|
|
|
|
let finalPoint = try XCTUnwrap(harness.point(for: "Paragraph"))
|
|
|
|
|
|
|
|
|
|
XCTAssertEqual(initialPoint.x, finalPoint.x, accuracy: 0.001)
|
|
|
|
|
XCTAssertEqual(initialPoint.y, finalPoint.y, accuracy: 0.001)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testLiveCheckboxClickPreservesSelectionActiveLineAndGeometry() throws {
|
|
|
|
|
let source = "Editing here\n* [ ] Move with arrow keys."
|
|
|
|
|
let editingRange = (source as NSString).range(of: "Editing")
|
|
|
|
|
let harness = HybridMarkdownLiveEditorHarness(
|
|
|
|
|
source: source,
|
|
|
|
|
selectedRange: NSRange(location: editingRange.location, length: 0)
|
|
|
|
|
)
|
|
|
|
|
harness.simulateLaunchFirstResponder()
|
|
|
|
|
harness.setSelection(NSRange(location: editingRange.location, length: 0))
|
|
|
|
|
|
|
|
|
|
let selectionBefore = harness.selectedRange()
|
|
|
|
|
let activeLineBefore = harness.effectiveActiveLineIndex()
|
|
|
|
|
let labelPointBefore = try XCTUnwrap(harness.point(for: "Move"))
|
|
|
|
|
let checkboxFrameBefore = try XCTUnwrap(harness.checklistButtonFrame(lineIndex: 1))
|
|
|
|
|
|
|
|
|
|
harness.clickRenderedCheckbox(lineIndex: 1)
|
|
|
|
|
|
|
|
|
|
let labelPointAfter = try XCTUnwrap(harness.point(for: "Move"))
|
|
|
|
|
let checkboxFrameAfter = try XCTUnwrap(harness.checklistButtonFrame(lineIndex: 1))
|
|
|
|
|
XCTAssertEqual(harness.selectedRange(), selectionBefore)
|
|
|
|
|
XCTAssertEqual(harness.effectiveActiveLineIndex(), activeLineBefore)
|
|
|
|
|
XCTAssertTrue(harness.source().contains("* [x] Move with arrow keys."))
|
|
|
|
|
XCTAssertEqual(labelPointBefore.x, labelPointAfter.x, accuracy: 0.001)
|
|
|
|
|
XCTAssertEqual(labelPointBefore.y, labelPointAfter.y, accuracy: 0.001)
|
|
|
|
|
XCTAssertEqual(checkboxFrameBefore.origin.x, checkboxFrameAfter.origin.x, accuracy: 0.001)
|
|
|
|
|
XCTAssertEqual(checkboxFrameBefore.origin.y, checkboxFrameAfter.origin.y, accuracy: 0.001)
|
|
|
|
|
XCTAssertEqual(checkboxFrameBefore.size.width, checkboxFrameAfter.size.width, accuracy: 0.001)
|
|
|
|
|
XCTAssertEqual(checkboxFrameBefore.size.height, checkboxFrameAfter.size.height, accuracy: 0.001)
|
|
|
|
|
}
|
2026-06-01 09:56:36 +02:00
|
|
|
|
|
|
|
|
func testInitialChecklistOverlayTracksFirstLiveLayoutPass() throws {
|
|
|
|
|
let source = """
|
|
|
|
|
## Navigation Checklist
|
|
|
|
|
|
|
|
|
|
Use this section for quick keyboard testing.
|
|
|
|
|
|
|
|
|
|
* [ ] Move with arrow keys.
|
|
|
|
|
* [ ] Jump by word with Option + Arrow.
|
|
|
|
|
* [ ] Extend selection with Shift + Arrow.
|
|
|
|
|
* [ ] Select across multiple lines.
|
|
|
|
|
* [ ] Use Home, End, Page Up, and Page Down.
|
|
|
|
|
* [ ] Type into the active line after moving quickly.
|
|
|
|
|
"""
|
|
|
|
|
let harness = HybridMarkdownLiveEditorHarness(source: source, initialWidth: 640)
|
|
|
|
|
let initialGaps = try Dictionary(uniqueKeysWithValues: (4...9).map { lineIndex in
|
|
|
|
|
(lineIndex, try XCTUnwrap(harness.checklistLabelGap(lineIndex: lineIndex)))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
harness.simulateLayout(width: 900)
|
|
|
|
|
|
|
|
|
|
for lineIndex in 4...9 {
|
|
|
|
|
let alignmentDelta = try XCTUnwrap(harness.checklistAlignmentDelta(lineIndex: lineIndex))
|
|
|
|
|
let labelGap = try XCTUnwrap(harness.checklistLabelGap(lineIndex: lineIndex))
|
|
|
|
|
XCTAssertLessThan(alignmentDelta, 8, "Checklist overlay for line \(lineIndex) is not aligned with its label")
|
|
|
|
|
XCTAssertEqual(
|
|
|
|
|
labelGap,
|
|
|
|
|
initialGaps[lineIndex] ?? labelGap,
|
|
|
|
|
accuracy: 0.001,
|
|
|
|
|
"Checklist overlay for line \(lineIndex) did not track the label through the first layout pass"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-01 09:40:19 +02:00
|
|
|
}
|
|
|
|
|
#endif
|