field_ios

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

TeraEditingReplacementTests.swift (9125B)


      1 import Foundation
      2 @testable import TeraApp
      3 import XCTest
      4 
      5 @MainActor
      6 final class TeraEditingReplacementTests: XCTestCase {
      7   func testCleanTypeChangeIsImmediateAndDirtyTypeChangePreservesExactAcknowledgment() async throws {
      8     let fixture = try await started()
      9     let backend = fixture.backend
     10     let client = fixture.client
     11     let store = fixture.store
     12     store.selectType(.createEvent)
     13     XCTAssertEqual(store.form.commandType, .createEvent)
     14     store.updateForm(\.eventStartDate, "2026-09-")
     15     store.updateForm(\.content, "  incomplete event  ")
     16     await store.save()
     17     let original = try XCTUnwrap(store.savedComposer)
     18     let pause = await backend.pause(.composer)
     19     store.updateForm(\.content, "  newer event  ")
     20     let editing = TeraComposerForm(editing: store.form)
     21     store.selectType(.createAsk)
     22     await pause.entered.wait()
     23     XCTAssertEqual(TeraComposerForm(editing: store.form), editing)
     24     store.selectType(.createFoodAvailability)
     25     await pause.resume.open()
     26     await TeraScopeFixtures.eventually { !store.protection.isWorking }
     27     XCTAssertEqual(store.form.commandType, .createAsk)
     28     XCTAssertEqual(store.form.content, "")
     29     let saved = try await client.loadComposer(scope: original.scope, id: original.id)
     30     XCTAssertEqual(saved.form, editing)
     31     XCTAssertEqual(saved.revision, original.revision + 1)
     32     XCTAssertGreaterThan(saved.editSequence, original.editSequence)
     33     await store.save()
     34     XCTAssertNotEqual(store.savedComposer?.id, original.id)
     35     store.stop()
     36     _ = try await client.stop()
     37   }
     38 
     39   func testNewerEditingWhileNewSaveCompletesCancelsReplacementAndKeepsNewestForm() async throws {
     40     let fixture = try await started()
     41     let backend = fixture.backend
     42     let client = fixture.client
     43     let store = fixture.store
     44     let first = await backend.pause(.composer)
     45     store.updateForm(\.content, "first")
     46     store.newDraft(type: .createEvent)
     47     await first.entered.wait()
     48     let second = await backend.pause(.composer)
     49     store.updateForm(\.content, "newer while saving")
     50     await first.resume.open()
     51     await second.entered.wait()
     52     XCTAssertEqual(store.form.content, "newer while saving")
     53     XCTAssertEqual(store.form.commandType, .createUpdate)
     54     await second.resume.open()
     55     await TeraScopeFixtures.eventually { !store.isWorking }
     56     XCTAssertEqual(store.form.content, "newer while saving")
     57     XCTAssertEqual(store.savedComposer?.form.content, "newer while saving")
     58     XCTAssertFalse(store.protection.failed)
     59     store.stop()
     60     _ = try await client.stop()
     61   }
     62 
     63   func testSaveFailureKeepEditingAndDiscardPreserveLastDurableDraft() async throws {
     64     let fixture = try await started()
     65     let backend = fixture.backend
     66     let client = fixture.client
     67     let store = fixture.store
     68     store.updateForm(\.content, "durable")
     69     await store.save()
     70     let original = try XCTUnwrap(store.savedComposer)
     71     let failure = await backend.pause(.composer, fails: true)
     72     store.updateForm(\.content, "unsaved")
     73     store.newDraft()
     74     await failure.entered.wait()
     75     await failure.resume.open()
     76     await TeraScopeFixtures.eventually { store.protection.failed }
     77     XCTAssertEqual(store.form.content, "unsaved")
     78     store.protection.cancel()
     79     store.protection.discard()
     80     XCTAssertEqual(store.form.content, "unsaved")
     81     let again = await backend.pause(.composer, fails: true)
     82     store.newDraft(type: .createEvent)
     83     await again.entered.wait()
     84     await again.resume.open()
     85     await TeraScopeFixtures.eventually { store.protection.failed }
     86     store.protection.discard()
     87     await TeraScopeFixtures.eventually { !store.protection.isWorking }
     88     XCTAssertEqual(store.form.commandType, .createEvent)
     89     XCTAssertEqual(store.form.content, "")
     90     let saved = try await client.loadComposer(scope: original.scope, id: original.id)
     91     XCTAssertEqual(saved, original)
     92     store.stop()
     93     _ = try await client.stop()
     94   }
     95 
     96   func testSavedSelectionRetryPreservesBufferBeforeLoadingAndSignalsSheetDismissal() async throws {
     97     let fixture = try await started()
     98     let backend = fixture.backend
     99     let client = fixture.client
    100     let store = fixture.store
    101     store.updateForm(\.content, "buffer")
    102     await store.save()
    103     let original = try XCTUnwrap(store.savedComposer)
    104     let failure = await backend.pause(.composer, fails: true)
    105     store.updateForm(\.content, "latest buffer")
    106     let selection = Task { await store.reopenSaved(.legacy("draft")) }
    107     await failure.entered.wait()
    108     await failure.resume.open()
    109     let applied = await selection.value
    110     XCTAssertFalse(applied)
    111     XCTAssertEqual(store.form.content, "latest buffer")
    112     XCTAssertNil(store.protection.reopened)
    113     let counts = await backend.counts
    114     XCTAssertNil(counts[.draftStatus])
    115     store.protection.retry()
    116     await TeraScopeFixtures.eventually { !store.protection.isWorking }
    117     XCTAssertEqual(store.form.content, "old")
    118     XCTAssertNotNil(store.protection.reopened)
    119     let kept = try await client.loadComposer(scope: original.scope, id: original.id)
    120     XCTAssertEqual(kept.form.content, "latest buffer")
    121     await store.save()
    122     XCTAssertNotEqual(store.savedComposer?.id, original.id)
    123     store.stop()
    124     _ = try await client.stop()
    125   }
    126 
    127   func testScopeChangeCancelsPendingReplacementAndLateSaveCannotReplaceNewAccount() async throws {
    128     let fixture = try await started()
    129     let backend = fixture.backend
    130     let client = fixture.client
    131     let store = fixture.store
    132     let pause = await backend.pause(.composer)
    133     store.updateForm(\.content, "old account")
    134     store.newDraft(type: .createEvent)
    135     await pause.entered.wait()
    136     let snapshot = TeraScopeFixtures.snapshot(account: "b")
    137     await backend.configure(snapshot)
    138     store.configure(snapshot: snapshot)
    139     await store.start()
    140     store.updateForm(\.content, "current account")
    141     await pause.resume.open()
    142     await store.save()
    143     XCTAssertEqual(store.form.content, "current account")
    144     XCTAssertEqual(store.form.commandType, .createUpdate)
    145     XCTAssertEqual(store.savedComposer?.scope.authorPublicKey, String(repeating: "b", count: 64))
    146     XCTAssertFalse(store.protection.failed)
    147     store.stop()
    148     _ = try await client.stop()
    149   }
    150 
    151   func testLegacyRetryPreservesPriorComposerIdentityAndFailedSelectionHasNoEffects() async throws {
    152     let fixture = try await started()
    153     let backend = fixture.backend
    154     let client = fixture.client
    155     let store = fixture.store
    156     store.updateForm(\.content, "first buffer")
    157     await store.save()
    158     let original = try XCTUnwrap(store.savedComposer)
    159     store.updateForm(\.content, "last buffer")
    160     await store.retry(id: "draft")
    161     XCTAssertEqual(store.form.content, "old")
    162     await store.save()
    163     XCTAssertNotEqual(store.savedComposer?.id, original.id)
    164     let kept = try await client.loadComposer(scope: original.scope, id: original.id)
    165     XCTAssertEqual(kept.form.content, "last buffer")
    166     let failure = await backend.pause(.composer, fails: true)
    167     store.updateForm(\.content, "keep for cancellation")
    168     let cancellation = Task { await store.cancel(id: "draft") }
    169     await failure.entered.wait()
    170     await failure.resume.open()
    171     await cancellation.value
    172     XCTAssertEqual(store.form.content, "keep for cancellation")
    173     let counts = await backend.counts
    174     XCTAssertEqual(counts[.draftStatus], 1)
    175     XCTAssertTrue(store.protection.failed)
    176     store.stop()
    177     _ = try await client.stop()
    178   }
    179 
    180   private struct Fixture {
    181     let backend: TeraScopeBackend
    182     let client: TeraRuntimeClient
    183     let store: TeraAddStore
    184   }
    185 
    186   func testCancelledSavedSelectionCannotApplyItsLateLoadedForm() async throws {
    187     let fixture = try await started()
    188     let store = fixture.store
    189     store.updateForm(\.content, "keep current")
    190     await store.save()
    191     let original = try XCTUnwrap(store.savedComposer)
    192     var form = TeraComposerForm(commandType: .createAsk)
    193     form.content = "other saved editing"
    194     let other = try await fixture.client.saveComposer(request: TeraComposerSaveRequest(
    195       scope: original.scope, id: String(repeating: "7", count: 32), expectedRevision: nil, editSequence: 1, form: form
    196     ))
    197     let pause = await fixture.backend.pause(.composerLoad)
    198     let selection = Task { await store.reopenSaved(.composer(other.draft.id)) }
    199     await pause.entered.wait()
    200     selection.cancel()
    201     await TeraScopeFixtures.eventually { !store.protection.isWorking }
    202     await pause.resume.open()
    203     let applied = await selection.value
    204     XCTAssertFalse(applied)
    205     XCTAssertEqual(store.form.content, "keep current")
    206     XCTAssertEqual(store.savedComposer, original)
    207     XCTAssertNil(store.protection.reopened)
    208     XCTAssertFalse(store.protection.failed)
    209     store.stop()
    210     _ = try await fixture.client.stop()
    211   }
    212 
    213   private func started() async throws -> Fixture {
    214     let backend = try TeraScopeBackend()
    215     let client = try await TeraScopeFixtures.client(backend)
    216     let store = TeraAddStore(runtimeClient: client)
    217     store.configure(snapshot: TeraScopeFixtures.snapshot())
    218     await store.start()
    219     return Fixture(backend: backend, client: client, store: store)
    220   }
    221 }