field_ios

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

TeraProductStartupTests.swift (6976B)


      1 @testable import TeraApp
      2 import XCTest
      3 
      4 @MainActor
      5 final class TeraProductStartupTests: XCTestCase {
      6   func testLocalAddIsEditableWhileTodayRefreshHasNotReturned() async throws {
      7     let fixture = try await StartupFixture()
      8     let refresh = await fixture.backend.pause(.refresh)
      9     fixture.stores.start()
     10     await refresh.entered.wait()
     11     await TeraScopeFixtures.eventually { fixture.stores.add.isProductReady }
     12     XCTAssertEqual(Set(fixture.stores.add.schemas.map(\.commandType)), Set(TeraAddCommandType.allCases))
     13     await TeraScopeFixtures.eventually { fixture.stores.add.drafts.first?.form?.content == "old" }
     14     XCTAssertEqual(fixture.stores.add.drafts.first?.form?.content, "old")
     15     XCTAssertEqual(fixture.stores.today.presentation.refresh, .refreshing)
     16     fixture.stores.add.updateForm(\.content, "Compose while refresh is held")
     17     XCTAssertTrue(fixture.stores.add.canSave)
     18     XCTAssertEqual(fixture.stores.add.form.content, "Compose while refresh is held")
     19     fixture.stores.suspend()
     20     await refresh.resume.open()
     21     try await fixture.close()
     22   }
     23 
     24   func testSuspendCancelsBothStartupReadsBeforeTheirBackendsReturn() async throws {
     25     let fixture = try await StartupFixture()
     26     let page = await fixture.backend.pause(.page)
     27     let drafts = await fixture.backend.pause(.drafts)
     28     let task = Task { await fixture.stores.resume() }
     29     await page.entered.wait()
     30     await drafts.entered.wait()
     31     let validated = fixture.stores.add.schemas
     32     XCTAssertEqual(validated.count, 5)
     33     fixture.stores.suspend()
     34     await task.value
     35     XCTAssertTrue(fixture.stores.today.cards.isEmpty)
     36     XCTAssertEqual(fixture.stores.add.schemas, validated)
     37     XCTAssertTrue(fixture.stores.add.drafts.isEmpty)
     38     XCTAssertTrue(fixture.stores.add.isProductReady)
     39     XCTAssertEqual(fixture.stores.today.observationState, .stopped)
     40     XCTAssertEqual(fixture.stores.add.observationState, .stopped)
     41     await page.resume.open()
     42     await drafts.resume.open()
     43     try await fixture.close()
     44     XCTAssertEqual(fixture.stores.add.schemas, validated)
     45     XCTAssertTrue(fixture.stores.add.drafts.isEmpty)
     46     XCTAssertTrue(fixture.stores.today.cards.isEmpty)
     47   }
     48 
     49   func testCanceledResumeStopsBothObserversAndCanStartAgain() async throws {
     50     let fixture = try await StartupFixture()
     51     let refresh = await fixture.backend.pause(.refresh)
     52     let drafts = await fixture.backend.pause(.drafts)
     53     let task = Task { await fixture.stores.resume() }
     54     await refresh.entered.wait()
     55     await drafts.entered.wait()
     56     task.cancel()
     57     await task.value
     58     XCTAssertEqual(fixture.stores.today.observationState, .stopped)
     59     XCTAssertEqual(fixture.stores.add.observationState, .stopped)
     60     XCTAssertTrue(fixture.stores.add.isProductReady, "Validated local editing remains available while optional reads are cancelled")
     61     await refresh.resume.open()
     62     await drafts.resume.open()
     63     await fixture.releaseObservers()
     64     await fixture.stores.resume()
     65     XCTAssertTrue(fixture.stores.add.isProductReady)
     66     XCTAssertEqual(fixture.stores.add.schemas.count, 5)
     67     try await fixture.close()
     68   }
     69 
     70   func testConcurrentStartsAndResumesKeepOneObserverPerStore() async throws {
     71     let fixture = try await StartupFixture()
     72     let refresh = await fixture.backend.pause(.refresh)
     73     fixture.stores.start()
     74     fixture.stores.start()
     75     let first = Task { await fixture.stores.resume() }
     76     let second = Task { await fixture.stores.resume() }
     77     await refresh.entered.wait()
     78     await TeraScopeFixtures.eventually { fixture.stores.add.isProductReady }
     79     for observer in fixture.observers {
     80       await observer.entered.wait()
     81     }
     82     let counts = await fixture.backend.counts
     83     XCTAssertEqual(counts[.subscribe], 2)
     84     XCTAssertEqual(counts[.refresh], 1)
     85     XCTAssertEqual(counts[.drafts], 1)
     86     await fixture.releaseObservers()
     87     await refresh.resume.open()
     88     await first.value
     89     await second.value
     90     let final = await fixture.backend.counts
     91     XCTAssertEqual(final[.subscribe], 2)
     92     XCTAssertEqual(final[.refresh], 1)
     93     try await fixture.close()
     94   }
     95 
     96   func testReconfigurationRejectsOldStartupAndKeepsTheReplacementOwned() async throws {
     97     for changed in [false, true] {
     98       try await assertReplacement(changed: changed)
     99     }
    100   }
    101 
    102   func testSuspendAndResumePreserveTheUnsentEditingForm() async throws {
    103     let fixture = try await StartupFixture()
    104     await fixture.releaseObservers()
    105     await fixture.stores.resume()
    106     fixture.stores.add.updateForm(\.content, "Unsent local editing")
    107     fixture.stores.suspend()
    108     await fixture.stores.resume()
    109     XCTAssertTrue(fixture.stores.add.isProductReady)
    110     XCTAssertEqual(fixture.stores.add.form.content, "Unsent local editing")
    111     try await fixture.close()
    112   }
    113 
    114   private func assertReplacement(changed: Bool) async throws {
    115     let fixture = try await StartupFixture()
    116     let oldRefresh = await fixture.backend.pause(.refresh)
    117     let oldDrafts = await fixture.backend.pause(.drafts)
    118     let old = Task { await fixture.stores.resume() }
    119     await oldRefresh.entered.wait()
    120     await oldDrafts.entered.wait()
    121     let snapshot = TeraScopeFixtures.snapshot(relay: changed ? "second" : "first")
    122     await fixture.backend.configure(snapshot)
    123     await fixture.backend.setDrafts([TeraScopeFixtures.draft("replacement", revision: 2)])
    124     fixture.stores.configure(snapshot: snapshot)
    125     let newRefresh = await fixture.backend.pause(.refresh)
    126     let current = Task { await fixture.stores.resume() }
    127     await newRefresh.entered.wait()
    128     await old.value
    129     await TeraScopeFixtures.eventually { fixture.stores.add.isProductReady }
    130     await TeraScopeFixtures.eventually { fixture.stores.add.drafts.first?.form?.content == "replacement" }
    131     XCTAssertEqual(fixture.stores.add.drafts.first?.form?.content, "replacement")
    132     XCTAssertEqual(fixture.stores.today.selectedContext?.relayURLs, snapshot.relay?.relays.map(\.url))
    133     fixture.stores.suspend()
    134     await current.value
    135     XCTAssertEqual(fixture.stores.today.observationState, .stopped)
    136     await oldRefresh.resume.open()
    137     await oldDrafts.resume.open()
    138     await newRefresh.resume.open()
    139     try await fixture.close()
    140     XCTAssertEqual(fixture.stores.add.drafts.first?.form?.content, "replacement")
    141   }
    142 }
    143 
    144 @MainActor
    145 private struct StartupFixture {
    146   let backend: TeraScopeBackend
    147   let client: TeraRuntimeClient
    148   let stores: TeraProductStores
    149   let observers: [ResourceTestPause]
    150 
    151   init() async throws {
    152     backend = try TeraScopeBackend()
    153     client = try await TeraScopeFixtures.client(backend)
    154     stores = TeraProductStores(runtimeClient: client)
    155     stores.configure(snapshot: TeraScopeFixtures.snapshot())
    156     observers = await [backend.pause(.subscribe), backend.pause(.subscribe)]
    157   }
    158 
    159   func releaseObservers() async {
    160     for observer in observers {
    161       await observer.resume.open()
    162     }
    163   }
    164 
    165   func close() async throws {
    166     stores.stop()
    167     await releaseObservers()
    168     _ = try await client.stop()
    169   }
    170 }