@@ -249,3 +249,56 @@ struct GraphicsTests {
249249 #expect( node. props [ " vertical " ] == . string( " bottom " ) )
250250 }
251251}
252+
253+ // MARK: - Animation
254+
255+ @Suite ( " Animation " )
256+ struct AnimationTests {
257+
258+ @Test ( " A withAnimation write stamps the next tree's root, once " )
259+ func withAnimationStampsRoot( ) {
260+ struct Screen : View {
261+ @State var on = false
262+ var body : some View { Text ( " x " ) . opacity ( on ? 1 : 0 ) }
263+ }
264+ let host = ViewHost ( Screen ( ) )
265+ var node = host. evaluate ( )
266+ #expect( node. props [ " animationCurve " ] == nil )
267+ // find the opacity toggle by writing through a registered callback:
268+ // simplest is to mutate via withAnimation around a state write driven
269+ // by re-linking — use onTapGesture to reach the state.
270+ struct Tappable : View {
271+ @State var on = false
272+ var body : some View {
273+ Text ( " x " ) . opacity ( on ? 1 : 0 ) . onTapGesture { withAnimation ( . easeIn( duration: 0.2 ) ) { on. toggle ( ) } }
274+ }
275+ }
276+ let tapHost = ViewHost ( Tappable ( ) )
277+ node = tapHost. evaluate ( )
278+ guard case . int( let id) ? = node. modifiers. first ( where: { $0. kind == " onTapGesture " } ) ? . args [ " action " ] else {
279+ Issue . record ( " missing tap id " ) ; return
280+ }
281+ tapHost. callbacks. invokeVoid ( Int64 ( id) )
282+ node = tapHost. evaluate ( )
283+ #expect( node. props [ " animationCurve " ] == . string( " easeIn " ) )
284+ #expect( node. props [ " animationDurationMs " ] == . double( 200 ) )
285+ // a subsequent plain write does not animate
286+ node = tapHost. evaluate ( )
287+ #expect( node. props [ " animationCurve " ] == nil )
288+ }
289+
290+ @Test ( " withAnimation returns its body's value and restores the transaction " )
291+ func withAnimationScoping( ) {
292+ let result = withAnimation ( . linear( duration: 1 ) ) { 41 + 1 }
293+ #expect( result == 42 )
294+ #expect( Transaction . _current == nil )
295+ }
296+
297+ @Test ( " .animation emits curve, duration, and value token " )
298+ func implicitAnimation( ) {
299+ let node = ViewHost ( Text ( " x " ) . animation ( . spring( ) , value: 3 ) ) . evaluate ( )
300+ let anim = node. modifiers. first { $0. kind == " animation " }
301+ #expect( anim? . args [ " curve " ] == . string( " spring " ) )
302+ #expect( anim? . args [ " token " ] == . string( " 3 " ) )
303+ }
304+ }
0 commit comments