Skip to content

iOS: XCUI test suite with a fixture/mock test mode - #25

Merged
roznet merged 4 commits into
mainfrom
feat/ios-ui-tests
Sep 21, 2026
Merged

roznet merged 4 commits into
mainfrom
feat/ios-ui-tests

Conversation

@roznet

@roznet roznet commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Closes #24

Adds the app's first UI tests, and its first iOS CI, following flyfun-weather's flyfun-weatherUITests pattern.

Test mode (DEBUG only; Services/UITestMode.swift)

  • FLYFUN_UITEST=1
    • Signs the app in with an in-memory token store, so the keychain is never touched.
    • Uses an in-memory, non-CloudKit ModelContainer seeded by UITestSupport/UITestFixtures.swift, so tests never write into the simulator's iCloud account.
    • Uses fixed airport timezones instead of reverse-geocoding them.
  • FLYFUN_MOCK=1
    • UITestURLProtocol answers every request.
    • Everything already goes through URLSession.shared (RollingBearerSession included), so FormService needed no refactor, and the real decoding and 422 parsing stay under test.
    • The airport-detail fixtures are real server output, generated from flightforms.api.airports.get_airport.
    • An unstubbed request fails, is logged, and fails the journey in tearDown.
  • FLYFUN_UITEST_CAPTURE_DIR: the stub writes each request body there, so journeys assert on the request the app generated. The server's output stays covered by the Python snapshot tests.
  • FLYFUN_UITEST_CLIPBOARD: replaces only the pasteboard read, which avoids the "Allow Paste" prompt. Parsing and apply are the real code.
  • FLYFUN_MOCK_GENERATE=422: makes /generate answer with a validation error.
  • About 30 accessibility identifiers added across nine views.
  • Small behaviour change: a travel document's expiry state is now spoken (accessibilityValue), not shown by colour alone.

Journeys (iPhone; macOS out of scope)

  1. Launch shows the seeded people, aircraft and flights; past flights collapse; an expired passport says so.
  2. New flight: route, then the "Same crew as…" suggestion, then create.
  3. Pasting a flight plan fills the route and creates the unknown aircraft.
  4. Setting a Paris departure to 00:xx local moves both the list's day and its UTC time. This works under either DST offset.
  5. Share on LFRM's form sends the right flight, aircraft and people, including the French passport for a Schengen airport, and the share sheet opens.
  6. A 422 shows "Crew 1 — ID Number".
  7. Add a person and a passport; the new passport's row shows its expiry state.
  8. Return flight, next leg and duplicate open with the right routes and land in the list.

CI

  • ios.yml is a PR gate that runs the unit target. It still builds the UI target, so a journey that stops compiling fails the PR.
  • ios-ui-nightly.yml runs the journeys nightly, with a retry and a zero-tests-ran guard, and uploads the .xcresult. It is not a gate.

Verified locally

iPhone 17 simulator, Xcode 27: 103/103 tests pass (95 unit + 8 UI), with counts read from the result bundle.

Not yet verified: neither workflow has run on GitHub. They pin Xcode 26.6 like flyfun-weather's, so the first PR run will show whether the runner image works for this project. Run the nightly with gh workflow run ios-ui-nightly.yml --ref feat/ios-ui-tests if you want it before merging.

🤖 Generated with Claude Code

@claude

claude Bot commented Sep 21, 2026

Copy link
Copy Markdown

Reviewed the diff (CI workflows, UITestMode/UITestURLProtocol/UITestFixtures, accessibility identifiers, designs/ios-app.md). Overall this is clean, well-scoped, and the design doc update matches what's implemented (in-memory/non-CloudKit container, mocked URLSession.shared, DEBUG-gated switches). One thing worth checking before merge:

.github/workflows/ios-ui-nightly.yml: -test-iterations 2 combined with -retry-tests-on-failure

The "UI tests" step passes both -retry-tests-on-failure and -test-iterations 2 to xcodebuild test in the same invocation. These are two different repeat-test mechanisms (unconditional N-times repetition vs. retry-only-on-failure), and xcodebuild has historically rejected them together as mutually exclusive options rather than composing them. If that's still true on Xcode 26.6, this step would fail immediately with an argument error before any journey runs, and the subsequent xcresulttool guard step would then fail too since ui.xcresult was never produced.

Since the PR description already flags that neither workflow has run on GitHub yet, it'd be worth doing the suggested gh workflow run ios-ui-nightly.yml --ref feat/ios-ui-tests dry run before merging to confirm this combination is accepted — if not, dropping -test-iterations 2 (or -retry-tests-on-failure) resolves it. Not a blocker since this workflow doesn't gate PRs, but it'd otherwise silently never produce a passing nightly run.

No other issues found — the mock/fixture layering, capture-dir mechanism, and accessibility-identifier additions all look correct and consistent with the stated design.

roznet and others added 3 commits September 21, 2026 21:21
DEBUG-only launch switches (FLYFUN_UITEST, FLYFUN_MOCK and friends) put the
app on an in-memory, non-CloudKit store seeded with fixtures, sign it in
without the keychain, fix airport timezones, and answer every request from a
URLProtocol stub that records what the app sent. Views gain the accessibility
identifiers the journeys select by, and a document's expiry state is now
spoken, not carried by colour alone.

Part of #24.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the flyfun-formsUITests target and a shared scheme, with journeys for
launch, new flight, flight-plan paste, a schedule edit across UTC midnight,
form generation (asserting the request sent), validation errors, adding a
person with a passport, and return / next leg / duplicate.

CI gates PRs on the unit target and runs the journeys nightly, as
flyfun-weather does.

Closes #24

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The shared scheme now lists flyfun-formsUITests, which is iOS-only. An
unfiltered `xcodebuild test -destination platform=macOS` builds it anyway and
fails launching its runner, so the macOS pre-flight runs the unit target only.
iOS keeps the whole scheme — the one place a release is sure to have passed
the journeys — with a timeout that fits them, and both read the count from the
result bundle instead of trusting TEST SUCCEEDED.

Part of #24.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…green

Part of #24.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@roznet
roznet merged commit ae9b4fe into main Sep 21, 2026
3 checks passed
@claude

claude Bot commented Sep 21, 2026

Copy link
Copy Markdown

Code Review

Reviewed the full diff (test-mode plumbing, new Xcode UI-test target/scheme, CI workflows, XCUI journeys, and the designs/ios-app.md update).

No high-confidence bugs, CLAUDE.md violations (none exists in this repo), or design-doc deviations found. Specifically checked and confirmed correct:

  • All DEBUG-only switches (UITestMode.isActive / .isMocked / .clipboard / .generateStatus) are gated so a Release build always reads them as off, and production code paths (AppState, AirportTimezoneCache, ClipboardFlightPlan) only branch on them, with no non-test behavior change.
  • AppState.tokenStore widening from KeychainBearerTokenStore to any BearerTokenStore doesn't break the one other call site (ContentView.swift:293), since it only needs token from the protocol.
  • Every accessibility identifier referenced by the new XCUI journeys (DepartureHourPicker, flightSectionPill_*, importButton, personRow-*, etc.) resolves to a real, matching identifier in the view code, including the pre-existing FlightDateTimeField.
  • rowCount's use of matching(identifier:) (exact match) against "flightRow-EGTF-" and similar works as intended because the destination is empty (????) for both rows it's meant to count — not a prefix-match bug.
  • No URLSession(...) instances outside .shared, matching the PR's claim that UITestURLProtocol sees all traffic.
  • ios.yml / ios-ui-nightly.yml gating logic (vacuous-pass guards, the changed-since-last-green skip, concurrency groups) is internally consistent.
  • New project.pbxproj GUIDs (42F0A1... prefix) don't collide with existing ones.

Nice attention to the DST/midnight-crossing and lazy-Form-scrolling traps called out in the comments — the mitigations (portrait-pinned orientation, section-pill dragging) match the failure modes described.

No issues to flag. Approving.

@claude

claude Bot commented Sep 21, 2026

Copy link
Copy Markdown

Code Review

Reviewed the diff (CI workflows, UITestMode/UITestFixtures/UITestURLProtocol, accessibility identifiers, AppState token store, and the new XCUI journeys) against designs/ios-app.md. No CLAUDE.md exists in this repo, so I checked against the design doc's stated architecture instead.

No high-confidence issues found. Specifically checked and ruled out:

  • AppState.tokenStore widened from KeychainBearerTokenStore to any BearerTokenStore — only .token is accessed elsewhere (ContentView.swift:293), so no call site depends on the concrete type.
  • All accessibility identifiers referenced by the new flyfun_formsUITests.swift journeys resolve to identifiers actually added to the views (or pre-existing ones like flightSectionNavBar, importButton, peopleSuggestionButton).
  • The flightRow-EGTF- prefix-looking assertion in testReturnNextLegAndDuplicate is actually an exact match — both the next-leg and duplicate flights have an empty destinationICAO, matching the identifier exactly.
  • focusSection(app, "actions") is only called once before three sequential flight-duplication actions; this relies on switchToFlight reusing the same FlightEditView instance (and thus selectedSection state) across the created flights — confirmed by the effectiveSelection doc comment in FlightEditView.swift.
  • UITestMode/UITestFixtures/UITestURLProtocol are all #if DEBUG-gated, consistent with the "never in Release" requirement called out in the archive skill's localhost check.
  • CI workflow split (PR-gated unit tests vs. nightly UI journeys, vacuous-pass guards reading xcresulttool counts) matches the documented rationale and flyfun-weather's precedent.

Nice attention to detail in the fixtures/mocking design (in-memory non-CloudKit store, stubbing at the URLProtocol layer rather than behind an app-level abstraction, capturing outgoing request bodies for assertions).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: add an XCUI test suite with a fixture/mock test mode

1 participant