Skip to content

docs(ios-ui-tests): document terminate-after-navigation race + when to use direct tap vs helper #320

@auerbachb

Description

@auerbachb

Following #319 (closes #318) we landed a fix for the iOS smoke-lane flake in `testLaunchLoginCompleteSessionAndHistoryPersistence`. The investigation surfaced a non-obvious pitfall that is worth pinning to repo docs so future test authors do not re-discover it.

The pitfall

When a UI test calls `XCUIApplication.terminate()` shortly after triggering navigation (e.g., immediately after a button `.tap()` that starts a new screen), `launchd` on macos-26/iOS 26 simulators sometimes loses the process and surfaces `Failed to terminate ... :0` after a long hang. Original failing-runs evidence: `25215904325`, `25216409703`, `25216402153`, `25216777439`.

Why the obvious helper isn't always the right call

The natural reach is for the existing `tap(_:thenWaitForRoot:in:)` helper at `ios/StillPointAppUITests/StillPointAppUITests.swift:396`. But that helper composes `tapByStableCenter` (8s stable-frame check) with a 12s root-existence wait, and `tapByStableCenter` returns `false` without tapping if the element's frame doesn't pass the stability check. In one specific flow (post-login → home → Begin) the home button frame failed that check; the helper retried 3× and never registered a tap. Evidence: PR #319 v1 (`d9e69aa`) failed CI on run 25218186115 with `StillPointAppUITests.swift:404: error: ... Expected tap to transition to root.currentView.session`.

The shipping fix on `main` (`ddf7b69`) keeps direct `beginButton.tap()` and adds an explicit `waitForExistence` for the destination root before `terminateAppReliably` — same idle-before-terminate benefit, no change to tap semantics.

What we want documented

Add a "Patterns and pitfalls" section to `ios/StillPointAppUITests/README.md` so future test authors have a clear rule. Concretely:

  • One short entry: "Wait for the destination root before terminating after navigation."
  • Explain the launchd race (terminate during navigation/audio init).
  • Show the "good" pattern (direct `.tap()` + `waitForExistence("root.currentView.")` before `terminateAppReliably`).
  • Note when `tap(_:thenWaitForRoot:in:)` is fine (already-quiescent flows: rotation/VoiceOver tests use it successfully) and when it can bite (post-login transitions where the destination element's frame is briefly unstable).
  • Cite the actual file/line pointers and PR/run IDs so the rationale is auditable.

Acceptance criteria

  • New `## Patterns and pitfalls` section added to `ios/StillPointAppUITests/README.md`
  • Section explains the terminate-after-navigation race in plain English
  • Section gives the canonical "good" pattern with a Swift snippet
  • Section explains when the `tap(_:thenWaitForRoot:in:)` helper is OK vs not
  • Section links to PR fix(ios-e2e): wait for session root before terminate to avoid launchd race #319 and the relevant failing/passing run IDs for traceability
  • No code changes outside `ios/StillPointAppUITests/README.md`

Implementation Plan

Source: Claude's analysis. CodeRabbit plan requested but doc-only changes typically don't need a planner round-trip; will incorporate any CR feedback during PR review.

File

  • ios/StillPointAppUITests/README.md (only)

Insertion

A new ## Patterns and pitfalls section, placed after ## CI setup de-duplication notes and before ## VoiceOver smoke steps (manual fallback). ~50 lines.

Section structure

  1. Heading ## Patterns and pitfalls
  2. Subsection: Wait for the destination root before terminating after navigation
    • Plain-English explanation of the launchd race (failure signature Failed to terminate ... :0)
    • "Do this" Swift snippet using direct .tap() + waitForExistence("root.currentView.<slug>") + terminateAppReliably
    • "Don't do this" counter-example
  3. Subsection: When tap(_:thenWaitForRoot:in:) is fine vs when it bites
    • Brief description of what the helper does (tapByStableCenter 8s stable-frame check + 12s root wait, 3× retry)
    • The trap: stable-frame check returns false silently when source element frame is briefly unstable (post-transition), helper retries 3× without ever tapping
    • Concrete evidence: PR #319 v1 commit d9e69aa failed run 25218186115; shipping fix ddf7b69 keeps direct .tap()
    • Rule of thumb: helper for already-quiescent flows; direct .tap() + explicit wait right after a transition

Out of scope

  • No code changes to StillPointAppUITests.swift or any product code
  • Not adding a lint rule / CI check (separate ticket if desired)
  • Not updating docs/testing/e2e-policy.md — its Section 3 already mandates "XCTest expectations/predicate waits" at the policy level; this README is the how-to for iOS specifically

Verification

  • Local coderabbit review --prompt-only clean
  • Open PR with Test Plan; CodeRabbit + BugBot review; merge after gate met (per repo policy)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions