| 1 | #if os(macOS) |
| 2 | import AppKit |
| 3 | import SwiftUI |
| 4 | import XCTest |
| 5 | @testable import DocsKit |
| 6 | |
| 7 | @MainActor private final class RenderState { |
| 8 | var pending = false |
| 9 | var allowed: UInt64 = 1 |
| 10 | var selection = NSRange(location: 0, length: 0) |
| 11 | var acknowledgements: [UInt64] = [] |
| 12 | var commands: [[String: JSONValue]] = [] |
| 13 | func candidate(_ content: NSAttributedString, generation: UInt64) -> NativeDocumentTextView { |
| 14 | NativeDocumentTextView(content: content, generation: generation, |
| 15 | didRender: { self.acknowledgements.append($0) }, compositionChanged: { _ in }, |
| 16 | isEditable: true, paged: false, pendingInput: pending, |
| 17 | canRender: { !self.pending && self.allowed == $0 }, currentSelection: { self.selection }, |
| 18 | selection: Binding(get: { self.selection }, set: { self.selection = $0 }), |
| 19 | edit: { self.pending = true; self.commands.append($0); return true }) |
| 20 | } |
| 21 | } |
| 22 | final class NativeRenderTests: XCTestCase { |
| 23 | @MainActor func testDelayedCandidateCannotOverwriteAcceptedNativeTyping() { |
| 24 | let state = RenderState() |
| 25 | let initial = state.candidate(NSAttributedString(string: "a😀b\n"), generation: 1) |
| 26 | let coordinator = initial.makeCoordinator() |
| 27 | let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) |
| 28 | text.delegate = coordinator |
| 29 | coordinator.applyRender(initial, to: text) |
| 30 | state.allowed = 2 |
| 31 | let delayed = state.candidate(NSAttributedString(string: "older server view\n"), generation: 2) |
| 32 | XCTAssertFalse(delayed.pendingInput) |
| 33 | text.insertText("X", replacementRange: NSRange(location: 4, length: 0)) |
| 34 | XCTAssertTrue(state.pending) |
| 35 | XCTAssertEqual(text.string, "a😀bX\n") |
| 36 | XCTAssertEqual(state.commands.last?["generation"], .number(1)) |
| 37 | coordinator.applyRender(delayed, to: text) |
| 38 | XCTAssertEqual(text.string, "a😀bX\n") |
| 39 | XCTAssertEqual(state.acknowledgements, [1]) |
| 40 | state.pending = false; state.allowed = 3 |
| 41 | coordinator.applyRender(delayed, to: text) |
| 42 | XCTAssertEqual(text.string, "a😀bX\n") |
| 43 | state.selection = NSRange(location: 6, length: 0) |
| 44 | let current = state.candidate(NSAttributedString(string: "a😀bX!\n"), generation: 3) |
| 45 | coordinator.applyRender(current, to: text) |
| 46 | XCTAssertEqual(text.string, "a😀bX!\n") |
| 47 | XCTAssertEqual(text.selectedRange(), NSRange(location: 6, length: 0)) |
| 48 | XCTAssertEqual(state.acknowledgements, [1, 3]) |
| 49 | } |
| 50 | @MainActor func testMarkedTextSurvivesPeerPresentationAndCommitsOnce() { |
| 51 | let state = RenderState() |
| 52 | let initial = state.candidate(NSAttributedString(string: "a😀b\n"), generation: 1) |
| 53 | let coordinator = initial.makeCoordinator() |
| 54 | let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) |
| 55 | text.delegate = coordinator |
| 56 | text.compositionCommitted = { coordinator.commitComposition(before: $0, after: $1) } |
| 57 | coordinator.applyRender(initial, to: text) |
| 58 | text.setMarkedText("に", selectedRange: NSRange(location: 1, length: 0), replacementRange: NSRange(location: 1, length: 2)) |
| 59 | XCTAssertTrue(text.hasMarkedText()) |
| 60 | XCTAssertTrue(state.commands.isEmpty) |
| 61 | state.allowed = 2 |
| 62 | coordinator.applyRender(state.candidate(NSAttributedString(string: "peer update\n"), generation: 2), to: text) |
| 63 | XCTAssertEqual(text.string, "aにb\n") |
| 64 | XCTAssertEqual(state.acknowledgements, [1]) |
| 65 | text.unmarkText() |
| 66 | XCTAssertEqual(state.commands.count, 1) |
| 67 | XCTAssertEqual(state.commands.first?["generation"], .number(1)) |
| 68 | XCTAssertEqual(state.commands.first?["at"], .number(1)) |
| 69 | XCTAssertEqual(state.commands.first?["length"], .number(2)) |
| 70 | XCTAssertEqual(state.commands.first?["text"], .string("に")) |
| 71 | } |
| 72 | |
| 73 | @MainActor func testAttributeOnlyRefreshPreservesNativeCaret() { |
| 74 | let state = RenderState() |
| 75 | let initial = state.candidate(NSAttributedString(string: "abcdef\n"), generation: 1) |
| 76 | let coordinator = initial.makeCoordinator() |
| 77 | let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) |
| 78 | coordinator.applyRender(initial, to: text) |
| 79 | text.setSelectedRange(NSRange(location: 4, length: 0)) |
| 80 | state.selection = NSRange(location: 0, length: 0) |
| 81 | let changed = NSAttributedString(string: "abcdef\n", attributes: [.foregroundColor: NSColor.red]) |
| 82 | coordinator.applyRender(state.candidate(changed, generation: 1), to: text) |
| 83 | XCTAssertEqual(text.selectedRange(), NSRange(location: 4, length: 0)) |
| 84 | XCTAssertEqual(text.textStorage?.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? NSColor, .red) |
| 85 | } |
| 86 | } |
| 87 | #endif |
| 88 | |