@@ -42,19 +42,29 @@ TEST_F(AnimationDriverTests, framesAnimation) {
4242
4343 const double startTimeInTick = 12345 ;
4444
45+ // Frame 1: deferred start outputs frame 0 and re-anchors timing
4546 runAnimationFrame (startTimeInTick);
4647 EXPECT_EQ (round (nodesManager_->getValue (valueNodeTag).value ()), 0 );
4748
48- runAnimationFrame (startTimeInTick + SingleFrameIntervalMs * 2.5 );
49+ // Frame 2: re-anchor completes, timeDelta=0 → still frame 0
50+ runAnimationFrame (startTimeInTick + SingleFrameIntervalMs);
51+ EXPECT_EQ (round (nodesManager_->getValue (valueNodeTag).value ()), 0 );
52+
53+ // Subsequent frames are measured from the re-anchored start
54+ runAnimationFrame (
55+ startTimeInTick + SingleFrameIntervalMs + SingleFrameIntervalMs * 2.5 );
4956 EXPECT_EQ (round (nodesManager_->getValue (valueNodeTag).value ()), 65 );
5057
51- runAnimationFrame (startTimeInTick + SingleFrameIntervalMs * 3 );
58+ runAnimationFrame (
59+ startTimeInTick + SingleFrameIntervalMs + SingleFrameIntervalMs * 3 );
5260 EXPECT_EQ (round (nodesManager_->getValue (valueNodeTag).value ()), 90 );
5361
54- runAnimationFrame (startTimeInTick + SingleFrameIntervalMs * 4 );
62+ runAnimationFrame (
63+ startTimeInTick + SingleFrameIntervalMs + SingleFrameIntervalMs * 4 );
5564 EXPECT_EQ (round (nodesManager_->getValue (valueNodeTag).value ()), toValue);
5665
57- runAnimationFrame (startTimeInTick + SingleFrameIntervalMs * 10 );
66+ runAnimationFrame (
67+ startTimeInTick + SingleFrameIntervalMs + SingleFrameIntervalMs * 10 );
5868 EXPECT_EQ (round (nodesManager_->getValue (valueNodeTag).value ()), toValue);
5969}
6070
@@ -84,12 +94,14 @@ TEST_F(AnimationDriverTests, framesAnimationReconfigurationClearsFrames) {
8494
8595 const double startTimeInTick = 12345 ;
8696
87- // Run first frame
97+ // Deferred start frame + re-anchor frame
8898 runAnimationFrame (startTimeInTick);
8999 EXPECT_EQ (round (nodesManager_->getValue (valueNodeTag).value ()), 0 );
100+ runAnimationFrame (startTimeInTick + SingleFrameIntervalMs);
101+ EXPECT_EQ (round (nodesManager_->getValue (valueNodeTag).value ()), 0 );
90102
91- // Reconfigure the same animation (same animationId) with new frames
92- // This triggers updateConfig on the existing FrameAnimationDriver
103+ // Reconfigure the same animation (same animationId) with new frames.
104+ // This triggers updateConfig → onConfigChanged → deferredStart_ = true again.
93105 const auto frames2 = folly::dynamic::array (0 .0f , 0 .5f , 1 .0f );
94106 const auto toValue2 = 200 ;
95107 nodesManager_->startAnimatingNode (
@@ -99,22 +111,222 @@ TEST_F(AnimationDriverTests, framesAnimationReconfigurationClearsFrames) {
99111 " toValue" , toValue2),
100112 std::nullopt );
101113
102- // Reset animation timing
103114 const double newStartTimeInTick = 20000 ;
104115
105- // Run animation at halfway point (1 frame into 3-frame animation)
116+ // Deferred start frame for reconfigured animation
106117 runAnimationFrame (newStartTimeInTick);
107- runAnimationFrame (newStartTimeInTick + SingleFrameIntervalMs * 1 );
118+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), 0 );
119+
120+ // Re-anchor frame
121+ runAnimationFrame (newStartTimeInTick + SingleFrameIntervalMs);
122+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), 0 );
108123
109124 // At frame 1 of 3 frames (50% progress), value should be approximately:
110125 // startValue (0) + 0.5 * (toValue2 - startValue) = 0 + 0.5 * 200 = 100
111126 // If frames accumulated (5 + 3 = 8 frames), we'd be at wrong position
112- // Use ceil rounding so 100.00x becomes 100.01
113- EXPECT_EQ ( round ( nodesManager_->getValue (valueNodeTag).value ()) , 100.01 );
127+ runAnimationFrame (newStartTimeInTick + SingleFrameIntervalMs * 2 );
128+ EXPECT_NEAR ( nodesManager_->getValue (valueNodeTag).value (), 100 , 0 .01 );
114129
115130 // Complete the animation
116- runAnimationFrame (newStartTimeInTick + SingleFrameIntervalMs * 2 );
117- EXPECT_EQ (round (nodesManager_->getValue (valueNodeTag).value ()), toValue2);
131+ runAnimationFrame (newStartTimeInTick + SingleFrameIntervalMs * 3 );
132+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), toValue2);
133+ }
134+
135+ TEST_F (AnimationDriverTests, framesAnimationDeferredStartPreventsSkipping) {
136+ // Deferred start outputs frame 0 on the first update and re-anchors
137+ // startFrameTimeMs_ so the second update also sees timeDelta=0.
138+ // Without the fix the second frame would already be at value 25.
139+ initNodesManager ();
140+
141+ auto rootTag = getNextRootViewTag ();
142+
143+ auto valueNodeTag = ++rootTag;
144+ nodesManager_->createAnimatedNode (
145+ valueNodeTag,
146+ folly::dynamic::object (" type" , " value" )(" value" , 0 )(" offset" , 0 ));
147+
148+ const auto animationId = 1 ;
149+ const auto frames = folly::dynamic::array (0 .0f , 0 .25f , 0 .5f , 0 .75f , 1 .0f );
150+ const auto toValue = 100 ;
151+ nodesManager_->startAnimatingNode (
152+ animationId,
153+ valueNodeTag,
154+ folly::dynamic::object (" type" , " frames" )(" frames" , frames)(
155+ " toValue" , toValue),
156+ std::nullopt );
157+
158+ const double t = 12345 ;
159+
160+ // Frame 1: both with and without fix, timeDelta=0 → value=0
161+ runAnimationFrame (t);
162+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), 0 );
163+
164+ // Frame 2: WITHOUT fix timeDelta=SI → value≈25.
165+ // WITH fix the deferred start re-anchored startFrameTimeMs_, so
166+ // timeDelta=0 → value=0. This assertion fails without the fix.
167+ runAnimationFrame (t + SingleFrameIntervalMs);
168+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), 0 );
169+
170+ // Frame 3: now timeDelta=SI from the re-anchored start
171+ runAnimationFrame (t + SingleFrameIntervalMs * 2 );
172+ EXPECT_NEAR (nodesManager_->getValue (valueNodeTag).value (), 25 , 0.01 );
173+
174+ // Frame 4
175+ runAnimationFrame (t + SingleFrameIntervalMs * 3 );
176+ EXPECT_NEAR (nodesManager_->getValue (valueNodeTag).value (), 50 , 0.01 );
177+
178+ // Complete
179+ runAnimationFrame (t + SingleFrameIntervalMs * 5 );
180+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), toValue);
181+ }
182+
183+ TEST_F (
184+ AnimationDriverTests,
185+ framesAnimationDoesNotStartBeforeEndOfEventLoopTick) {
186+ // Validates that animations don't start before the end of the current event
187+ // loop tick, even if the main thread progresses after they've been set up.
188+ //
189+ // Scenario: animation is started, but the UI thread is busy with
190+ // layout/mount work for several frames (~200ms) before the view composites.
191+ // Without deferred start, the animation would skip ahead to the 200ms mark.
192+ // With deferred start, the animation re-anchors and starts cleanly from
193+ // frame 0 regardless of how much wall-clock time elapsed.
194+ initNodesManager ();
195+
196+ auto rootTag = getNextRootViewTag ();
197+
198+ auto valueNodeTag = ++rootTag;
199+ nodesManager_->createAnimatedNode (
200+ valueNodeTag,
201+ folly::dynamic::object (" type" , " value" )(" value" , 0 )(" offset" , 0 ));
202+
203+ const auto animationId = 1 ;
204+ const auto frames = folly::dynamic::array (0 .0f , 0 .25f , 0 .5f , 0 .75f , 1 .0f );
205+ const auto toValue = 100 ;
206+ nodesManager_->startAnimatingNode (
207+ animationId,
208+ valueNodeTag,
209+ folly::dynamic::object (" type" , " frames" )(" frames" , frames)(
210+ " toValue" , toValue),
211+ std::nullopt );
212+
213+ const double setupTime = 10000 ;
214+
215+ // First runAnimationFrame happens right after setup — this is the first
216+ // Choreographer tick that processes the animation. The deferred start
217+ // outputs frame 0 and resets the time anchor.
218+ runAnimationFrame (setupTime);
219+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), 0 );
220+
221+ // Main thread is now busy with heavy layout/mount work for ~200ms
222+ // (about 12 frame intervals). The next Choreographer callback arrives
223+ // much later. Without deferred start, timeDelta would be 200ms and
224+ // the animation would skip to the 200ms mark. With it, re-anchoring
225+ // ensures timeDelta=0.
226+ const double afterBusyPeriod = setupTime + 200 ;
227+ runAnimationFrame (afterBusyPeriod);
228+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), 0 )
229+ << " Animation should not have progressed during the busy period" ;
230+
231+ // From here, animation progresses normally frame by frame
232+ runAnimationFrame (afterBusyPeriod + SingleFrameIntervalMs);
233+ EXPECT_NEAR (nodesManager_->getValue (valueNodeTag).value (), 25 , 0.01 );
234+
235+ runAnimationFrame (afterBusyPeriod + SingleFrameIntervalMs * 2 );
236+ EXPECT_NEAR (nodesManager_->getValue (valueNodeTag).value (), 50 , 0.01 );
237+
238+ runAnimationFrame (afterBusyPeriod + SingleFrameIntervalMs * 3 );
239+ EXPECT_NEAR (nodesManager_->getValue (valueNodeTag).value (), 75 , 0.01 );
240+
241+ runAnimationFrame (afterBusyPeriod + SingleFrameIntervalMs * 4 );
242+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), toValue);
243+ }
244+
245+ TEST_F (
246+ AnimationDriverTests,
247+ framesAnimationDoesNotStartBeforeEndOfEventLoopTick_multipleFrameGap) {
248+ // Same scenario but the first runAnimationFrame itself arrives late
249+ // (500ms after animation was set up) — simulating the case where the
250+ // entire event loop tick is blocked by heavy JS/native work and
251+ // Choreographer catches up later.
252+ initNodesManager ();
253+
254+ auto rootTag = getNextRootViewTag ();
255+
256+ auto valueNodeTag = ++rootTag;
257+ nodesManager_->createAnimatedNode (
258+ valueNodeTag,
259+ folly::dynamic::object (" type" , " value" )(" value" , 0 )(" offset" , 0 ));
260+
261+ const auto animationId = 1 ;
262+ const auto frames = folly::dynamic::array (0 .0f , 0 .25f , 0 .5f , 0 .75f , 1 .0f );
263+ const auto toValue = 100 ;
264+ nodesManager_->startAnimatingNode (
265+ animationId,
266+ valueNodeTag,
267+ folly::dynamic::object (" type" , " frames" )(" frames" , frames)(
268+ " toValue" , toValue),
269+ std::nullopt );
270+
271+ // Simulate: 500ms passes before the first animation frame runs.
272+ // The animation must still start at value 0 — it should never
273+ // retroactively apply elapsed time.
274+ const double lateFirstFrame = 50000 ;
275+ runAnimationFrame (lateFirstFrame);
276+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), 0 )
277+ << " Animation must not skip ahead even if first frame arrives late" ;
278+
279+ // Second frame shortly after — re-anchor means timeDelta=0 still
280+ runAnimationFrame (lateFirstFrame + SingleFrameIntervalMs);
281+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), 0 )
282+ << " Re-anchored start means second frame also sees timeDelta=0" ;
283+
284+ // Normal progression from the re-anchored point
285+ runAnimationFrame (lateFirstFrame + SingleFrameIntervalMs * 2 );
286+ EXPECT_NEAR (nodesManager_->getValue (valueNodeTag).value (), 25 , 0.01 );
287+
288+ runAnimationFrame (lateFirstFrame + SingleFrameIntervalMs * 4 );
289+ EXPECT_NEAR (nodesManager_->getValue (valueNodeTag).value (), 75 , 0.01 );
290+
291+ runAnimationFrame (lateFirstFrame + SingleFrameIntervalMs * 5 );
292+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), toValue);
293+ }
294+
295+ TEST_F (AnimationDriverTests, framesAnimationDeferredStartOptOut) {
296+ // When deferredStart is false, the animation starts immediately without
297+ // the extra re-anchor frame.
298+ initNodesManager ();
299+
300+ auto rootTag = getNextRootViewTag ();
301+
302+ auto valueNodeTag = ++rootTag;
303+ nodesManager_->createAnimatedNode (
304+ valueNodeTag,
305+ folly::dynamic::object (" type" , " value" )(" value" , 0 )(" offset" , 0 ));
306+
307+ const auto animationId = 1 ;
308+ const auto frames = folly::dynamic::array (0 .0f , 0 .25f , 0 .5f , 0 .75f , 1 .0f );
309+ const auto toValue = 100 ;
310+ nodesManager_->startAnimatingNode (
311+ animationId,
312+ valueNodeTag,
313+ folly::dynamic::object (" type" , " frames" )(" frames" , frames)(
314+ " toValue" , toValue)(" deferredStart" , false ),
315+ std::nullopt );
316+
317+ const double t = 12345 ;
318+
319+ // Frame 1: timeDelta=0, value=0 (no deferred start delay)
320+ runAnimationFrame (t);
321+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), 0 );
322+
323+ // Frame 2: timeDelta=SI, value≈25 (animation progresses immediately)
324+ runAnimationFrame (t + SingleFrameIntervalMs);
325+ EXPECT_NEAR (nodesManager_->getValue (valueNodeTag).value (), 25 , 0.01 );
326+
327+ // Complete
328+ runAnimationFrame (t + SingleFrameIntervalMs * 4 );
329+ EXPECT_EQ (nodesManager_->getValue (valueNodeTag).value (), toValue);
118330}
119331
120332} // namespace facebook::react
0 commit comments