field_ios

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

TeraTodayPresentationTests.swift (9113B)


      1 @testable import TeraApp
      2 import XCTest
      3 
      4 @MainActor
      5 final class TeraTodayPresentationTests: XCTestCase {
      6   func testContentAvailabilityIsIndependentOfRunningSuccessfulAndFailedRefresh() async throws {
      7     for cached in [nil, [], [TeraScopeFixtures.card("cached")]] as [[TeraTodayCard]?] {
      8       for fails in [false, true] {
      9         try await assertRefresh(cached: cached, fails: fails)
     10       }
     11     }
     12   }
     13 
     14   func testReadFailureRetainsCachedContentAndDoesNotInventAnEmptyResult() async throws {
     15     for cached in [false, true] {
     16       let backend = try TeraScopeBackend()
     17       let (client, store) = try await makeStore(backend)
     18       if cached {
     19         await store.reload(refreshProjection: false)
     20       }
     21       let expected = store.presentation.content
     22       let firstRead = await backend.pause(.page, fails: true)
     23       let pause = await backend.pause(.page, fails: true)
     24       let task = Task { await store.reload() }
     25       await firstRead.entered.wait()
     26       await firstRead.resume.open()
     27       await pause.entered.wait()
     28       XCTAssertEqual(store.presentation.refresh, .completed)
     29       XCTAssertEqual(store.presentation.content, expected)
     30       XCTAssertTrue(store.presentation.isReading)
     31       XCTAssertEqual(store.presentation.freshness, .unconfirmed)
     32       await pause.resume.open()
     33       await task.value
     34       XCTAssertEqual(store.presentation.content, expected)
     35       XCTAssertNotNil(store.presentation.readFailure)
     36       XCTAssertEqual(store.presentation.refresh, .completed)
     37       XCTAssertEqual(store.presentation.freshness, .unconfirmed)
     38       XCTAssertFalse(store.presentation.isReading)
     39       XCTAssertTrue(store.presentation.accessibilityStatus.contains("Saved posts could not be read."))
     40       XCTAssertFalse(store.presentation.accessibilityStatus.contains("Refresh failed."))
     41       XCTAssertEqual(store.cards.isEmpty, !cached)
     42       _ = try await client.stop()
     43     }
     44   }
     45 
     46   func testCanceledRefreshStopsActivityWithoutDiscardingCachedContent() async throws {
     47     let backend = try TeraScopeBackend()
     48     let (client, store) = try await makeStore(backend)
     49     await store.reload(refreshProjection: false)
     50     let cards = store.cards
     51     let pause = await backend.pause(.refresh)
     52     let task = Task { await store.reload() }
     53     await pause.entered.wait()
     54     task.cancel()
     55     await task.value
     56     XCTAssertEqual(store.cards, cards)
     57     XCTAssertEqual(store.presentation.content, .available)
     58     XCTAssertEqual(store.presentation.refresh, .idle)
     59     XCTAssertEqual(store.presentation.freshness, .unconfirmed)
     60     XCTAssertFalse(store.presentation.isReading)
     61     XCTAssertFalse(store.presentation.accessibilityStatus.contains("Checking for updates."))
     62     await pause.resume.open()
     63     _ = try await client.stop()
     64   }
     65 
     66   func testOldReadCancellationCannotClearTheNewRefreshActivity() async throws {
     67     let backend = try TeraScopeBackend()
     68     let (client, store) = try await makeStore(backend)
     69     await store.reload(refreshProjection: false)
     70     let reading = await backend.pause(.page)
     71     let old = Task { await store.reload(refreshProjection: false) }
     72     await reading.entered.wait()
     73     let refreshing = await backend.pause(.refresh)
     74     let current = Task { await store.reload() }
     75     await refreshing.entered.wait()
     76     old.cancel()
     77     await old.value
     78     XCTAssertEqual(store.presentation.refresh, .refreshing)
     79     XCTAssertEqual(store.presentation.content, .available)
     80     XCTAssertFalse(store.presentation.isReading)
     81     XCTAssertTrue(store.presentation.accessibilityStatus.contains("Checking for updates."))
     82     await reading.resume.open()
     83     await refreshing.resume.open()
     84     await current.value
     85     XCTAssertEqual(store.presentation.refresh, .completed)
     86     XCTAssertEqual(store.presentation.freshness, .refreshed(contentGeneration: 1))
     87     _ = try await client.stop()
     88   }
     89 
     90   func testMissingContextDoesNotClaimAnEmptyReadOrSuccessfulRefresh() async throws {
     91     let backend = try TeraScopeBackend()
     92     let client = try await TeraScopeFixtures.client(backend)
     93     let store = TeraTodayStore(runtimeClient: client)
     94     await store.reload()
     95     XCTAssertEqual(store.presentation.content, .notLoaded)
     96     XCTAssertEqual(store.presentation.refresh, .idle)
     97     XCTAssertEqual(store.presentation.freshness, .unconfirmed)
     98     XCTAssertNotNil(store.presentation.readFailure)
     99     let counts = await backend.counts
    100     XCTAssertNil(counts[.refresh])
    101     XCTAssertNil(counts[.page])
    102     _ = try await client.stop()
    103   }
    104 
    105   func testPaginationReadFailureAndRecoveryPreserveTheRefreshOutcome() async throws {
    106     let backend = try TeraScopeBackend()
    107     await backend.setPage(page([TeraScopeFixtures.card("one")], next: "two"))
    108     await backend.setPage(page([TeraScopeFixtures.card("two")]), cursor: "two")
    109     let (client, store) = try await makeStore(backend)
    110     await store.reload()
    111     let pause = await backend.pause(.page, fails: true)
    112     let task = Task { await store.loadNextPage() }
    113     await pause.entered.wait()
    114     await pause.resume.open()
    115     await task.value
    116     XCTAssertEqual(store.cards.map(\.id), ["one"])
    117     XCTAssertEqual(store.presentation.content, .available)
    118     XCTAssertEqual(store.presentation.refresh, .completed)
    119     XCTAssertNotNil(store.presentation.readFailure)
    120     XCTAssertEqual(store.presentation.freshness, .unconfirmed)
    121     XCTAssertTrue(store.canLoadNextPage)
    122     XCTAssertFalse(store.presentation.readFailure?.requiresRefresh == true)
    123     await store.loadNextPage()
    124     XCTAssertEqual(store.cards.map(\.id), ["one", "two"])
    125     XCTAssertNil(store.presentation.readFailure)
    126     XCTAssertEqual(store.presentation.refresh, .completed)
    127     _ = try await client.stop()
    128   }
    129 
    130   func testFailedRefreshAndFailedLocalReadHaveIndependentAccessibleOutcomes() async throws {
    131     let backend = try TeraScopeBackend()
    132     let (client, store) = try await makeStore(backend)
    133     let refreshing = await backend.pause(.refresh, fails: true)
    134     let reading = await backend.pause(.page, fails: true)
    135     let rereading = await backend.pause(.page, fails: true)
    136     let task = Task { await store.reload() }
    137     await reading.entered.wait()
    138     await reading.resume.open()
    139     await refreshing.entered.wait()
    140     await refreshing.resume.open()
    141     await rereading.entered.wait()
    142     XCTAssertTrue(store.presentation.isReading)
    143     await rereading.resume.open()
    144     await task.value
    145     XCTAssertEqual(store.presentation.content, .notLoaded)
    146     XCTAssertNotNil(store.presentation.readFailure)
    147     XCTAssertTrue(store.presentation.accessibilityStatus.contains("Refresh failed."))
    148     XCTAssertTrue(store.presentation.accessibilityStatus.contains("Saved posts could not be read."))
    149     XCTAssertFalse(store.presentation.accessibilityStatus.contains("Reading saved posts."))
    150     _ = try await client.stop()
    151   }
    152 
    153   private func assertRefresh(cached: [TeraTodayCard]?, fails: Bool) async throws {
    154     let backend = try TeraScopeBackend()
    155     await backend.setPage(page(cached ?? []))
    156     let (client, store) = try await makeStore(backend)
    157     if cached != nil {
    158       await store.reload(refreshProjection: false)
    159     }
    160     let expected: TeraTodayContentAvailability = (cached ?? []).isEmpty ? .empty : .available
    161     let pause = await backend.pause(.refresh, fails: fails)
    162     let task = Task { await store.reload() }
    163     await pause.entered.wait()
    164     XCTAssertEqual(store.presentation.content, expected)
    165     XCTAssertEqual(store.presentation.refresh, .refreshing)
    166     XCTAssertEqual(store.presentation.freshness, .unconfirmed)
    167     XCTAssertFalse(store.presentation.isReading)
    168     XCTAssertTrue(store.presentation.accessibilityStatus.contains("Checking for updates."))
    169     XCTAssertEqual(store.cards, cached ?? [])
    170     await pause.resume.open()
    171     await task.value
    172     XCTAssertEqual(store.presentation.content, (cached ?? []).isEmpty ? .empty : .available)
    173     XCTAssertNil(store.presentation.readFailure)
    174     XCTAssertFalse(store.presentation.isReading)
    175     if fails {
    176       XCTAssertEqual(store.presentation.refresh, .failed(TeraTodayFailure(TeraScopeFixtures.failure())))
    177       XCTAssertEqual(store.presentation.freshness, .unconfirmed)
    178       XCTAssertTrue(store.presentation.accessibilityStatus.contains("Refresh failed."))
    179       XCTAssertTrue(store.presentation.accessibilityStatus.contains("Showing saved posts."))
    180     } else {
    181       XCTAssertEqual(store.presentation.refresh, .completed)
    182       XCTAssertEqual(store.presentation.freshness, .refreshed(contentGeneration: 1))
    183       XCTAssertEqual(store.presentation.accessibilityStatus, "Saved posts checked.")
    184     }
    185     _ = try await client.stop()
    186   }
    187 
    188   private func makeStore(_ backend: TeraScopeBackend) async throws -> (TeraRuntimeClient, TeraTodayStore) {
    189     let client = try await TeraScopeFixtures.client(backend)
    190     let store = TeraTodayStore(runtimeClient: client, clock: .fixed(unixSeconds: 1))
    191     store.configure(snapshot: TeraScopeFixtures.snapshot())
    192     return (client, store)
    193   }
    194 
    195   private func page(_ cards: [TeraTodayCard], next: String? = nil) -> TeraTodayPage {
    196     TeraTodayPage(asOfUnixSeconds: 1, items: cards, nextCursor: next, calendar: TeraScopeFixtures.viewerCalendar(asOf: 1))
    197   }
    198 }