oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1#if os(macOS)
2import AppKit
3import XCTest
4@testable import DocsKit
5
6@MainActor final class DocumentPageEditingTests: XCTestCase {
7 func testOptimisticAndMarkedEditsInvalidatePageOffsets() throws {
8 let nodes = (0..<100).map { index in
9 CRDTNode(id: "p\(index)", type: "paragraph", children: [CRDTNode(id: "t\(index)", type: "text", runs: [CRDTRun(insert: "A paragraph kept together when pages are recalculated.")])])
10 }
11 let view = DocsPagedTextView(frame: CGRect(x: 0, y: 0, width: 636, height: 800))
12 let storage = try XCTUnwrap(view.textStorage)
13 storage.setAttributedString(NativeTextProjection.render(CRDTProjection(children: nodes, canUndo: false, canRedo: false)))
14 view.paged = true
15 view.layout()
16 let container = try XCTUnwrap(view.textContainer as? DocsPageTextContainer)
17 XCTAssertFalse(container.blockStarts.isEmpty)
18 let initial = container.blockStarts
19 let insertion = String(repeating: "Inserted text before the boundary. ", count: 20)
20 storage.replaceCharacters(in: NSRange(location: 0, length: 0), with: insertion)
21 assertBlockStarts(container, storage)
22 view.layout()
23 XCTAssertNotEqual(initial, container.blockStarts)
24 assertBlockStarts(container, storage)
25 view.setSelectedRange(NSRange(location: 0, length: 0))
26 view.setMarkedText("日本語", selectedRange: NSRange(location: 3, length: 0), replacementRange: NSRange(location: 0, length: 0))
27 assertBlockStarts(container, storage)
28 view.layout()
29 assertBlockStarts(container, storage)
30 view.unmarkText()
31 storage.replaceCharacters(in: NSRange(location: 0, length: 3 + insertion.utf16.count), with: "")
32 assertBlockStarts(container, storage)
33 view.layout()
34 XCTAssertEqual(container.blockStarts, initial)
35 XCTAssertEqual(storage.string, nodes.map(\.plainText).joined(separator: "\n") + "\n")
36 }
37
38 func testMetadataReconciliationNeverExposesUnpaginatedCaretGeometry() throws {
39 let nodes = (0..<1000).map { index in
40 CRDTNode(id: "p\(index)", type: "paragraph", children: [CRDTNode(id: "t\(index)", type: "text", runs: [CRDTRun(insert: "A paragraph kept together when pages are recalculated.")])])
41 }
42 let view = DocsPagedTextView(frame: CGRect(x: 0, y: 0, width: 636, height: 800))
43 let storage = try XCTUnwrap(view.textStorage)
44 storage.setAttributedString(NativeTextProjection.render(CRDTProjection(children: nodes, canUndo: false, canRedo: false)))
45 view.paged = true
46 view.layout()
47 let manager = try XCTUnwrap(view.layoutManager)
48 let container = try XCTUnwrap(view.textContainer as? DocsPageTextContainer)
49 XCTAssertGreaterThanOrEqual(container.blockStarts.count, 2)
50 let selected = storage.length - 3
51 view.setSelectedRange(NSRange(location: selected, length: 0))
52 func caretY() -> CGFloat {
53 manager.ensureLayout(for: container)
54 return manager.lineFragmentRect(forGlyphAt: manager.glyphIndexForCharacter(at: selected), effectiveRange: nil).minY
55 }
56 let before = caretY()
57 let start = ContinuousClock.now
58 for generation in 0..<10 {
59 storage.addAttribute(NSAttributedString.Key("MetadataOnly"), value: generation, range: NSRange(location: 0, length: storage.length))
60 XCTAssertEqual(caretY(), before, accuracy: 0.01, "AppKit can query the caret before the next view layout")
61 }
62 print("Ten metadata updates in a 1,000-paragraph document:", start.duration(to: .now))
63 view.layout()
64 XCTAssertEqual(caretY(), before, accuracy: 0.01)
65 XCTAssertEqual(view.selectedRange(), NSRange(location: selected, length: 0))
66 }
67
68 func testCaretBackgroundErasureStaysInsideRequestedRectangle() throws {
69 let view = DocsPagedTextView(frame: CGRect(x: 0, y: 0, width: 636, height: 800))
70 view.layout()
71 let bitmap = try XCTUnwrap(NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: 100, pixelsHigh: 100,
72 bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
73 colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0))
74 let context = try XCTUnwrap(NSGraphicsContext(bitmapImageRep: bitmap))
75 NSGraphicsContext.saveGraphicsState()
76 defer { NSGraphicsContext.restoreGraphicsState() }
77 NSGraphicsContext.current = context
78 NSColor.red.setFill()
79 CGRect(x: 0, y: 0, width: 100, height: 100).fill()
80 view.drawBackground(in: CGRect(x: 40, y: 40, width: 20, height: 20))
81 XCTAssertEqual(try XCTUnwrap(bitmap.colorAt(x: 20, y: 20)).greenComponent, 0, accuracy: 0.01,
82 "Erasing one caret must not paint over neighboring glyphs")
83 XCTAssertGreaterThan(try XCTUnwrap(bitmap.colorAt(x: 50, y: 50)).greenComponent, 0.9)
84 }
85
86 func testTypingKeepsCaretInsidePageBoundsWithStableOrigin() throws {
87 let nodes = (0..<60).map { index in
88 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))])])
89 }
90 let view = DocsTextView(frame: CGRect(x: 0, y: 0, width: 900, height: 800))
91 view.textStorage!.setAttributedString(NativeTextProjection.render(CRDTProjection(children: nodes, canUndo: false, canRedo: false)))
92 view.paged = true
93 view.layout()
94 view.setSelectedRange(NSRange(location: view.string.utf16.count - 1, length: 0))
95 for text in ["\n", "a", "\n", "b"] {
96 view.insertText(text, replacementRange: view.selectedRange())
97 view.layoutManager!.ensureLayout(for: view.textContainer!)
98 let glyph = view.layoutManager!.glyphIndexForCharacter(at: view.selectedRange().location)
99 let line = view.layoutManager!.lineFragmentRect(forGlyphAt: glyph, effectiveRange: nil)
100 XCTAssertEqual(view.textContainerOrigin, .zero)
101 XCTAssertGreaterThanOrEqual(view.bounds.maxY, line.maxY)
102 }
103 }
104
105 private func assertBlockStarts(_ container: DocsPageTextContainer, _ storage: NSTextStorage) {
106 for location in container.blockStarts.keys {
107 var range = NSRange()
108 XCTAssertNotNil(storage.attribute(.docsTopBlock, at: location, effectiveRange: &range))
109 XCTAssertEqual(range.location, location, "A separator must never move into a paragraph")
110 }
111 }
112}
113#endif
114