Skip to content

test(hig): catch the animation modifier in the Reduce Motion gate, and gate the four call sites it was missing - #3020

Merged
datlechin merged 2 commits into
mainfrom
fix/reduce-motion-gate
Sep 20, 2026
Merged

datlechin merged 2 commits into
mainfrom
fix/reduce-motion-gate

Conversation

@datlechin

Copy link
Copy Markdown
Member

Found while investigating #2995. The AI composer's focus crossfade played at full duration under Reduce Motion, and the reason it had never been caught is that ReduceMotionGateTests only ever looked for the literal string withAnimation.

The guard had a blind spot

There are two ways to animate in SwiftUI, and the scan only knew one:

withAnimation { … }                      // scanned since the test was written
.animation(.easeOut(duration: 0.25), …)  // invisible to it

The modifier form is the one a view uses for a state-driven transition, so the gap was not a corner case. modifierOffenders now scans for it, skipping the three shapes that are already correct: .animation(nil, …) is the reduced behaviour already, .motionAnimation( is the gate itself, and CALayer.animation(forKey:) is a lookup rather than a change.

What the widened scan found

Four call sites, now going through motionAnimation / withMotion:

Where What it was
SyncStatusIndicator .animation(.default, value: syncStatus) on every sync state change
AIChatPanelView the assistant's scroll-to-bottom button fading in and out
AIChatMessageView the typing indicator, hand-gated correctly but off the shared helper
ForeignKeyPickerView two ungated withAnimation { … } calls

OpacityPulse in View+SymbolEffectCompat needed more than a gate. Gating its animation alone would have parked the view at the dimmed end of a pulse that never runs, which reads as a disabled control, so Reduce Motion now withdraws the pulse and holds opacity at 1.

main is currently red on this test

The two ForeignKeyPickerView calls came in with #3009, which merged earlier today. They are plain withAnimation { … }, so the existing scan already flags them: ReduceMotionGateTests fails on main right now, before any of my changes. This PR fixes them because it owns that guard, but the failure is not mine and is worth knowing about separately.

Verification

  • build BUILD SUCCEEDED (xcodebuild, Debug, private -derivedDataPath)
  • The widened scan was run over the real tree as a standalone script reimplementing its exact algorithm: 0 offenders across TablePro/, which is what the assertion checks.
  • TableProTests compiled clean (module emitted, zero error: in 43k lines of log).

The test case itself did not get to execute. Three other Claude sessions were building on this machine throughout, the shared DerivedData build database was locked (database is locked. Possibly there are two concurrent builds running in the same filesystem location), and a private DerivedData took free disk from 28 GB to 4.6 GB, at which point I stopped rather than risk an out-of-space failure across the other sessions. The scan result above is the substantive claim and it is verified; what is unconfirmed is only the XCTest run around it. CI will run it.

Worth re-running ReduceMotionGateTests on CI for this branch before merge, since it is the whole point of the change.

@mintlify

mintlify Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 20, 2026, 12:44 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@datlechin
datlechin merged commit e62710a into main Sep 20, 2026
11 of 13 checks passed
@datlechin
datlechin deleted the fix/reduce-motion-gate branch September 20, 2026 15:14

This branch was successfully deployed

1 active deployment
staging - docs — ab9362b3 Deployed Sep 20, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant