| 1 | import Foundation |
| 2 | import XCTest |
| 3 | @testable import DocsKit |
| 4 | |
| 5 | final class CRDTClipboardTests: XCTestCase { |
| 6 | @MainActor func testPasteThenImmediateTypingBeforeRenderPreservesMarksAndOffsets() async throws { |
| 7 | let replica = CRDTReplica(guid: "clipboard-typeahead") |
| 8 | _ = try await replica.open(update: Data()) |
| 9 | _ = try await replica.command(["op": .string("insert_element"), "at": .number(0), "type": .string("paragraph")]) |
| 10 | _ = try await replica.command(["op": .string("display_edit"), "at": .number(0), "length": .number(0), "text": .string("AB")]) |
| 11 | let presentation = try await replica.presentation(source: 0, selection: NSRange(location: 1, length: 0)) |
| 12 | let (source, projection, _) = try XCTUnwrap(presentation) |
| 13 | let content = NativeTextProjection.render(projection) |
| 14 | let blocks: [JSONValue] = [.object(["type": .string("paragraph"), "children": .array([.object(["type": .string("text"), "runs": .array([.object(["insert": .string("bold😀"), "marks": .object(["strong": .object([:])])])])])])])] |
| 15 | let pasted = try XCTUnwrap(DocumentClipboardInsertion.attributed(blocks: blocks, in: content, range: NSRange(location: 1, length: 0))) |
| 16 | _ = try await replica.command(["op": .string("display_paste"), "generation": .number(Double(source)), "at": .number(1), "length": .number(0), "blocks": .array(blocks)]) |
| 17 | let native = NSMutableAttributedString(attributedString: content) |
| 18 | native.replaceCharacters(in: NSRange(location: 1, length: 0), with: pasted) |
| 19 | var typing = try XCTUnwrap(TextEditMapping.displayCommand(in: native, range: NSRange(location: 7, length: 0), replacement: "!")) |
| 20 | typing["generation"] = .number(Double(source)) |
| 21 | _ = try await replica.command(typing) |
| 22 | let final = try await replica.projection() |
| 23 | XCTAssertEqual(final.children.map(\.plainText).joined(), "Abold😀!B") |
| 24 | let runs = final.children[0].children?.flatMap { $0.runs ?? [] } ?? [] |
| 25 | XCTAssertTrue(runs.contains { $0.insert.contains("!") && $0.marks?["strong"] == .object([:]) }) |
| 26 | await replica.close() |
| 27 | } |
| 28 | @MainActor func testRuleRevertAndRapidNewlineTypingUseSameDisplayedSource() async throws { |
| 29 | let replica = CRDTReplica(guid: "rules-typeahead") |
| 30 | _ = try await replica.open(update: Data()) |
| 31 | let receiver = CRDTReplica(guid: "rules-typeahead") |
| 32 | _ = try await receiver.open(update: Data()) |
| 33 | let (initial, _) = try await replica.command(["op": .string("insert_element"), "at": .number(0), "type": .string("paragraph")]) |
| 34 | _ = try await receiver.apply(initial) |
| 35 | let presentation = try await replica.presentation(source: 0, selection: NSRange(location: 0, length: 0)) |
| 36 | let source = try XCTUnwrap(presentation?.0) |
| 37 | for command: [String: JSONValue] in [ |
| 38 | ["op": .string("display_edit"), "at": .number(0), "length": .number(0), "text": .string("-")], |
| 39 | ["op": .string("display_autoformat"), "at": .number(0), "length": .number(1), "kind": .string("bullet_list"), "level": .number(0)], |
| 40 | ["op": .string("display_revert_rule"), "at": .number(0), "kind": .string("bullet_list"), "prior_type": .string("paragraph"), "prior_attrs": .object([:]), "text": .string("- ")], |
| 41 | ["op": .string("display_edit"), "at": .number(2), "length": .number(0), "text": .string("one\n")], |
| 42 | ["op": .string("display_edit"), "at": .number(6), "length": .number(0), "text": .string("two😀")] |
| 43 | ] { |
| 44 | var command = command; command["generation"] = .number(Double(source)) |
| 45 | let (update, _) = try await replica.command(command) |
| 46 | _ = try await receiver.apply(update) |
| 47 | let local = try await replica.projection(); let remote = try await receiver.projection() |
| 48 | XCTAssertEqual(local.children.map(\.id), remote.children.map(\.id)) |
| 49 | XCTAssertEqual(local.children.map(\.type), remote.children.map(\.type)) |
| 50 | XCTAssertEqual(local.children.map(\.plainText), remote.children.map(\.plainText)) |
| 51 | } |
| 52 | let final = try await replica.projection() |
| 53 | XCTAssertEqual(final.children.map(\.plainText), ["- one", "two😀"]) |
| 54 | XCTAssertEqual(final.children.map(\.type), ["paragraph", "paragraph"]) |
| 55 | let received = try await receiver.projection() |
| 56 | XCTAssertEqual(received.children.map(\.plainText), ["- one", "two😀"]) |
| 57 | XCTAssertEqual(received.children.map(\.type), ["paragraph", "paragraph"]) |
| 58 | await receiver.close() |
| 59 | await replica.close() |
| 60 | } |
| 61 | } |
| 62 | |