#if os(macOS) import AppKit import SwiftUI import XCTest @testable import DocsKit final class NativeClipboardInputTests: XCTestCase { @MainActor func testDelegateTextReplacementAlsoRunsStructuralInputRule() { var commands: [[String: JSONValue]] = [] 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 }) let coordinator = candidate.makeCoordinator() let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) text.delegate = coordinator text.ruleCommand = coordinator.ruleEdit coordinator.applyRender(candidate, to: text) let allowed = coordinator.textView(text, shouldChangeTextIn: NSRange(location: 1, length: 0), replacementString: " ") XCTAssertFalse(allowed) XCTAssertEqual(text.string, "\n") XCTAssertEqual(commands.last?["op"], .string("display_autoformat")) XCTAssertEqual(commands.last?["generation"], .number(1)) } @MainActor func testDelayedDuplicateRuleSelectionNotificationDoesNotCancelReversal() { var commands: [[String: JSONValue]] = [] var selected = NSRange(location: 1, length: 0) 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 }) let coordinator = candidate.makeCoordinator() let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) text.delegate = coordinator; text.ruleCommand = coordinator.ruleEdit coordinator.applyRender(candidate, to: text) text.insertText(" ", replacementRange: NSRange(location: NSNotFound, length: 0)) coordinator.textViewDidChangeSelection(Notification(name: NSTextView.didChangeSelectionNotification, object: text)) coordinator.textViewDidChangeSelection(Notification(name: NSTextView.didChangeSelectionNotification, object: text)) XCTAssertNotNil(text.lastInputRule) text.deleteBackward(nil) XCTAssertEqual(text.string, "- \n") XCTAssertEqual(commands.last?["op"], .string("display_revert_rule")) } @MainActor func testDirectDelegateBackspaceReversesRuleBeforeGenericDeletion() { var commands: [[String: JSONValue]] = [] var selected = NSRange(location: 1, length: 0) 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 }) let coordinator = candidate.makeCoordinator() let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) text.delegate = coordinator; text.ruleCommand = coordinator.ruleEdit coordinator.applyRender(candidate, to: text) text.insertText(" ", replacementRange: NSRange(location: NSNotFound, length: 0)) XCTAssertNotNil(text.lastInputRule) let allowed = coordinator.textView(text, shouldChangeTextIn: NSRange(location: 0, length: 0), replacementString: "") XCTAssertFalse(allowed) XCTAssertEqual(text.string, "- \n") XCTAssertEqual(commands.last?["op"], .string("display_revert_rule")) } @MainActor func testRichPasteOptimisticMarksMatchImmediateTyping() throws { let blocks: [JSONValue] = [.object(["type": .string("paragraph"), "children": .array([.object(["type": .string("text"), "runs": .array([.object(["insert": .string("bold😀"), "marks": .object(["strong": .object([:])])])])])])])] let initial = NSAttributedString(string: "AB\n") let insertion = try XCTUnwrap(DocumentClipboardInsertion.attributed(blocks: blocks, in: initial, range: NSRange(location: 1, length: 0))) let optimistic = NSMutableAttributedString(attributedString: initial) optimistic.replaceCharacters(in: NSRange(location: 1, length: 0), with: insertion) XCTAssertEqual(optimistic.string, "Abold😀B\n") let command = try XCTUnwrap(TextEditMapping.displayCommand(in: optimistic, range: NSRange(location: 7, length: 0), replacement: "!")) XCTAssertEqual(command["at"], .number(7)) XCTAssertEqual(command["marks"], .object(["strong": .object([:])])) } @MainActor func testNativeSpaceRuleAndImmediateBackspaceRestorePrefix() { let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) text.textStorage?.setAttributedString(NSAttributedString(string: "-\n")) text.setSelectedRange(NSRange(location: 1, length: 0)) var commands: [[String: JSONValue]] = [] text.ruleCommand = { commands.append($0); return true } text.insertText(" ", replacementRange: NSRange(location: NSNotFound, length: 0)) XCTAssertEqual(text.string, "\n") XCTAssertEqual(text.selectedRange().location, 0) XCTAssertEqual(commands.last?["op"], .string("display_autoformat")) text.deleteBackward(nil) XCTAssertEqual(text.string, "- \n") XCTAssertEqual(commands.last?["op"], .string("display_revert_rule")) let terminatorStyle = text.attributedString().attribute(.paragraphStyle, at: 2, effectiveRange: nil) as? NSParagraphStyle XCTAssertTrue(terminatorStyle?.textLists.isEmpty == true, "The restored paragraph terminator must not continue an old native list") } @MainActor func testImmediateSecondReturnLeavesEmptyNativeListWithoutAddingText() { let text = DocsTextView(frame: CGRect(x: 0, y: 0, width: 500, height: 700)) text.textStorage?.setAttributedString(NSAttributedString(string: "-\n")) text.setSelectedRange(NSRange(location: 1, length: 0)) var commands: [[String: JSONValue]] = [] text.ruleCommand = { commands.append($0); return true } text.insertText(" ", replacementRange: NSRange(location: NSNotFound, length: 0)) text.insertText("\n", replacementRange: NSRange(location: NSNotFound, length: 0)) XCTAssertEqual(text.string, "\n") XCTAssertEqual(commands.last?["op"], .string("display_exit_list")) } } #endif