TeraTodayCacheFirstTests.swift (6807B)
1 @testable import TeraApp 2 import XCTest 3 4 @MainActor 5 final class TeraTodayCacheFirstTests: XCTestCase { 6 func testInitialLocalContentAppearsBeforeAStalledRefreshAndSurvivesCancellation() async throws { 7 for starting in [true, false] { 8 for cards in [[], [TeraScopeFixtures.card("saved")]] { 9 let backend = try TeraScopeBackend() 10 await backend.setPage(page(cards)) 11 let (client, store) = try await makeStore(backend) 12 let refresh = await backend.pause(.refresh) 13 let task = Task { 14 if starting { 15 await store.start() 16 } else { 17 await store.reload() 18 } 19 } 20 await refresh.entered.wait() 21 XCTAssertEqual(store.cards, cards) 22 XCTAssertEqual(store.presentation.content, cards.isEmpty ? .empty : .available) 23 XCTAssertEqual(store.presentation.refresh, .refreshing) 24 XCTAssertEqual(store.presentation.freshness, .unconfirmed) 25 XCTAssertFalse(store.presentation.isReading) 26 let counts = await backend.counts 27 XCTAssertEqual(counts[.page], 1) 28 XCTAssertEqual(counts[.refresh], 1) 29 task.cancel() 30 await task.value 31 XCTAssertEqual(store.cards, cards) 32 XCTAssertEqual(store.presentation.refresh, .idle) 33 store.stop() 34 // Release only after the caller has completed without a relay result. 35 await refresh.resume.open() 36 _ = try await client.stop() 37 } 38 } 39 } 40 41 func testSuccessfulRefreshReplacesTheAlreadyVisibleLocalPage() async throws { 42 let backend = try TeraScopeBackend() 43 await backend.setPage(page([TeraScopeFixtures.card("saved")])) 44 let (client, store) = try await makeStore(backend) 45 let refresh = await backend.pause(.refresh) 46 let task = Task { await store.reload() } 47 await refresh.entered.wait() 48 XCTAssertEqual(store.cards.map(\.id), ["saved"]) 49 await backend.setPage(page([TeraScopeFixtures.card("updated")])) 50 let read = await backend.pause(.page) 51 await refresh.resume.open() 52 await read.entered.wait() 53 XCTAssertEqual(store.cards.map(\.id), ["saved"]) 54 XCTAssertTrue(store.presentation.isReading) 55 XCTAssertEqual(store.presentation.freshness, .unconfirmed) 56 await read.resume.open() 57 await task.value 58 XCTAssertEqual(store.cards.map(\.id), ["updated"]) 59 XCTAssertEqual(store.presentation.freshness, .refreshed(contentGeneration: 1)) 60 _ = try await client.stop() 61 } 62 63 func testCancelingTheFirstLocalReadDoesNotStartRefreshOrClaimNetworkActivity() async throws { 64 let backend = try TeraScopeBackend() 65 let (client, store) = try await makeStore(backend) 66 let read = await backend.pause(.page) 67 let task = Task { await store.reload() } 68 await read.entered.wait() 69 XCTAssertEqual(store.presentation.refresh, .idle) 70 XCTAssertTrue(store.presentation.isReading) 71 XCTAssertEqual(store.presentation.content, .notLoaded) 72 task.cancel() 73 await task.value 74 XCTAssertFalse(store.presentation.isReading) 75 XCTAssertEqual(store.presentation.refresh, .idle) 76 let counts = await backend.counts 77 XCTAssertNil(counts[.refresh]) 78 await read.resume.open() 79 _ = try await client.stop() 80 } 81 82 func testLateRefreshedPageCannotReplaceANewerLocalReload() async throws { 83 let backend = try TeraScopeBackend() 84 let (client, store) = try await makeStore(backend) 85 let refresh = await backend.pause(.refresh) 86 let old = Task { await store.reload() } 87 await refresh.entered.wait() 88 await backend.setPage(page([TeraScopeFixtures.card("obsolete")])) 89 let oldPage = await backend.pause(.page) 90 await refresh.resume.open() 91 await oldPage.entered.wait() 92 await backend.setPage(page([TeraScopeFixtures.card("current")])) 93 await store.reload(refreshProjection: false) 94 await oldPage.resume.open() 95 await old.value 96 XCTAssertEqual(store.cards.map(\.id), ["current"]) 97 XCTAssertEqual(store.presentation.freshness, .unconfirmed) 98 XCTAssertFalse(store.presentation.isReading) 99 _ = try await client.stop() 100 } 101 102 func testLateCachedPaginationCannotAppendToTheRefreshedPageAtTheSameTime() async throws { 103 let backend = try TeraScopeBackend() 104 await backend.setPage(page([TeraScopeFixtures.card("saved")], next: "old-next")) 105 await backend.setPage(page([TeraScopeFixtures.card("old-next")]), cursor: "old-next") 106 let (client, store) = try await makeStore(backend) 107 let refresh = await backend.pause(.refresh) 108 let reload = Task { await store.reload() } 109 await refresh.entered.wait() 110 XCTAssertTrue(store.canLoadNextPage) 111 let oldPage = await backend.pause(.page) 112 let pagination = Task { await store.loadNextPage() } 113 await oldPage.entered.wait() 114 await backend.setPage(page([TeraScopeFixtures.card("updated")], next: "new-next")) 115 await backend.setPage(page([TeraScopeFixtures.card("new-next")]), cursor: "new-next") 116 await refresh.resume.open() 117 await reload.value 118 XCTAssertEqual(store.cards.map(\.id), ["updated"]) 119 XCTAssertFalse(store.isLoadingNextPage) 120 await oldPage.resume.open() 121 await pagination.value 122 XCTAssertEqual(store.cards.map(\.id), ["updated"]) 123 await store.loadNextPage() 124 XCTAssertEqual(store.cards.map(\.id), ["updated", "new-next"]) 125 _ = try await client.stop() 126 } 127 128 func testScopeChangeDuringRefreshClearsCacheAndRejectsLateSuccess() async throws { 129 let backend = try TeraScopeBackend() 130 let (client, store) = try await makeStore(backend) 131 let refresh = await backend.pause(.refresh) 132 let old = Task { await store.reload() } 133 await refresh.entered.wait() 134 XCTAssertEqual(store.cards.map(\.id), ["wss://first.example"]) 135 store.configure(snapshot: TeraScopeFixtures.snapshot(relay: "second")) 136 XCTAssertTrue(store.cards.isEmpty) 137 XCTAssertEqual(store.presentation.content, .notLoaded) 138 await store.reload(refreshProjection: false) 139 let counts = await backend.counts 140 await refresh.resume.open() 141 await old.value 142 XCTAssertEqual(store.cards.map(\.id), ["wss://second.example"]) 143 XCTAssertEqual(store.presentation.freshness, .unconfirmed) 144 let current = await backend.counts 145 XCTAssertEqual(current[.page], counts[.page]) 146 _ = try await client.stop() 147 } 148 149 private func makeStore(_ backend: TeraScopeBackend) async throws -> (TeraRuntimeClient, TeraTodayStore) { 150 let client = try await TeraScopeFixtures.client(backend) 151 let store = TeraTodayStore(runtimeClient: client, clock: .fixed(unixSeconds: 1)) 152 store.configure(snapshot: TeraScopeFixtures.snapshot()) 153 return (client, store) 154 } 155 156 private func page(_ cards: [TeraTodayCard], next: String? = nil) -> TeraTodayPage { 157 TeraTodayPage(asOfUnixSeconds: 1, items: cards, nextCursor: next, calendar: TeraScopeFixtures.viewerCalendar(asOf: 1)) 158 } 159 }