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
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
- Heading
## Patterns and pitfalls
- 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
- 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)
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:
Acceptance criteria
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 pitfallssection, placed after## CI setup de-duplication notesand before## VoiceOver smoke steps (manual fallback). ~50 lines.Section structure
## Patterns and pitfallsFailed to terminate ... :0).tap()+waitForExistence("root.currentView.<slug>")+terminateAppReliablytap(_:thenWaitForRoot:in:)is fine vs when it bitestapByStableCenter8s stable-frame check + 12s root wait, 3× retry)d9e69aafailed run 25218186115; shipping fixddf7b69keeps direct.tap().tap()+ explicit wait right after a transitionOut of scope
StillPointAppUITests.swiftor any product codedocs/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 specificallyVerification
coderabbit review --prompt-onlyclean