TeraTodayContextTests.swift (8844B)
1 import Combine 2 @testable import TeraApp 3 import XCTest 4 5 @MainActor 6 final class TeraTodayContextTests: XCTestCase { 7 func testReplacementPreservesOnlyPresentSelectionAndIgnoresUnknownChoice() async throws { 8 let backend = try TeraScopeBackend() 9 let client = try await TeraScopeFixtures.client(backend) 10 let first = context("first"), second = context("second") 11 let store = TeraTodayStore(runtimeClient: client, contexts: [first, second], selectedContextID: second.id) 12 await store.reload(refreshProjection: false) 13 let original = store.scopeGeneration 14 store.replaceContexts([first, second, second], selectedID: nil) 15 store.selectContext(id: "absent") 16 XCTAssertEqual(store.scopeGeneration, original) 17 XCTAssertEqual(store.cards.map(\.id), second.relayURLs) 18 store.replaceContexts([second, first], selectedID: "absent") 19 XCTAssertEqual(store.selectedContext, second) 20 store.replaceContexts([first], selectedID: second.id) 21 XCTAssertEqual(store.selectedContext, first) 22 XCTAssertTrue(store.cards.isEmpty) 23 store.replaceContexts([], selectedID: nil) 24 XCTAssertNil(store.selectedContext) 25 XCTAssertFalse(store.canLoadNextPage) 26 store.stop() 27 _ = try await client.stop() 28 } 29 30 func testFirstConfigurationReconcilesInjectedChoicesWithRuntimeSnapshot() async throws { 31 let backend = try TeraScopeBackend() 32 let client = try await TeraScopeFixtures.client(backend) 33 let stale = context("old") 34 let store = TeraTodayStore(runtimeClient: client, contexts: [stale]) 35 await store.reload(refreshProjection: false) 36 let snapshot = TeraScopeFixtures.snapshot() 37 store.configure(snapshot: snapshot) 38 XCTAssertTrue(store.cards.isEmpty) 39 XCTAssertEqual(store.contexts, [.defaultContext(snapshot: snapshot)]) 40 XCTAssertEqual(store.selectedContext?.id, "default") 41 store.stop() 42 _ = try await client.stop() 43 } 44 45 func testAccountAndProfileChangesInvalidateSameNamedScopeButEvidenceDoesNot() async throws { 46 let backend = try TeraScopeBackend() 47 let client = try await TeraScopeFixtures.client(backend) 48 let store = TeraTodayStore(runtimeClient: client) 49 store.configure(snapshot: TeraScopeFixtures.snapshot()) 50 await store.reload(refreshProjection: false) 51 var generation = store.scopeGeneration 52 store.configure(snapshot: TeraScopeFixtures.snapshot(evidence: TeraScopeFixtures.evidence(observedAt: 9))) 53 XCTAssertEqual(store.scopeGeneration, generation) 54 XCTAssertFalse(store.cards.isEmpty) 55 for snapshot in [TeraScopeFixtures.snapshot(profile: "public"), TeraScopeFixtures.snapshot(account: "b", profile: "public")] { 56 let paused = await backend.pause(.page) 57 let old = Task { await store.reload(refreshProjection: false) } 58 await paused.entered.wait() 59 store.configure(snapshot: snapshot) 60 XCTAssertNotEqual(store.scopeGeneration, generation) 61 XCTAssertTrue(store.cards.isEmpty) 62 XCTAssertEqual(store.selectedContext?.id, "default") 63 await paused.resume.open() 64 await old.value 65 XCTAssertTrue(store.cards.isEmpty) 66 generation = store.scopeGeneration 67 await store.reload(refreshProjection: false) 68 XCTAssertEqual(store.scopeGeneration, generation) 69 } 70 store.stop() 71 _ = try await client.stop() 72 } 73 74 func testFirstPageSwitchClearsBeforeLabelForCachedAndEmptyDestinations() async throws { 75 for cached in [false, true] { 76 try await assertSwitch(nextPage: false, cached: cached) 77 } 78 } 79 80 func testSameIDReplacementClearsBeforePublishingChangedLabelAndDefinition() async throws { 81 let backend = try TeraScopeBackend() 82 let client = try await TeraScopeFixtures.client(backend) 83 let original = context("first") 84 let store = TeraTodayStore(runtimeClient: client, contexts: [original]) 85 await store.reload(refreshProjection: false) 86 let paused = await backend.pause(.page) 87 let old = Task { await store.reload(refreshProjection: false) } 88 await paused.entered.wait() 89 let updated = TeraLocalNetwork( 90 schemaVersion: 1, id: original.id, label: "Updated network", relayURLs: ["wss://second.example"], 91 locality: nil, followedAuthors: [], generation: 2 92 ) 93 var published = false 94 let subscription = store.$contexts.dropFirst().sink { values in 95 published = true 96 XCTAssertEqual(values, [updated]) 97 XCTAssertTrue(store.cards.isEmpty) 98 XCTAssertEqual(store.presentation.content, .notLoaded) 99 } 100 store.replaceContexts([updated], selectedID: nil) 101 XCTAssertTrue(published) 102 XCTAssertEqual(store.selectedContext, updated) 103 await TeraScopeFixtures.eventually { store.cards.map(\.id) == updated.relayURLs } 104 await paused.resume.open() 105 await old.value 106 XCTAssertEqual(store.cards.map(\.id), updated.relayURLs) 107 subscription.cancel() 108 store.stop() 109 _ = try await client.stop() 110 } 111 112 func testNextPageSwitchClearsBeforeLabelForCachedAndEmptyDestinations() async throws { 113 for cached in [false, true] { 114 try await assertSwitch(nextPage: true, cached: cached) 115 } 116 } 117 118 func testContextSwitchClearsSupportingStoresBeforeLabelAndRejectsLateResults() async throws { 119 let backend = try TeraScopeBackend() 120 let client = try await TeraScopeFixtures.client(backend) 121 let stores = TeraProductStores(runtimeClient: client) 122 stores.configure(snapshot: TeraScopeFixtures.snapshot()) 123 stores.search.updateQuery("old") 124 await stores.search.search() 125 await stores.me.reload() 126 XCTAssertFalse(stores.search.results.isEmpty) 127 XCTAssertNotNil(stores.me.snapshot) 128 let searchPause = await backend.pause(.search), mePause = await backend.pause(.me) 129 let search = Task { await stores.search.search() }, me = Task { await stores.me.reload() } 130 await searchPause.entered.wait() 131 await mePause.entered.wait() 132 var published = false 133 let subscription = stores.today.$selectedContextID.dropFirst().sink { selected in 134 published = true 135 XCTAssertEqual(selected, "second") 136 XCTAssertTrue(stores.search.results.isEmpty) 137 XCTAssertNil(stores.me.snapshot) 138 XCTAssertEqual(stores.me.observationState, .stopped) 139 } 140 stores.today.replaceContexts([context("second")], selectedID: nil) 141 XCTAssertTrue(published) 142 await searchPause.resume.open() 143 await mePause.resume.open() 144 await search.value 145 await me.value 146 XCTAssertTrue(stores.search.results.isEmpty) 147 XCTAssertNil(stores.me.snapshot) 148 await stores.me.reload() 149 let actual = await backend.lastMeContext 150 XCTAssertEqual(actual, stores.today.selectedContext) 151 subscription.cancel() 152 stores.stop() 153 _ = try await client.stop() 154 } 155 156 private func assertSwitch(nextPage: Bool, cached: Bool) async throws { 157 let backend = try TeraScopeBackend() 158 let client = try await TeraScopeFixtures.client(backend) 159 let first = context("first"), second = context("second") 160 let store = TeraTodayStore(runtimeClient: client, contexts: [first, second]) 161 await backend.setPage(page(["old"], cursor: "next")) 162 await backend.setPage(page(["old-next"]), cursor: "next") 163 await store.reload(refreshProjection: false) 164 let paused = await backend.pause(.page) 165 let old = Task { 166 if nextPage { 167 await store.loadNextPage() 168 } else { 169 await store.reload(refreshProjection: false) 170 } 171 } 172 await paused.entered.wait() 173 let expected = cached ? ["new"] : [] 174 await backend.setPage(page(expected)) 175 var changes = 0 176 let subscription = store.$selectedContextID.dropFirst().sink { selected in 177 changes += 1 178 XCTAssertEqual(selected, second.id) 179 XCTAssertTrue(store.cards.isEmpty) 180 XCTAssertEqual(store.presentation.content, .notLoaded) 181 XCTAssertFalse(store.isLoadingNextPage) 182 XCTAssertFalse(store.canLoadNextPage) 183 } 184 let generation = store.scopeGeneration 185 store.selectContext(id: second.id) 186 XCTAssertNotEqual(store.scopeGeneration, generation) 187 XCTAssertEqual(changes, 1) 188 XCTAssertEqual(store.selectedContext?.label, "second") 189 await TeraScopeFixtures.eventually { store.presentation.content == (cached ? .available : .empty) } 190 await paused.resume.open() 191 await old.value 192 XCTAssertEqual(store.cards.map(\.id), expected) 193 XCTAssertEqual(store.selectedContext, second) 194 subscription.cancel() 195 store.stop() 196 _ = try await client.stop() 197 } 198 199 private func context(_ id: String) -> TeraLocalNetwork { 200 TeraLocalNetwork(schemaVersion: 1, id: id, label: id, relayURLs: ["wss://\(id).example"], 201 locality: nil, followedAuthors: [], generation: 1) 202 } 203 204 private func page(_ ids: [String], cursor: String? = nil) -> TeraTodayPage { 205 TeraTodayPage(asOfUnixSeconds: 1, items: ids.map(TeraScopeFixtures.card), nextCursor: cursor, calendar: TeraScopeFixtures.viewerCalendar(asOf: 1)) 206 } 207 }