| 1 | #if os(macOS) |
| 2 | import AppKit |
| 3 | import SwiftUI |
| 4 | import XCTest |
| 5 | @testable import DocsKit |
| 6 | |
| 7 | final class NativeClipboardInputTests: XCTestCase { |
| 8 | @MainActor func testDelegateTextReplacementAlsoRunsStructuralInputRule() { |
| 9 | var commands: [[String: JSONValue]] = [] |
| 10 | let candidate = NativeDocumentTextView(content: NSAttributedString(string: "-\n"), generation: 1, didRender: { _ in }, compositionChanged: { _ in }, isEditable: true, paged: false, pendingInput: false, canRender: { _ in true }, currentSelection: { NSRange(location: 1, length: 0) }, selection: .constant(NSRange(location: 1, length: 0)), edit: { commands.append($0); return true }) |
| 11 | let coordinator = candidate.makeCoordinator() |
| 12 | let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) |
| 13 | text.delegate = coordinator |
| 14 | text.ruleCommand = coordinator.ruleEdit |
| 15 | coordinator.applyRender(candidate, to: text) |
| 16 | let allowed = coordinator.textView(text, shouldChangeTextIn: NSRange(location: 1, length: 0), replacementString: " ") |
| 17 | XCTAssertFalse(allowed) |
| 18 | XCTAssertEqual(text.string, "\n") |
| 19 | XCTAssertEqual(commands.last?["op"], .string("display_autoformat")) |
| 20 | XCTAssertEqual(commands.last?["generation"], .number(1)) |
| 21 | } |
| 22 | @MainActor func testDelayedDuplicateRuleSelectionNotificationDoesNotCancelReversal() { |
| 23 | var commands: [[String: JSONValue]] = [] |
| 24 | var selected = NSRange(location: 1, length: 0) |
| 25 | let candidate = NativeDocumentTextView(content: NSAttributedString(string: "-\n"), generation: 1, didRender: { _ in }, compositionChanged: { _ in }, isEditable: true, paged: false, pendingInput: false, canRender: { _ in true }, currentSelection: { selected }, selection: Binding(get: { selected }, set: { selected = $0 }), edit: { commands.append($0); return true }) |
| 26 | let coordinator = candidate.makeCoordinator() |
| 27 | let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) |
| 28 | text.delegate = coordinator; text.ruleCommand = coordinator.ruleEdit |
| 29 | coordinator.applyRender(candidate, to: text) |
| 30 | text.insertText(" ", replacementRange: NSRange(location: NSNotFound, length: 0)) |
| 31 | coordinator.textViewDidChangeSelection(Notification(name: NSTextView.didChangeSelectionNotification, object: text)) |
| 32 | coordinator.textViewDidChangeSelection(Notification(name: NSTextView.didChangeSelectionNotification, object: text)) |
| 33 | XCTAssertNotNil(text.lastInputRule) |
| 34 | text.deleteBackward(nil) |
| 35 | XCTAssertEqual(text.string, "- \n") |
| 36 | XCTAssertEqual(commands.last?["op"], .string("display_revert_rule")) |
| 37 | } |
| 38 | @MainActor func testDirectDelegateBackspaceReversesRuleBeforeGenericDeletion() { |
| 39 | var commands: [[String: JSONValue]] = [] |
| 40 | var selected = NSRange(location: 1, length: 0) |
| 41 | let candidate = NativeDocumentTextView(content: NSAttributedString(string: "-\n"), generation: 1, didRender: { _ in }, compositionChanged: { _ in }, isEditable: true, paged: false, pendingInput: false, canRender: { _ in true }, currentSelection: { selected }, selection: Binding(get: { selected }, set: { selected = $0 }), edit: { commands.append($0); return true }) |
| 42 | let coordinator = candidate.makeCoordinator() |
| 43 | let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) |
| 44 | text.delegate = coordinator; text.ruleCommand = coordinator.ruleEdit |
| 45 | coordinator.applyRender(candidate, to: text) |
| 46 | text.insertText(" ", replacementRange: NSRange(location: NSNotFound, length: 0)) |
| 47 | XCTAssertNotNil(text.lastInputRule) |
| 48 | let allowed = coordinator.textView(text, shouldChangeTextIn: NSRange(location: 0, length: 0), replacementString: "") |
| 49 | XCTAssertFalse(allowed) |
| 50 | XCTAssertEqual(text.string, "- \n") |
| 51 | XCTAssertEqual(commands.last?["op"], .string("display_revert_rule")) |
| 52 | } |
| 53 | @MainActor func testRichPasteOptimisticMarksMatchImmediateTyping() throws { |
| 54 | let blocks: [JSONValue] = [.object(["type": .string("paragraph"), "children": .array([.object(["type": .string("text"), "runs": .array([.object(["insert": .string("bold😀"), "marks": .object(["strong": .object([:])])])])])])])] |
| 55 | let initial = NSAttributedString(string: "AB\n") |
| 56 | let insertion = try XCTUnwrap(DocumentClipboardInsertion.attributed(blocks: blocks, in: initial, range: NSRange(location: 1, length: 0))) |
| 57 | let optimistic = NSMutableAttributedString(attributedString: initial) |
| 58 | optimistic.replaceCharacters(in: NSRange(location: 1, length: 0), with: insertion) |
| 59 | XCTAssertEqual(optimistic.string, "Abold😀B\n") |
| 60 | let command = try XCTUnwrap(TextEditMapping.displayCommand(in: optimistic, range: NSRange(location: 7, length: 0), replacement: "!")) |
| 61 | XCTAssertEqual(command["at"], .number(7)) |
| 62 | XCTAssertEqual(command["marks"], .object(["strong": .object([:])])) |
| 63 | } |
| 64 | @MainActor func testNativeSpaceRuleAndImmediateBackspaceRestorePrefix() { |
| 65 | let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) |
| 66 | text.textStorage?.setAttributedString(NSAttributedString(string: "-\n")) |
| 67 | text.setSelectedRange(NSRange(location: 1, length: 0)) |
| 68 | var commands: [[String: JSONValue]] = [] |
| 69 | text.ruleCommand = { commands.append($0); return true } |
| 70 | text.insertText(" ", replacementRange: NSRange(location: NSNotFound, length: 0)) |
| 71 | XCTAssertEqual(text.string, "\n") |
| 72 | XCTAssertEqual(text.selectedRange().location, 0) |
| 73 | XCTAssertEqual(commands.last?["op"], .string("display_autoformat")) |
| 74 | text.deleteBackward(nil) |
| 75 | XCTAssertEqual(text.string, "- \n") |
| 76 | XCTAssertEqual(commands.last?["op"], .string("display_revert_rule")) |
| 77 | let terminatorStyle = text.attributedString().attribute(.paragraphStyle, at: 2, effectiveRange: nil) as? NSParagraphStyle |
| 78 | XCTAssertTrue(terminatorStyle?.textLists.isEmpty == true, "The restored paragraph terminator must not continue an old native list") |
| 79 | } |
| 80 | @MainActor func testImmediateSecondReturnLeavesEmptyNativeListWithoutAddingText() { |
| 81 | let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) |
| 82 | text.textStorage?.setAttributedString(NSAttributedString(string: "-\n")) |
| 83 | text.setSelectedRange(NSRange(location: 1, length: 0)) |
| 84 | var commands: [[String: JSONValue]] = [] |
| 85 | text.ruleCommand = { commands.append($0); return true } |
| 86 | text.insertText(" ", replacementRange: NSRange(location: NSNotFound, length: 0)) |
| 87 | text.insertText("\n", replacementRange: NSRange(location: NSNotFound, length: 0)) |
| 88 | XCTAssertEqual(text.string, "\n") |
| 89 | XCTAssertEqual(commands.last?["op"], .string("display_exit_list")) |
| 90 | } |
| 91 | } |
| 92 | #endif |
| 93 | |