Unit tests are a universal best practice in software development. React-Native user interface code is no exception, with Jest being the most common testing framework. React’s Animated View component is particularly bothersome to test. The solution is nothing short of time travel. Let us bend space-time and solve our unit testing woes.
Our Example Animated View Component
Let’s consider the following example animated React Component. Our SignUp component starts off screen and slides up into view. Unfortunately, testing our animation behave is not straight forward using the Jest testing framework. Our component’s behavior is tied to the the passage of 1000ms, in which the view slides up.
import React from 'react' import { Animated, View, Text } from 'react-native' import Button from 'components/button' import { container, sheet } from './styles/sign-up-styles' /** Types for the component */ type PropType = { /** Handler for the user pressing the "Sign Up" button */ onPressSignUp: () => void } type StateType = { /** The slide animation value */ slideAnim: Animated.Value } /** Animated menu that slides up to prompt the user to sign up */ export default class SignUp extends React.Component<PropType, StateType> { constructor(props: PropType) { super(props) // Place the prompt below the bottom of the screen out of sight this.state = { slideAnim: new Animated.Value(-350), } } /** When the component is placed on the DOM */ componentDidMount() { // Slide up into view Animated.timing( this.state.slideAnim, { toValue: 0, // Transition to the bottom of the prompt along the bottom of the screen duration: 1000, // Transition over the course of a second }, ).start() // Start the animation } /** Render the component */ render() { const { slideAnim } = this.state return ( <Animated.View id="animatedView" style={{ ...container(), bottom: slideAnim, }} > <View style={sheet.container}> <Button id="signInUp" primary style={[sheet.button, sheet.signUpButton]} onPress={this.props.onPressSignUp} > Sign Up </Button> <Text>OR</Text> </View> </Animated.View> ) } }
Unit Testing Our Animated View Component
Imagine we want to perform a simple test that checks that the Sign Up view is half-way onto the screen after half of the animation time has elapsed, and then also test that the Sign Up view all the way onto the screen after the full animation has elapsed. Here’s what our sample test might look like:
import { shallow } from 'enzyme' import toJson from 'enzyme-to-json' import React from 'react' import MockDate from 'mockdate' import SignUp from 'SignUpOrDiscover' describe('SignUp', () => { let component; let mockOnPressUp; beforeEach(() => { MockDate.set(0) jest.useFakeTimers() mockOnPressSignUp = jest.fn() component = shallow(<SignUp onPressSignUp={mockOnPressSignInUp}/>) }) describe('render', () => { let style beforeEach(() => { style = component.find('[id="animatedView"]').prop('style') }) describe('at start of animation', () => { it('renders as expected', () => { expect(toJson(component)).toMatchSnapshot() }) it('has a bottom of -350', () => { expect(style.bottom._value).toEqual(-350) }) }) describe('half way through animantion', () => { it('has a bottom of -175', () => { // TODO: Make 500 milliseconds pass expect(style.bottom._value).toEqual(-175) }) }) describe('at end of animation', () => { it('has a bottom of 0', () => { // TODO: Make 1000 milliseconds pass expect(style.bottom._value).toEqual(0) }) }) }) })
Now, The Time Travel
In order to advance time inside the unit tests, we need to interact with Jest’s advanceTimersByTime()
method. Here is how we can configure a global helper method to unlock the promised time travel in our setupTests.js
file. The idea here is that we are slowing the requestAnimationFrame rate to be exactly 100 fps, and the timeTravel function allows you to step forward in time increments of one frame.
import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() }); const MockDate = require('mockdate') const frameTime = 10 global.timeTravel = (time = frameTime) => { const tickTravel = () => { // The React Animations module looks at the elapsed time for each frame to calculate its // new position const now = Date.now() MockDate.set(new Date(now + frameTime)) // Run the timers forward jest.advanceTimersByTime(frameTime) } // Step through each of the frames const frames = time / frameTime let framesEllapsed for (framesEllapsed = 0; framesEllapsed < frames; framesEllapsed++) { tickTravel() } }
Now with our handy timeTravel
global method defines, we can update our tests to move through time to the exact moment we want to evaluate with our unit tests:
describe('half way through animation', () => { it('has a bottom of -175', () => { global.timeTravel(500); expect(style.bottom._value).toEqual(-175); }) }) describe('at end of animation', () => { it('has a bottom of 0', () => { global.timeTravel(1000); expect(style.bottom._value).toEqual(0); }) })
References
You can find my answer contribution on StackOverflow, as well as the answers of fellow contributors whose ideas fed into this solution here: