field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }