TeraProductStartupTests.swift (6976B)
1 @testable import TeraApp 2 import XCTest 3 4 @MainActor 5 final class TeraProductStartupTests: XCTestCase { 6 func testLocalAddIsEditableWhileTodayRefreshHasNotReturned() async throws { 7 let fixture = try await StartupFixture() 8 let refresh = await fixture.backend.pause(.refresh) 9 fixture.stores.start() 10 await refresh.entered.wait() 11 await TeraScopeFixtures.eventually { fixture.stores.add.isProductReady } 12 XCTAssertEqual(Set(fixture.stores.add.schemas.map(\.commandType)), Set(TeraAddCommandType.allCases)) 13 await TeraScopeFixtures.eventually { fixture.stores.add.drafts.first?.form?.content == "old" } 14 XCTAssertEqual(fixture.stores.add.drafts.first?.form?.content, "old") 15 XCTAssertEqual(fixture.stores.today.presentation.refresh, .refreshing) 16 fixture.stores.add.updateForm(\.content, "Compose while refresh is held") 17 XCTAssertTrue(fixture.stores.add.canSave) 18 XCTAssertEqual(fixture.stores.add.form.content, "Compose while refresh is held") 19 fixture.stores.suspend() 20 await refresh.resume.open() 21 try await fixture.close() 22 } 23 24 func testSuspendCancelsBothStartupReadsBeforeTheirBackendsReturn() async throws { 25 let fixture = try await StartupFixture() 26 let page = await fixture.backend.pause(.page) 27 let drafts = await fixture.backend.pause(.drafts) 28 let task = Task { await fixture.stores.resume() } 29 await page.entered.wait() 30 await drafts.entered.wait() 31 let validated = fixture.stores.add.schemas 32 XCTAssertEqual(validated.count, 5) 33 fixture.stores.suspend() 34 await task.value 35 XCTAssertTrue(fixture.stores.today.cards.isEmpty) 36 XCTAssertEqual(fixture.stores.add.schemas, validated) 37 XCTAssertTrue(fixture.stores.add.drafts.isEmpty) 38 XCTAssertTrue(fixture.stores.add.isProductReady) 39 XCTAssertEqual(fixture.stores.today.observationState, .stopped) 40 XCTAssertEqual(fixture.stores.add.observationState, .stopped) 41 await page.resume.open() 42 await drafts.resume.open() 43 try await fixture.close() 44 XCTAssertEqual(fixture.stores.add.schemas, validated) 45 XCTAssertTrue(fixture.stores.add.drafts.isEmpty) 46 XCTAssertTrue(fixture.stores.today.cards.isEmpty) 47 } 48 49 func testCanceledResumeStopsBothObserversAndCanStartAgain() async throws { 50 let fixture = try await StartupFixture() 51 let refresh = await fixture.backend.pause(.refresh) 52 let drafts = await fixture.backend.pause(.drafts) 53 let task = Task { await fixture.stores.resume() } 54 await refresh.entered.wait() 55 await drafts.entered.wait() 56 task.cancel() 57 await task.value 58 XCTAssertEqual(fixture.stores.today.observationState, .stopped) 59 XCTAssertEqual(fixture.stores.add.observationState, .stopped) 60 XCTAssertTrue(fixture.stores.add.isProductReady, "Validated local editing remains available while optional reads are cancelled") 61 await refresh.resume.open() 62 await drafts.resume.open() 63 await fixture.releaseObservers() 64 await fixture.stores.resume() 65 XCTAssertTrue(fixture.stores.add.isProductReady) 66 XCTAssertEqual(fixture.stores.add.schemas.count, 5) 67 try await fixture.close() 68 } 69 70 func testConcurrentStartsAndResumesKeepOneObserverPerStore() async throws { 71 let fixture = try await StartupFixture() 72 let refresh = await fixture.backend.pause(.refresh) 73 fixture.stores.start() 74 fixture.stores.start() 75 let first = Task { await fixture.stores.resume() } 76 let second = Task { await fixture.stores.resume() } 77 await refresh.entered.wait() 78 await TeraScopeFixtures.eventually { fixture.stores.add.isProductReady } 79 for observer in fixture.observers { 80 await observer.entered.wait() 81 } 82 let counts = await fixture.backend.counts 83 XCTAssertEqual(counts[.subscribe], 2) 84 XCTAssertEqual(counts[.refresh], 1) 85 XCTAssertEqual(counts[.drafts], 1) 86 await fixture.releaseObservers() 87 await refresh.resume.open() 88 await first.value 89 await second.value 90 let final = await fixture.backend.counts 91 XCTAssertEqual(final[.subscribe], 2) 92 XCTAssertEqual(final[.refresh], 1) 93 try await fixture.close() 94 } 95 96 func testReconfigurationRejectsOldStartupAndKeepsTheReplacementOwned() async throws { 97 for changed in [false, true] { 98 try await assertReplacement(changed: changed) 99 } 100 } 101 102 func testSuspendAndResumePreserveTheUnsentEditingForm() async throws { 103 let fixture = try await StartupFixture() 104 await fixture.releaseObservers() 105 await fixture.stores.resume() 106 fixture.stores.add.updateForm(\.content, "Unsent local editing") 107 fixture.stores.suspend() 108 await fixture.stores.resume() 109 XCTAssertTrue(fixture.stores.add.isProductReady) 110 XCTAssertEqual(fixture.stores.add.form.content, "Unsent local editing") 111 try await fixture.close() 112 } 113 114 private func assertReplacement(changed: Bool) async throws { 115 let fixture = try await StartupFixture() 116 let oldRefresh = await fixture.backend.pause(.refresh) 117 let oldDrafts = await fixture.backend.pause(.drafts) 118 let old = Task { await fixture.stores.resume() } 119 await oldRefresh.entered.wait() 120 await oldDrafts.entered.wait() 121 let snapshot = TeraScopeFixtures.snapshot(relay: changed ? "second" : "first") 122 await fixture.backend.configure(snapshot) 123 await fixture.backend.setDrafts([TeraScopeFixtures.draft("replacement", revision: 2)]) 124 fixture.stores.configure(snapshot: snapshot) 125 let newRefresh = await fixture.backend.pause(.refresh) 126 let current = Task { await fixture.stores.resume() } 127 await newRefresh.entered.wait() 128 await old.value 129 await TeraScopeFixtures.eventually { fixture.stores.add.isProductReady } 130 await TeraScopeFixtures.eventually { fixture.stores.add.drafts.first?.form?.content == "replacement" } 131 XCTAssertEqual(fixture.stores.add.drafts.first?.form?.content, "replacement") 132 XCTAssertEqual(fixture.stores.today.selectedContext?.relayURLs, snapshot.relay?.relays.map(\.url)) 133 fixture.stores.suspend() 134 await current.value 135 XCTAssertEqual(fixture.stores.today.observationState, .stopped) 136 await oldRefresh.resume.open() 137 await oldDrafts.resume.open() 138 await newRefresh.resume.open() 139 try await fixture.close() 140 XCTAssertEqual(fixture.stores.add.drafts.first?.form?.content, "replacement") 141 } 142 } 143 144 @MainActor 145 private struct StartupFixture { 146 let backend: TeraScopeBackend 147 let client: TeraRuntimeClient 148 let stores: TeraProductStores 149 let observers: [ResourceTestPause] 150 151 init() async throws { 152 backend = try TeraScopeBackend() 153 client = try await TeraScopeFixtures.client(backend) 154 stores = TeraProductStores(runtimeClient: client) 155 stores.configure(snapshot: TeraScopeFixtures.snapshot()) 156 observers = await [backend.pause(.subscribe), backend.pause(.subscribe)] 157 } 158 159 func releaseObservers() async { 160 for observer in observers { 161 await observer.resume.open() 162 } 163 } 164 165 func close() async throws { 166 stores.stop() 167 await releaseObservers() 168 _ = try await client.stop() 169 } 170 }