Record being wrong about which change broke a test

A UI test went red in the same run that adopted three new APIs. The one
touching that screen was reverted; the test still failed. A modifier
order was corrected; still failed. Simulator data was wiped; still
failed. Running it against a clean checkout — which should have been the
first move, not the fourth — it failed there too, with none of the work
present.

The scrolling entry had already been rewritten to explain the fault in
confident detail. That write-up was fiction, so it now says what actually
happened: the API was convicted on circumstance and remains unproven
here, and the one claim that survives is about its two-way binding
costing invalidations.

xcode-practice gets the general lesson, since it will outlive this
particular test: stash and rerun before reasoning about which edit did
it, and a note recording a false cause is worse than no note, because
the next person believes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
TomUviandClaude Opus 5 committed 2026-08-17 01:04:24 +02:00
1 parent f5a3d932fa
commit 542dd925d0
4 files changed
+46 -5

No files matched your search

+1 -1
View File
@@ -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.2.0", "version": "0.3.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.2.0", "version": "0.3.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.",
@@ -25,9 +25,29 @@ following the offset while the user drags.
to reach `proxy.scrollTo` from a `.task`. In our card list that is the to reach `proxy.scrollTo` from a `.task`. In our card list that is the
filter chip strip, which scrolls the active chip into view. filter chip strip, which scrolls the active chip into view.
**Step forward?** Modest but real. One layer of nesting disappears and the **Step forward? Unproven — we backed it out.** Swapping `ScrollViewReader`
scroll position stops being write-only. Not urgent — the existing code for `.scrollPosition(id:)` coincided with the UI test for that strip
works — so worth doing when that view is touched for another reason. failing, so it looked like the culprit and was reverted. It was not:
the same test fails on a completely clean tree, with none of the change
present. Something in the environment broke that test, and the swap was
convicted on circumstance.
The revert stands anyway, because it was never worth much here — this
strip only ever *writes* a scroll target, which is exactly what the proxy
already does. But the reasoning above is worth keeping, because being
wrong about it taught the real lesson: **when a change and a failure
arrive together, check the failure against an unchanged tree before
concluding.** Two rounds of edits went into explaining a fault that was
not there.
One thing that does hold, independent of all that:
- **`scrollPosition` is two-way, so it costs invalidations.** It writes to
its binding on every scroll, where the proxy only takes commands. It
therefore belongs in a small view. Ours would have sat in a screen whose
body derives a sorted, grouped card list — every drag of the strip would
have recomputed all of it. Extracting the strip into its own view was
worth keeping regardless of which scrolling API ends up inside it.
**Available since.** iOS 17, so nothing blocks it at our deployment **Available since.** iOS 17, so nothing blocks it at our deployment
target. `onScrollGeometryChange` needs iOS 18. target. `onScrollGeometryChange` needs iOS 18.
@@ -136,6 +136,27 @@ dead when it is passing. Also: do not pipe a test run through `tail` or a
narrow `grep` and then diagnose from what survived — filter the saved log narrow `grep` and then diagnose from what survived — filter the saved log
afterwards instead. afterwards instead.
## Check a failure against an unchanged tree before blaming the change
A UI test started failing in the same run that introduced three API
changes. The obvious suspect — the one touching that exact screen — was
reverted, and the test still failed; a modifier order was corrected, still
failed; the simulator's app data was wiped, still failed. Only then was
the test run against a clean checkout with none of the work present, and
it failed there too. The change had been innocent all along, and two
rounds of edits went into explaining a fault that did not exist.
`git stash` and one run is cheaper than any amount of reasoning about
which edit did it. Do it first, not fourth. The same goes for what gets
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.
## Core Data in tests ## Core Data in tests
Build objects through the context's own model rather than Build objects through the context's own model rather than