/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @emails oncall+react_native */ jest .clearAllMocks() .mock('../../BatchedBridge/NativeModules', () => ({ NativeAnimatedModule: {}, PlatformConstants: { getConstants() { return {}; }, }, })) .mock('../NativeAnimatedModule') .mock('../../EventEmitter/NativeEventEmitter') // findNodeHandle is imported from ReactNative so mock that whole module. .setMock('../../Renderer/shims/ReactNative', {findNodeHandle: () => 1}); import TestRenderer from 'react-test-renderer'; import * as React from 'react'; const Animated = require('../Animated'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); describe('Native Animated', () => { const NativeAnimatedModule = require('../NativeAnimatedModule').default; beforeEach(() => { Object.assign(NativeAnimatedModule, { getValue: jest.fn(), addAnimatedEventToView: jest.fn(), connectAnimatedNodes: jest.fn(), connectAnimatedNodeToView: jest.fn(), createAnimatedNode: jest.fn(), disconnectAnimatedNodeFromView: jest.fn(), disconnectAnimatedNodes: jest.fn(), dropAnimatedNode: jest.fn(), extractAnimatedNodeOffset: jest.fn(), flattenAnimatedNodeOffset: jest.fn(), removeAnimatedEventFromView: jest.fn(), restoreDefaultValues: jest.fn(), setAnimatedNodeOffset: jest.fn(), setAnimatedNodeValue: jest.fn(), startAnimatingNode: jest.fn(), startListeningToAnimatedNodeValue: jest.fn(), stopAnimation: jest.fn(), stopListeningToAnimatedNodeValue: jest.fn(), }); }); describe('Animated Value', () => { it('proxies `setValue` correctly', () => { const opacity = new Animated.Value(0); const ref = React.createRef(null); Animated.timing(opacity, { toValue: 10, duration: 1000, useNativeDriver: true, }).start(); TestRenderer.create(); expect(ref.current).not.toBeNull(); jest.spyOn(ref.current, 'setNativeProps'); opacity.setValue(0.5); expect(NativeAnimatedModule.setAnimatedNodeValue).toBeCalledWith( expect.any(Number), 0.5, ); expect(ref.current.setNativeProps).not.toHaveBeenCalled(); }); it('should set offset', () => { const opacity = new Animated.Value(0); opacity.setOffset(10); opacity.__makeNative(); TestRenderer.create(); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'value', value: 0, offset: 10}, ); opacity.setOffset(20); expect(NativeAnimatedModule.setAnimatedNodeOffset).toBeCalledWith( expect.any(Number), 20, ); }); it('should flatten offset', () => { const opacity = new Animated.Value(0); opacity.__makeNative(); TestRenderer.create(); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'value', value: 0, offset: 0}, ); opacity.flattenOffset(); expect(NativeAnimatedModule.flattenAnimatedNodeOffset).toBeCalledWith( expect.any(Number), ); }); it('should save value on unmount', () => { NativeAnimatedModule.getValue = jest.fn((tag, saveCallback) => { saveCallback(1); }); const opacity = new Animated.Value(0); opacity.__makeNative(); const root = TestRenderer.create(); const tag = opacity.__getNativeTag(); root.unmount(); expect(NativeAnimatedModule.getValue).toBeCalledWith( tag, expect.any(Function), ); expect(opacity.__getValue()).toBe(1); }); it('should extract offset', () => { const opacity = new Animated.Value(0); opacity.__makeNative(); TestRenderer.create(); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'value', value: 0, offset: 0}, ); opacity.extractOffset(); expect(NativeAnimatedModule.extractAnimatedNodeOffset).toBeCalledWith( expect.any(Number), ); }); }); describe('Animated Listeners', () => { it('should get updates', () => { const value1 = new Animated.Value(0); value1.__makeNative(); const listener = jest.fn(); const id = value1.addListener(listener); expect( NativeAnimatedModule.startListeningToAnimatedNodeValue, ).toHaveBeenCalledWith(value1.__getNativeTag()); NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', { value: 42, tag: value1.__getNativeTag(), }); expect(listener).toHaveBeenCalledTimes(1); expect(listener).toBeCalledWith({value: 42}); expect(value1.__getValue()).toBe(42); NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', { value: 7, tag: value1.__getNativeTag(), }); expect(listener).toHaveBeenCalledTimes(2); expect(listener).toBeCalledWith({value: 7}); expect(value1.__getValue()).toBe(7); value1.removeListener(id); expect( NativeAnimatedModule.stopListeningToAnimatedNodeValue, ).toHaveBeenCalledWith(value1.__getNativeTag()); NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', { value: 1492, tag: value1.__getNativeTag(), }); expect(listener).toHaveBeenCalledTimes(2); expect(value1.__getValue()).toBe(7); }); it('should removeAll', () => { const value1 = new Animated.Value(0); value1.__makeNative(); const listener = jest.fn(); [1, 2, 3, 4].forEach(() => value1.addListener(listener)); expect( NativeAnimatedModule.startListeningToAnimatedNodeValue, ).toHaveBeenCalledWith(value1.__getNativeTag()); NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', { value: 42, tag: value1.__getNativeTag(), }); expect(listener).toHaveBeenCalledTimes(4); expect(listener).toBeCalledWith({value: 42}); value1.removeAllListeners(); expect( NativeAnimatedModule.stopListeningToAnimatedNodeValue, ).toHaveBeenCalledWith(value1.__getNativeTag()); NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', { value: 7, tag: value1.__getNativeTag(), }); expect(listener).toHaveBeenCalledTimes(4); }); }); describe('Animated Events', () => { it('should map events', () => { const value = new Animated.Value(0); value.__makeNative(); const event = Animated.event([{nativeEvent: {state: {foo: value}}}], { useNativeDriver: true, }); const root = TestRenderer.create(); expect(NativeAnimatedModule.addAnimatedEventToView).toBeCalledWith( expect.any(Number), 'onTouchMove', { nativeEventPath: ['state', 'foo'], animatedValueTag: value.__getNativeTag(), }, ); expect( NativeAnimatedModule.removeAnimatedEventFromView, ).not.toHaveBeenCalled(); root.unmount(); expect(NativeAnimatedModule.removeAnimatedEventFromView).toBeCalledWith( expect.any(Number), 'onTouchMove', value.__getNativeTag(), ); }); it('should throw on invalid event path', () => { const value = new Animated.Value(0); value.__makeNative(); const event = Animated.event([{notNativeEvent: {foo: value}}], { useNativeDriver: true, }); jest.spyOn(console, 'error').mockImplementationOnce((...args) => { if (args[0].startsWith('The above error occurred in the')) { return; } console.errorDebug(...args); }); expect(() => { TestRenderer.create(); }).toThrowError(/nativeEvent/); expect(NativeAnimatedModule.addAnimatedEventToView).not.toBeCalled(); console.error.mockRestore(); }); it('should call listeners', () => { const value = new Animated.Value(0); value.__makeNative(); const listener = jest.fn(); const event = Animated.event([{nativeEvent: {foo: value}}], { useNativeDriver: true, listener, }); const handler = event.__getHandler(); handler({nativeEvent: {foo: 42}}); expect(listener).toHaveBeenCalledTimes(1); expect(listener).toBeCalledWith({nativeEvent: {foo: 42}}); }); }); describe('Animated Graph', () => { it('creates and detaches nodes', () => { const opacity = new Animated.Value(0); const root = TestRenderer.create(); Animated.timing(opacity, { toValue: 10, duration: 1000, useNativeDriver: true, }).start(); expect(NativeAnimatedModule.createAnimatedNode).toHaveBeenCalledTimes(3); expect(NativeAnimatedModule.connectAnimatedNodes).toHaveBeenCalledTimes( 2, ); expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { type: 'frames', frames: expect.any(Array), toValue: expect.any(Number), iterations: 1, }, expect.any(Function), ); expect( NativeAnimatedModule.disconnectAnimatedNodes, ).not.toHaveBeenCalled(); expect(NativeAnimatedModule.dropAnimatedNode).not.toHaveBeenCalled(); root.unmount(); expect( NativeAnimatedModule.disconnectAnimatedNodes, ).toHaveBeenCalledTimes(2); expect(NativeAnimatedModule.dropAnimatedNode).toHaveBeenCalledTimes(3); }); it('sends a valid description for value, style and props nodes', () => { const opacity = new Animated.Value(0); TestRenderer.create(); Animated.timing(opacity, { toValue: 10, duration: 1000, useNativeDriver: true, }).start(); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'value', value: 0, offset: 0}, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'style', style: {opacity: expect.any(Number)}}, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'props', props: {style: expect.any(Number)}}, ); }); it('sends a valid graph description for Animated.add nodes', () => { const first = new Animated.Value(1); const second = new Animated.Value(2); first.__makeNative(); second.__makeNative(); TestRenderer.create( , ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'addition', input: expect.any(Array)}, ); const additionCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'addition', ); expect(additionCalls.length).toBe(1); const additionCall = additionCalls[0]; const additionNodeTag = additionCall[0]; const additionConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === additionNodeTag, ); expect(additionConnectionCalls.length).toBe(2); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( additionCall[1].input[0], { type: 'value', value: 1, offset: 0, }, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( additionCall[1].input[1], { type: 'value', value: 2, offset: 0, }, ); }); it('sends a valid graph description for Animated.subtract nodes', () => { const first = new Animated.Value(2); const second = new Animated.Value(1); first.__makeNative(); second.__makeNative(); TestRenderer.create( , ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'subtraction', input: expect.any(Array)}, ); const subtractionCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'subtraction', ); expect(subtractionCalls.length).toBe(1); const subtractionCall = subtractionCalls[0]; const subtractionNodeTag = subtractionCall[0]; const subtractionConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === subtractionNodeTag, ); expect(subtractionConnectionCalls.length).toBe(2); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( subtractionCall[1].input[0], { type: 'value', value: 2, offset: 0, }, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( subtractionCall[1].input[1], { type: 'value', value: 1, offset: 0, }, ); }); it('sends a valid graph description for Animated.multiply nodes', () => { const first = new Animated.Value(2); const second = new Animated.Value(1); first.__makeNative(); second.__makeNative(); TestRenderer.create( , ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'multiplication', input: expect.any(Array)}, ); const multiplicationCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'multiplication', ); expect(multiplicationCalls.length).toBe(1); const multiplicationCall = multiplicationCalls[0]; const multiplicationNodeTag = multiplicationCall[0]; const multiplicationConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === multiplicationNodeTag, ); expect(multiplicationConnectionCalls.length).toBe(2); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( multiplicationCall[1].input[0], { type: 'value', value: 2, offset: 0, }, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( multiplicationCall[1].input[1], { type: 'value', value: 1, offset: 0, }, ); }); it('sends a valid graph description for Animated.divide nodes', () => { const first = new Animated.Value(4); const second = new Animated.Value(2); first.__makeNative(); second.__makeNative(); TestRenderer.create( , ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'division', input: expect.any(Array)}, ); const divisionCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'division', ); expect(divisionCalls.length).toBe(1); const divisionCall = divisionCalls[0]; const divisionNodeTag = divisionCall[0]; const divisionConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === divisionNodeTag, ); expect(divisionConnectionCalls.length).toBe(2); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( divisionCall[1].input[0], { type: 'value', value: 4, offset: 0, }, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( divisionCall[1].input[1], { type: 'value', value: 2, offset: 0, }, ); }); it('sends a valid graph description for Animated.modulo nodes', () => { const value = new Animated.Value(4); value.__makeNative(); TestRenderer.create( , ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'modulus', modulus: 4, input: expect.any(Number)}, ); const moduloCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'modulus', ); expect(moduloCalls.length).toBe(1); const moduloCall = moduloCalls[0]; const moduloNodeTag = moduloCall[0]; const moduloConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === moduloNodeTag, ); expect(moduloConnectionCalls.length).toBe(1); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( moduloCall[1].input, { type: 'value', value: 4, offset: 0, }, ); }); it('sends a valid graph description for interpolate() nodes', () => { const value = new Animated.Value(10); value.__makeNative(); TestRenderer.create( , ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'value', value: 10, offset: 0}, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), { type: 'interpolation', inputRange: [10, 20], outputRange: [0, 1], extrapolateLeft: 'extend', extrapolateRight: 'extend', }, ); const interpolationNodeTag = NativeAnimatedModule.createAnimatedNode.mock.calls.find( call => call[1].type === 'interpolation', )[0]; const valueNodeTag = NativeAnimatedModule.createAnimatedNode.mock.calls.find( call => call[1].type === 'value', )[0]; expect(NativeAnimatedModule.connectAnimatedNodes).toBeCalledWith( valueNodeTag, interpolationNodeTag, ); }); it('sends a valid graph description for transform nodes', () => { const translateX = new Animated.Value(0); translateX.__makeNative(); TestRenderer.create( , ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), { type: 'transform', transforms: [ { nodeTag: expect.any(Number), property: 'translateX', type: 'animated', }, { value: 2, property: 'scale', type: 'static', }, ], }, ); }); it('sends a valid graph description for Animated.diffClamp nodes', () => { const value = new Animated.Value(2); value.__makeNative(); TestRenderer.create( , ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'diffclamp', input: expect.any(Number), max: 20, min: 0}, ); const diffClampCalls = NativeAnimatedModule.createAnimatedNode.mock.calls.filter( call => call[1].type === 'diffclamp', ); expect(diffClampCalls.length).toBe(1); const diffClampCall = diffClampCalls[0]; const diffClampNodeTag = diffClampCall[0]; const diffClampConnectionCalls = NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( call => call[1] === diffClampNodeTag, ); expect(diffClampConnectionCalls.length).toBe(1); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( diffClampCall[1].input, { type: 'value', value: 2, offset: 0, }, ); }); it("doesn't call into native API if useNativeDriver is set to false", () => { const opacity = new Animated.Value(0); const root = TestRenderer.create(); Animated.timing(opacity, { toValue: 10, duration: 1000, useNativeDriver: false, }).start(); root.unmount(); expect(NativeAnimatedModule.createAnimatedNode).not.toBeCalled(); }); it('fails when trying to run non-native animation on native node', () => { const opacity = new Animated.Value(0); const ref = React.createRef(null); TestRenderer.create(); // Necessary to simulate the native animation. expect(ref.current).not.toBeNull(); ref.current.setNativeProps = jest.fn(); Animated.timing(opacity, { toValue: 10, duration: 50, useNativeDriver: true, }).start(); jest.runAllTimers(); Animated.timing(opacity, { toValue: 4, duration: 500, useNativeDriver: false, }).start(); try { process.env.NODE_ENV = 'development'; expect(jest.runAllTimers).toThrow( 'Attempting to run JS driven animation on animated node that has ' + 'been moved to "native" earlier by starting an animation with ' + '`useNativeDriver: true`', ); } finally { process.env.NODE_ENV = 'test'; } }); it('fails for unsupported styles', () => { const left = new Animated.Value(0); TestRenderer.create(); const animation = Animated.timing(left, { toValue: 10, duration: 50, useNativeDriver: true, }); expect(animation.start).toThrowError(/left/); }); it('works for any `static` props and styles', () => { // Passing "unsupported" props should work just fine as long as they are not animated const opacity = new Animated.Value(0); opacity.__makeNative(); TestRenderer.create( , ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'style', style: {opacity: expect.any(Number)}}, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( expect.any(Number), {type: 'props', props: {style: expect.any(Number)}}, ); }); }); describe('Animations', () => { it('sends a valid timing animation description', () => { const anim = new Animated.Value(0); Animated.timing(anim, { toValue: 10, duration: 1000, useNativeDriver: true, }).start(); expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { type: 'frames', frames: expect.any(Array), toValue: expect.any(Number), iterations: 1, }, expect.any(Function), ); }); it('sends a valid spring animation description', () => { const anim = new Animated.Value(0); Animated.spring(anim, { toValue: 10, friction: 5, tension: 164, useNativeDriver: true, }).start(); expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { type: 'spring', stiffness: 679.08, damping: 16, mass: 1, initialVelocity: 0, overshootClamping: false, restDisplacementThreshold: 0.001, restSpeedThreshold: 0.001, toValue: 10, iterations: 1, }, expect.any(Function), ); Animated.spring(anim, { toValue: 10, stiffness: 1000, damping: 500, mass: 3, useNativeDriver: true, }).start(); expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { type: 'spring', stiffness: 1000, damping: 500, mass: 3, initialVelocity: 0, overshootClamping: false, restDisplacementThreshold: 0.001, restSpeedThreshold: 0.001, toValue: 10, iterations: 1, }, expect.any(Function), ); Animated.spring(anim, { toValue: 10, bounciness: 8, speed: 10, useNativeDriver: true, }).start(); expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { type: 'spring', damping: 23.05223140901191, initialVelocity: 0, overshootClamping: false, restDisplacementThreshold: 0.001, restSpeedThreshold: 0.001, stiffness: 299.61882352941177, mass: 1, toValue: 10, iterations: 1, }, expect.any(Function), ); }); it('sends a valid decay animation description', () => { const anim = new Animated.Value(0); Animated.decay(anim, { velocity: 10, deceleration: 0.1, useNativeDriver: true, }).start(); expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), {type: 'decay', deceleration: 0.1, velocity: 10, iterations: 1}, expect.any(Function), ); }); it('works with Animated.loop', () => { const anim = new Animated.Value(0); Animated.loop( Animated.decay(anim, { velocity: 10, deceleration: 0.1, useNativeDriver: true, }), {iterations: 10}, ).start(); expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), {type: 'decay', deceleration: 0.1, velocity: 10, iterations: 10}, expect.any(Function), ); }); it('sends stopAnimation command to native', () => { const value = new Animated.Value(0); const animation = Animated.timing(value, { toValue: 10, duration: 50, useNativeDriver: true, }); animation.start(); expect(NativeAnimatedModule.startAnimatingNode).toBeCalledWith( expect.any(Number), expect.any(Number), { type: 'frames', frames: expect.any(Array), toValue: expect.any(Number), iterations: 1, }, expect.any(Function), ); const animationId = NativeAnimatedModule.startAnimatingNode.mock.calls[0][0]; animation.stop(); expect(NativeAnimatedModule.stopAnimation).toBeCalledWith(animationId); }); }); describe('Animated Components', () => { it('Should restore default values on prop updates only', () => { const opacity = new Animated.Value(0); opacity.__makeNative(); const root = TestRenderer.create(); expect(NativeAnimatedModule.restoreDefaultValues).not.toHaveBeenCalled(); root.update(); expect(NativeAnimatedModule.restoreDefaultValues).toHaveBeenCalledWith( expect.any(Number), ); root.unmount(); // Make sure it doesn't get called on unmount. expect(NativeAnimatedModule.restoreDefaultValues).toHaveBeenCalledTimes( 1, ); }); }); });