#if os(macOS) import AppKit import XCTest @testable import DocsKit @MainActor final class DocumentPageEditingTests: XCTestCase { func testOptimisticAndMarkedEditsInvalidatePageOffsets() throws { let nodes = (0..<100).map { index in CRDTNode(id: "p\(index)", type: "paragraph", children: [CRDTNode(id: "t\(index)", type: "text", runs: [CRDTRun(insert: "A paragraph kept together when pages are recalculated.")])]) } let view = DocsPagedTextView(frame: CGRect(x: 0, y: 0, width: 636, height: 800)) let storage = try XCTUnwrap(view.textStorage) storage.setAttributedString(NativeTextProjection.render(CRDTProjection(children: nodes, canUndo: false, canRedo: false))) view.paged = true view.layout() let container = try XCTUnwrap(view.textContainer as? DocsPageTextContainer) XCTAssertFalse(container.blockStarts.isEmpty) let initial = container.blockStarts let insertion = String(repeating: "Inserted text before the boundary. ", count: 20) storage.replaceCharacters(in: NSRange(location: 0, length: 0), with: insertion) assertBlockStarts(container, storage) view.layout() XCTAssertNotEqual(initial, container.blockStarts) assertBlockStarts(container, storage) view.setSelectedRange(NSRange(location: 0, length: 0)) view.setMarkedText("日本語", selectedRange: NSRange(location: 3, length: 0), replacementRange: NSRange(location: 0, length: 0)) assertBlockStarts(container, storage) view.layout() assertBlockStarts(container, storage) view.unmarkText() storage.replaceCharacters(in: NSRange(location: 0, length: 3 + insertion.utf16.count), with: "") assertBlockStarts(container, storage) view.layout() XCTAssertEqual(container.blockStarts, initial) XCTAssertEqual(storage.string, nodes.map(\.plainText).joined(separator: "\n") + "\n") } func testMetadataReconciliationNeverExposesUnpaginatedCaretGeometry() throws { let nodes = (0..<1000).map { index in CRDTNode(id: "p\(index)", type: "paragraph", children: [CRDTNode(id: "t\(index)", type: "text", runs: [CRDTRun(insert: "A paragraph kept together when pages are recalculated.")])]) } let view = DocsPagedTextView(frame: CGRect(x: 0, y: 0, width: 636, height: 800)) let storage = try XCTUnwrap(view.textStorage) storage.setAttributedString(NativeTextProjection.render(CRDTProjection(children: nodes, canUndo: false, canRedo: false))) view.paged = true view.layout() let manager = try XCTUnwrap(view.layoutManager) let container = try XCTUnwrap(view.textContainer as? DocsPageTextContainer) XCTAssertGreaterThanOrEqual(container.blockStarts.count, 2) let selected = storage.length - 3 view.setSelectedRange(NSRange(location: selected, length: 0)) func caretY() -> CGFloat { manager.ensureLayout(for: container) return manager.lineFragmentRect(forGlyphAt: manager.glyphIndexForCharacter(at: selected), effectiveRange: nil).minY } let before = caretY() let start = ContinuousClock.now for generation in 0..<10 { storage.addAttribute(NSAttributedString.Key("MetadataOnly"), value: generation, range: NSRange(location: 0, length: storage.length)) XCTAssertEqual(caretY(), before, accuracy: 0.01, "AppKit can query the caret before the next view layout") } print("Ten metadata updates in a 1,000-paragraph document:", start.duration(to: .now)) view.layout() XCTAssertEqual(caretY(), before, accuracy: 0.01) XCTAssertEqual(view.selectedRange(), NSRange(location: selected, length: 0)) } func testCaretBackgroundErasureStaysInsideRequestedRectangle() throws { let view = DocsPagedTextView(frame: CGRect(x: 0, y: 0, width: 636, height: 800)) view.layout() let bitmap = try XCTUnwrap(NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: 100, pixelsHigh: 100, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)) let context = try XCTUnwrap(NSGraphicsContext(bitmapImageRep: bitmap)) NSGraphicsContext.saveGraphicsState() defer { NSGraphicsContext.restoreGraphicsState() } NSGraphicsContext.current = context NSColor.red.setFill() CGRect(x: 0, y: 0, width: 100, height: 100).fill() view.drawBackground(in: CGRect(x: 40, y: 40, width: 20, height: 20)) XCTAssertEqual(try XCTUnwrap(bitmap.colorAt(x: 20, y: 20)).greenComponent, 0, accuracy: 0.01, "Erasing one caret must not paint over neighboring glyphs") XCTAssertGreaterThan(try XCTUnwrap(bitmap.colorAt(x: 50, y: 50)).greenComponent, 0.9) } func testTypingKeepsCaretInsidePageBoundsWithStableOrigin() throws { let nodes = (0..<60).map { index in CRDTNode(id: "p\(index)", type: "paragraph", children: [CRDTNode(id: "t\(index)", type: "text", runs: [CRDTRun(insert: String(repeating: "A paragraph that wraps across lines. ", count: 8))])]) } let view = DocsTextView(frame: CGRect(x: 0, y: 0, width: 900, height: 800)) view.textStorage!.setAttributedString(NativeTextProjection.render(CRDTProjection(children: nodes, canUndo: false, canRedo: false))) view.paged = true view.layout() view.setSelectedRange(NSRange(location: view.string.utf16.count - 1, length: 0)) for text in ["\n", "a", "\n", "b"] { view.insertText(text, replacementRange: view.selectedRange()) view.layoutManager!.ensureLayout(for: view.textContainer!) let glyph = view.layoutManager!.glyphIndexForCharacter(at: view.selectedRange().location) let line = view.layoutManager!.lineFragmentRect(forGlyphAt: glyph, effectiveRange: nil) XCTAssertEqual(view.textContainerOrigin, .zero) XCTAssertGreaterThanOrEqual(view.bounds.maxY, line.maxY) } } private func assertBlockStarts(_ container: DocsPageTextContainer, _ storage: NSTextStorage) { for location in container.blockStarts.keys { var range = NSRange() XCTAssertNotNil(storage.attribute(.docsTopBlock, at: location, effectiveRange: &range)) XCTAssertEqual(range.location, location, "A separator must never move into a paragraph") } } } #endif