TeraTodayPagingTests.swift (5140B)
1 @testable import TeraApp 2 import XCTest 3 4 @MainActor 5 final class TeraTodayPagingTests: XCTestCase { 6 func testStalePagePreservesCardsAndIdentityUntilExplicitRefresh() async throws { 7 let backend = try TeraScopeBackend() 8 let (client, store) = try await makeStore(backend) 9 let cards = store.cards 10 let generation = store.scopeGeneration 11 let pause = await backend.pause(.page, failure: stale()) 12 let loading = Task { await store.loadNextPage() } 13 await pause.entered.wait() 14 XCTAssertEqual(store.cards, cards) 15 await pause.resume.open() 16 await loading.value 17 XCTAssertEqual(store.cards, cards) 18 XCTAssertEqual(store.scopeGeneration, generation) 19 XCTAssertEqual(store.presentation.content, .available) 20 XCTAssertEqual(store.presentation.refresh, .completed) 21 XCTAssertTrue(store.presentation.readFailure?.requiresRefresh == true) 22 XCTAssertFalse(store.isLoadingNextPage) 23 XCTAssertFalse(store.canLoadNextPage) 24 let before = await backend.counts 25 for _ in 0 ..< 3 { 26 await store.loadNextPage() 27 } 28 let after = await backend.counts 29 XCTAssertEqual(before, after) 30 await assertExplicitRefresh(backend, store: store, retained: cards) 31 _ = try await client.stop() 32 } 33 34 func testMismatchedAsOfRefusesAppendAndStopsTheOldCursor() async throws { 35 let backend = try TeraScopeBackend() 36 let (client, store) = try await makeStore(backend) 37 await backend.setPage(TeraTodayPage(asOfUnixSeconds: 2, items: [TeraScopeFixtures.card("wrong")], nextCursor: "wrong", calendar: TeraScopeFixtures.viewerCalendar(asOf: 2)), cursor: "next") 38 let cards = store.cards 39 let generation = store.scopeGeneration 40 await store.loadNextPage() 41 XCTAssertEqual(store.cards, cards) 42 XCTAssertEqual(store.scopeGeneration, generation) 43 XCTAssertTrue(store.presentation.readFailure?.requiresRefresh == true) 44 XCTAssertFalse(store.canLoadNextPage) 45 XCTAssertFalse(store.isLoadingNextPage) 46 _ = try await client.stop() 47 } 48 49 func testLateStaleFailureCannotDisableTheNewContextCursor() async throws { 50 let backend = try TeraScopeBackend() 51 let (client, store) = try await makeStore(backend) 52 let pause = await backend.pause(.page, failure: stale()) 53 let old = Task { await store.loadNextPage() } 54 await pause.entered.wait() 55 store.configure(snapshot: TeraScopeFixtures.snapshot(account: "b", relay: "second")) 56 await backend.setPage(page(["new"], next: "new-next")) 57 await store.reload(refreshProjection: false) 58 await pause.resume.open() 59 await old.value 60 XCTAssertEqual(store.cards.map(\.id), ["new"]) 61 XCTAssertTrue(store.canLoadNextPage) 62 XCTAssertNil(store.presentation.readFailure) 63 _ = try await client.stop() 64 } 65 66 func testStalenessUsesTypedRecoveryAndKeepsAccessibleGuidance() { 67 let failure = TeraTodayFailure(TeraRuntimeClientError.today(stale())) 68 XCTAssertTrue(failure.requiresRefresh) 69 XCTAssertEqual(failure.readStatus, TeraUserMessages.text(.todayChanged)) 70 let storage = TeraRuntimeFailure.local(operation: "test", code: "today_storage_failed", safeMessage: "stale cursor") 71 XCTAssertFalse(TeraTodayFailure(storage).requiresRefresh) 72 var presentation = TeraTodayPresentation() 73 presentation.acceptPage(count: 2) 74 presentation.failRead(failure) 75 XCTAssertEqual(presentation.content, .available) 76 XCTAssertTrue(presentation.accessibilityStatus.contains(failure.message)) 77 XCTAssertFalse(presentation.accessibilityStatus.contains("Saved posts could not be read.")) 78 } 79 80 private func assertExplicitRefresh(_ backend: TeraScopeBackend, store: TeraTodayStore, retained: [TeraTodayCard]) async { 81 await backend.setPage(page(["one", "new"], next: "fresh-next")) 82 await backend.setPage(page(["last"]), cursor: "fresh-next") 83 let pause = await backend.pause(.page) 84 let refresh = Task { await store.reload(refreshProjection: false) } 85 await pause.entered.wait() 86 XCTAssertEqual(store.cards, retained) 87 await pause.resume.open() 88 await refresh.value 89 XCTAssertEqual(store.cards.map(\.id), ["one", "new"]) 90 XCTAssertNil(store.presentation.readFailure) 91 XCTAssertTrue(store.canLoadNextPage) 92 await store.loadNextPage() 93 XCTAssertEqual(store.cards.map(\.id), ["one", "new", "last"]) 94 XCTAssertFalse(store.canLoadNextPage) 95 } 96 97 private func makeStore(_ backend: TeraScopeBackend) async throws -> (TeraRuntimeClient, TeraTodayStore) { 98 await backend.setPage(page(["one", "two"], next: "next")) 99 let client = try await TeraScopeFixtures.client(backend) 100 let store = TeraTodayStore(runtimeClient: client, clock: .fixed(unixSeconds: 1)) 101 store.configure(snapshot: TeraScopeFixtures.snapshot()) 102 await store.reload() 103 return (client, store) 104 } 105 106 private func page(_ ids: [String], next: String? = nil) -> TeraTodayPage { 107 TeraTodayPage(asOfUnixSeconds: 1, items: ids.map(TeraScopeFixtures.card), nextCursor: next, calendar: TeraScopeFixtures.viewerCalendar(asOf: 1)) 108 } 109 110 private func stale() -> TeraRuntimeFailure { 111 .local(operation: "test", code: "today_cursor_invalid", safeMessage: "Controlled stale page.") 112 } 113 }