import Foundation import RastrilloApple import XCTest @testable import DocsKit final class DocumentDraftTests: XCTestCase { @MainActor func testImmediateDraftLostAcknowledgementRenameAndDiscard() async throws { guard let origin = ProcessInfo.processInfo.environment["DOCS_DRAFT_TEST_ORIGIN"] else { throw XCTSkip("Run test-document-drafts.py") } let client = NativeHTTP(origin: URL(string: origin)!) let model = DocsModel(client: client, draft: true) XCTAssertTrue(model.hasUnsavedChanges, "Protect the window before the first suspension") let start = ContinuousClock.now await model.newDraft() XCTAssertLessThan(start.duration(to: .now), .milliseconds(500), "Opening cannot wait for the server") XCTAssertNotNil(model.projection) XCTAssertTrue(model.isDraft) let identity = model.editorIdentity for command in [ ["op": JSONValue.string("display_edit"), "at": .number(0), "length": .number(0), "text": .string("Draft text!")], ["op": .string("display_edit"), "at": .number(10), "length": .number(1), "text": .string("")] ] { model.didRender(model.projectionGeneration) XCTAssertTrue(model.enqueue(command)) while model.hasPendingInput { try await Task.sleep(for: .milliseconds(5)) } } model.rename("My document") let deadline = ContinuousClock.now.advanced(by: .seconds(5)) while model.creatingDocument && .now < deadline { try await Task.sleep(for: .milliseconds(10)) } XCTAssertTrue(model.isDraft, "The fixture drops the first creation acknowledgement") XCTAssertEqual(model.projection?.children.map(\.plainText), ["Draft text"]) await model.retry() let saved = await model.saveAll() XCTAssertTrue(saved) try await Task.sleep(for: .milliseconds(1000)) // Let the stale metadata reply finish. XCTAssertEqual(model.editorIdentity, identity) XCTAssertEqual(model.detail?.title, "My document") struct Stats: Decodable { let documents: Int; let updates: [Data]; let title: String; let creations: Int; let metadata: Int } let stats: Stats = try await client.request("/stats") XCTAssertEqual(stats.documents, 1) XCTAssertEqual(stats.creations, 2) XCTAssertEqual(stats.title, "My document") XCTAssertGreaterThanOrEqual(stats.metadata, 2, "Reconnection must refresh capabilities while rename is pending") let peer = CRDTReplica(guid: "different-server-guid") _ = try await peer.open(update: Data()) for update in stats.updates { _ = try await peer.apply(update) } let projected = try await peer.projection() XCTAssertEqual(projected.children.map(\.plainText), ["Draft text"], "Insertions and deletes converge across GUIDs") await peer.close(); await model.stop() let closing = DocsModel(client: client, draft: true) await closing.newDraft() closing.renameInput = String(repeating: "a", count: 1025) let refused = await closing.saveAll() XCTAssertFalse(refused) XCTAssertNotNil(closing.renameInput, "Invalid title must remain editable") closing.renameInput = "Saved on close" let closed = await closing.saveAll() XCTAssertTrue(closed, "Close-save must await creation and the pending title") XCTAssertFalse(closing.isDraft) await closing.stop() let beforeDiscard: Stats = try await client.request("/stats") let discarded = DocsModel(client: client, draft: true) await discarded.newDraft() try await Task.sleep(for: .milliseconds(100)) await discarded.discardAll() try await Task.sleep(for: .milliseconds(1200)) XCTAssertNil(discarded.detail) XCTAssertFalse(discarded.hasUnsavedChanges) let after: Stats = try await client.request("/stats") XCTAssertEqual(after.updates.count, beforeDiscard.updates.count, "Late creation must not upload a discarded draft") } }