diff --git a/Libraries/Animated/AnimatedMock.js b/Libraries/Animated/AnimatedMock.js index 225669b1f640c9..561f4b5a87502c 100644 --- a/Libraries/Animated/AnimatedMock.js +++ b/Libraries/Animated/AnimatedMock.js @@ -27,8 +27,38 @@ import type {SpringAnimationConfig} from './animations/SpringAnimation'; /** * Animations are a source of flakiness in snapshot testing. This mock replaces * animation functions from AnimatedImplementation with empty animations for - * predictability in tests. + * predictability in tests. When possible the animation will run immediately + * to the final state. */ + +// Prevent any callback invocation from recursively triggering another +// callback, which may trigger another animation +let inAnimationCallback = false; +function mockAnimationStart( + start: (callback?: ?EndCallback) => void, +): (callback?: ?EndCallback) => void { + return callback => { + const guardedCallback = + callback == null + ? callback + : (...args) => { + if (inAnimationCallback) { + console.warn( + 'Ignoring recursive animation callback when running mock animations', + ); + return; + } + inAnimationCallback = true; + try { + callback(...args); + } finally { + inAnimationCallback = false; + } + }; + start(guardedCallback); + }; +} + export type CompositeAnimation = { start: (callback?: ?EndCallback) => void, stop: () => void, @@ -48,6 +78,16 @@ const emptyAnimation = { }, }; +const mockCompositeAnimation = ( + animations: Array, +): CompositeAnimation => ({ + ...emptyAnimation, + start: mockAnimationStart((callback?: ?EndCallback): void => { + animations.forEach(animation => animation.start()); + callback?.({finished: true}); + }), +}); + const spring = function( value: AnimatedValue | AnimatedValueXY, config: SpringAnimationConfig, @@ -55,10 +95,10 @@ const spring = function( const anyValue: any = value; return { ...emptyAnimation, - start: (callback?: ?EndCallback): void => { + start: mockAnimationStart((callback?: ?EndCallback): void => { anyValue.setValue(config.toValue); - callback && callback({finished: true}); - }, + callback?.({finished: true}); + }), }; }; @@ -69,10 +109,10 @@ const timing = function( const anyValue: any = value; return { ...emptyAnimation, - start: (callback?: ?EndCallback): void => { + start: mockAnimationStart((callback?: ?EndCallback): void => { anyValue.setValue(config.toValue); - callback && callback({finished: true}); - }, + callback?.({finished: true}); + }), }; }; @@ -86,7 +126,7 @@ const decay = function( const sequence = function( animations: Array, ): CompositeAnimation { - return emptyAnimation; + return mockCompositeAnimation(animations); }; type ParallelConfig = {stopTogether?: boolean, ...}; @@ -94,7 +134,7 @@ const parallel = function( animations: Array, config?: ?ParallelConfig, ): CompositeAnimation { - return emptyAnimation; + return mockCompositeAnimation(animations); }; const delay = function(time: number): CompositeAnimation { @@ -105,7 +145,7 @@ const stagger = function( time: number, animations: Array, ): CompositeAnimation { - return emptyAnimation; + return mockCompositeAnimation(animations); }; type LoopAnimationConfig = {