Write down what actually made a UI test go red

The earlier entry said only "suspect what the test does not control".
Having now found it, the specifics are worth more than the warning.

State leaks between UI tests because @AppStorage is UserDefaults and the
simulator keeps them for days; the fix is a debug hook writing to the
normal domain, because launch arguments land in NSArgumentDomain and
outrank the app, making settings unchangeable rather than merely
predictable. Elements below the fold of a lazy container genuinely do not
exist, which reads exactly like a bug. Both cost a full evening.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
TomUviandClaude Opus 5 committed 2026-08-17 22:23:42 +02:00
1 parent 542dd925d0
commit 463f45cacb
3 files changed
+51 -6

No files matched your search

@@ -1,6 +1,6 @@
{
"name": "pdat-swift-practice",
"version": "0.3.0",
"version": "0.4.0",
"description": "Curated Apple platform findings with sources, plus practices learned from production incidents.",
"author": {
"name": "PDAT s.r.o.",
@@ -152,10 +152,55 @@ written down afterwards: a note recording a false cause is worse than no
note, because the next person believes it.
Corollary for UI tests: when one goes red without a code change, suspect
what the test depends on but does not control — accumulated seed data,
`@AppStorage` surviving between launches, a simulator location and the
"nearby" sort order it drives, or an element scrolled out of a lazy
container and therefore missing from the accessibility tree entirely.
what the test depends on but does not control. In our case it was exactly
that, and the two causes are worth naming.
## UI tests inherit whatever the last test left behind
`@AppStorage` is UserDefaults, and the simulator keeps those between
tests, between runs, and between days. A test that switched the card list
to its List layout left it that way; a different test, written for the
grid, then failed — days later, with nobody having touched the code.
Reset the state at launch. Do it with a **debug hook that writes to the
normal domain**, not with launch arguments:
```swift
// in the app, under #if DEBUG
if ProcessInfo.processInfo.arguments.contains("-resetUserState") {
UserDefaults.standard.removeObject(forKey: "cardTypeFilter")
UserDefaults.standard.set("cards", forKey: "cardLayoutStyle")
}
```
Launch arguments look like the obvious answer and are a trap: they form
`NSArgumentDomain`, which outranks anything the app writes at runtime. The
setting can then be forced but never changed, so every test that toggles
that setting fails. Trying it turned one failing test into five.
While resetting, prefer values that don't depend on the environment. A
"sort by distance" default makes card order a function of the simulator's
location.
## An element scrolled out of a lazy container does not exist
`List`, `LazyVStack` and `LazyVGrid` only build what is visible, so
`element.exists` is `false` for a row further down — indistinguishable
from a genuine bug. Anything below the fold needs a helper that swipes
until the element appears:
```swift
func scrollToStaticText(_ label: String, maxSwipes: Int = 8) -> XCUIElement {
let element = staticTexts[label]
if element.waitForExistence(timeout: 5) { return element }
for _ in 0..<maxSwipes where !element.exists { swipeUp() }
return element
}
```
Watch for content whose position depends on grouping: ours groups cards by
type with user-defined types last, which put exactly those cards off
screen.
## Core Data in tests