A small Binderdex bug that turned out to be a good lesson in testing seams: after dismissing a nudge on the home screen, the next card in the deck would sometimes not render at all. The counter advanced, the space was reserved, but the card itself was blank. Only on a real device.
Mocked tests, broken device
Every component test passed. Of course they did: the tests mock the animation driver, so the sequence was logically correct everywhere except the one place it ran, the native animation graph. Re-mounting the wrapper did nothing. The culprit: the deck reused one Animated.Value across cards, so the successor card inherited the exit animation state of the card before it.
The fix is one line of concept and a lesson in identity:
// before: one shared progress value for the whole deck
let exitProgress = new Animated.Value(0);
// after: a fresh native value per promoted card, keyed and owned
const progressFor = (key: string) => getOrCreate(progressMap, key);
A fresh value per promoted card, with cleanup that stops only the value it owns. The successor card stops inheriting the outgoing card's animation graph.
The takeaway I keep relearning: when mocked tests pass but the device disagrees, the seam between them is your bug's home address. It took a native replay to catch it, so I added a test that asserts the two dismissals use two different Animated.Value instances, so this cannot quietly come back.