Time Travel To Easily Test Animated Views In React-Native

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:

https://stackoverflow.com/questions/42268673/jest-test-animated-view-for-react-native-app/51067606#51067606