| 1 | import Foundation |
| 2 | import RastrilloApple |
| 3 | import XCTest |
| 4 | @testable import DocsKit |
| 5 | |
| 6 | final class DocumentDraftTests: XCTestCase { |
| 7 | @MainActor func testImmediateDraftLostAcknowledgementRenameAndDiscard() async throws { |
| 8 | guard let origin = ProcessInfo.processInfo.environment["DOCS_DRAFT_TEST_ORIGIN"] else { throw XCTSkip("Run test-document-drafts.py") } |
| 9 | let client = NativeHTTP(origin: URL(string: origin)!) |
| 10 | let model = DocsModel(client: client, draft: true) |
| 11 | XCTAssertTrue(model.hasUnsavedChanges, "Protect the window before the first suspension") |
| 12 | let start = ContinuousClock.now |
| 13 | await model.newDraft() |
| 14 | XCTAssertLessThan(start.duration(to: .now), .milliseconds(500), "Opening cannot wait for the server") |
| 15 | XCTAssertNotNil(model.projection) |
| 16 | XCTAssertTrue(model.isDraft) |
| 17 | let identity = model.editorIdentity |
| 18 | for command in [ |
| 19 | ["op": JSONValue.string("display_edit"), "at": .number(0), "length": .number(0), "text": .string("Draft text!")], |
| 20 | ["op": .string("display_edit"), "at": .number(10), "length": .number(1), "text": .string("")] |
| 21 | ] { |
| 22 | model.didRender(model.projectionGeneration) |
| 23 | XCTAssertTrue(model.enqueue(command)) |
| 24 | while model.hasPendingInput { try await Task.sleep(for: .milliseconds(5)) } |
| 25 | } |
| 26 | model.rename("My document") |
| 27 | let deadline = ContinuousClock.now.advanced(by: .seconds(5)) |
| 28 | while model.creatingDocument && .now < deadline { try await Task.sleep(for: .milliseconds(10)) } |
| 29 | XCTAssertTrue(model.isDraft, "The fixture drops the first creation acknowledgement") |
| 30 | XCTAssertEqual(model.projection?.children.map(\.plainText), ["Draft text"]) |
| 31 | await model.retry() |
| 32 | let saved = await model.saveAll() |
| 33 | XCTAssertTrue(saved) |
| 34 | try await Task.sleep(for: .milliseconds(1000)) // Let the stale metadata reply finish. |
| 35 | XCTAssertEqual(model.editorIdentity, identity) |
| 36 | XCTAssertEqual(model.detail?.title, "My document") |
| 37 | struct Stats: Decodable { let documents: Int; let updates: [Data]; let title: String; let creations: Int; let metadata: Int } |
| 38 | let stats: Stats = try await client.request("/stats") |
| 39 | XCTAssertEqual(stats.documents, 1) |
| 40 | XCTAssertEqual(stats.creations, 2) |
| 41 | XCTAssertEqual(stats.title, "My document") |
| 42 | XCTAssertGreaterThanOrEqual(stats.metadata, 2, "Reconnection must refresh capabilities while rename is pending") |
| 43 | let peer = CRDTReplica(guid: "different-server-guid") |
| 44 | _ = try await peer.open(update: Data()) |
| 45 | for update in stats.updates { _ = try await peer.apply(update) } |
| 46 | let projected = try await peer.projection() |
| 47 | XCTAssertEqual(projected.children.map(\.plainText), ["Draft text"], "Insertions and deletes converge across GUIDs") |
| 48 | await peer.close(); await model.stop() |
| 49 | |
| 50 | let closing = DocsModel(client: client, draft: true) |
| 51 | await closing.newDraft() |
| 52 | closing.renameInput = String(repeating: "a", count: 1025) |
| 53 | let refused = await closing.saveAll() |
| 54 | XCTAssertFalse(refused) |
| 55 | XCTAssertNotNil(closing.renameInput, "Invalid title must remain editable") |
| 56 | closing.renameInput = "Saved on close" |
| 57 | let closed = await closing.saveAll() |
| 58 | XCTAssertTrue(closed, "Close-save must await creation and the pending title") |
| 59 | XCTAssertFalse(closing.isDraft) |
| 60 | await closing.stop() |
| 61 | let beforeDiscard: Stats = try await client.request("/stats") |
| 62 | let discarded = DocsModel(client: client, draft: true) |
| 63 | await discarded.newDraft() |
| 64 | try await Task.sleep(for: .milliseconds(100)) |
| 65 | await discarded.discardAll() |
| 66 | try await Task.sleep(for: .milliseconds(1200)) |
| 67 | XCTAssertNil(discarded.detail) |
| 68 | XCTAssertFalse(discarded.hasUnsavedChanges) |
| 69 | let after: Stats = try await client.request("/stats") |
| 70 | XCTAssertEqual(after.updates.count, beforeDiscard.updates.count, "Late creation must not upload a discarded draft") |
| 71 | } |
| 72 | } |
| 73 | |