oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import RastrilloApple
3import RastrilloTestSupport
4import XCTest
5@testable import DocsKit
6
7/// Stands in for NativeConnection: which transport the account has right now.
8@MainActor private final class Session {
9 var current: NativeTransport?
10 init(_ current: NativeTransport?) { self.current = current }
11}
12
13private func list(_ titles: [String]) -> Data {
14 let documents = titles.enumerated().map { #"{"sqid":"d\#($0.offset)","title":"\#($0.element)"}"# }
15 return Data(#"{"documents":[\#(documents.joined(separator: ","))],"can_create":true}"#.utf8)
16}
17
18/// A server for one writable, empty document: its stream stays open without
19/// events, and every update is accepted. Answers by request, since the model's
20/// stream and saves race.
21private func documentServer() -> ScriptedTransport {
22 ScriptedTransport { call in
23 switch (call.kind, call.method, call.path) {
24 case (.request, "GET", "/d/doc"):
25 return .respond(status: 200, body: Data(#"{"sqid":"doc","title":"Doc","guid":"doc-guid","can_write":true,"can_share":false}"#.utf8))
26 case (.request, "GET", "/d/doc/updates"):
27 return .respond(status: 200, headers: ["Docs-Seq": "0"])
28 case (.stream, _, "/d/doc/stream"):
29 return .hang
30 case (.request, "POST", "/d/doc/updates"):
31 return .respond(status: 204)
32 default:
33 return nil
34 }
35 }
36}
37
38final class TransportSupplierTests: XCTestCase {
39 @MainActor func testEachRequestUsesTheSessionsCurrentTransport() async {
40 let first = ScriptedTransport([.respond(status: 200, body: list(["Before"]))])
41 let replacement = ScriptedTransport([.respond(status: 200, body: list(["After", "Signing in again"]))])
42 let session = Session(first)
43 let model = DocsModel(transport: { session.current })
44 await model.load()
45 XCTAssertEqual(model.documents.map(\.title), ["Before"])
46 session.current = replacement
47 await model.load()
48 XCTAssertEqual(model.listState, .loaded)
49 XCTAssertEqual(model.documents.map(\.title), ["After", "Signing in again"])
50 XCTAssertEqual(first.calls.count, 1, "The model kept using the transport it was created with")
51 XCTAssertEqual(replacement.calls.count, 1)
52 }
53
54 /// The case the supplier exists for: an edit made while one session
55 /// is in place is sent with whichever session is current when it is sent.
56 @MainActor func testUnsentEditGoesOutOnTheReplacementTransport() async throws {
57 let ended = documentServer()
58 let session = Session(ended)
59 let model = DocsModel(transport: { session.current })
60 await model.open("doc")
61 XCTAssertEqual(model.detail?.sqid, "doc")
62 let deadline = ContinuousClock.now.advanced(by: .seconds(5))
63 // Opening an empty document inserts its first paragraph and saves it.
64 while model.hasUnsavedChanges && .now < deadline { try await Task.sleep(for: .milliseconds(10)) }
65 XCTAssertFalse(model.hasUnsavedChanges)
66 let sentBefore = ended.committed.filter { $0.method == "POST" }.count
67
68 let replacement = documentServer()
69 session.current = replacement
70 model.didRender(model.projectionGeneration)
71 XCTAssertTrue(model.enqueue(["op": .string("display_edit"), "at": .number(0), "length": .number(0), "text": .string("Hello")]))
72 while model.hasUnsavedChanges && .now < deadline { try await Task.sleep(for: .milliseconds(10)) }
73 XCTAssertFalse(model.hasUnsavedChanges, "The edit was never acknowledged")
74
75 XCTAssertEqual(ended.committed.filter { $0.method == "POST" }.count, sentBefore, "The edit went out on the ended session")
76 XCTAssertEqual(replacement.committed.map(\.path), ["/d/doc/updates"])
77 await model.discardAll()
78 }
79}
80