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:
1 parent
542dd925d0
commit
463f45cacb
3 files changed
+51
-6
No files matched your search
@@ -12,7 +12,7 @@
|
|||||||
"name": "pdat-swift-practice",
|
"name": "pdat-swift-practice",
|
||||||
"source": "./pdat-swift-practice",
|
"source": "./pdat-swift-practice",
|
||||||
"description": "Curated Apple platform findings with sources, plus practices learned from production incidents.",
|
"description": "Curated Apple platform findings with sources, plus practices learned from production incidents.",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "PDAT s.r.o."
|
"name": "PDAT s.r.o."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pdat-swift-practice",
|
"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.",
|
"description": "Curated Apple platform findings with sources, plus practices learned from production incidents.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "PDAT s.r.o.",
|
"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.
|
note, because the next person believes it.
|
||||||
|
|
||||||
Corollary for UI tests: when one goes red without a code change, suspect
|
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,
|
what the test depends on but does not control. In our case it was exactly
|
||||||
`@AppStorage` surviving between launches, a simulator location and the
|
that, and the two causes are worth naming.
|
||||||
"nearby" sort order it drives, or an element scrolled out of a lazy
|
|
||||||
container and therefore missing from the accessibility tree entirely.
|
## 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
|
## Core Data in tests
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user