Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jest test Animated.View for React-Native app

I try to test an Animated.View with Jest for React-Native. When I set a property visible to true, it supposed to animate my view from opacity 0 to opacity 1.

This is what my component renders:

<Animated.View
    style={{
        opacity: opacityValue,
    }}
>
    <Text>{message}</Text>
</Animated.View>

Where opacityValue gets updated when the props visible changes:

Animated.timing(
    this.opacityValue, {
        toValue: this.props.visible ? 1 : 0,
        duration: 350,
    },
).start(),

I want to make sure my view is visible when I set it the property visible=true. Although it takes some time for the view to become visible and as the test runs, the opacity is equal to 0.

This is my test it:

it('Becomes visible when visible=true', () => {
    const tree = renderer.create(
        <MessageBar
            visible={true}
        />
    ).toJSON();
    expect(tree).toMatchSnapshot();
});

I was wondering how I could have Jest to wait? Or how I could test this to make sure the view becomes visible when I set the props to true?

Thanks.

like image 841
alexmngn Avatar asked Feb 16 '17 08:02

alexmngn


People also ask

How do I run jest tests with React Native?

Run yarn test to run tests with Jest. Let's create a snapshot test for a small intro component with a few views and text components and some styles: This is a React Native snapshot test. Now let's use React's test renderer and Jest's snapshot feature to interact with the component and capture the rendered output and create a snapshot file:

What is the best way to test a react app?

At Facebook, we use Jest to test React applications. If you are new to React, we recommend using Create React App. It is ready to use and ships with Jest! You will only need to add react-test-renderer for rendering snapshots.

How do I use React's test renderer and jest's snapshot feature?

Now let's use React's test renderer and Jest's snapshot feature to interact with the component and capture the rendered output and create a snapshot file: When you run yarn test or jest, this will produce an output file like this:

What is a React Native SNAPSHOT test?

This is a React Native snapshot test. The next time you run the tests, the rendered output will be compared to the previously created snapshot. The snapshot should be committed along with code changes. When a snapshot test fails, you need to inspect whether it is an intended or unintended change.


3 Answers

I solved this problem by creating an Animated stub for tests.

I see you are using visible as a property, so an working example is:

Components code

import React from 'react';                                                                                                                                                                            
import { Animated, Text, View, TouchableOpacity } from 'react-native';                                                                                                                                

// This class will control the visible prop                                                                                                                                                                                                  
class AnimatedOpacityController extends React.Component {                                                                                                                                             

  constructor(props, ctx) {                                                                                                                                                                           
    super(props, ctx);                                                                                                                                                                                
    this.state = {                                                                                                                                                                                    
      showChild: false,                                                                                                                                                                               
    };                                                                                                                                                                                                
  }                                                                                                                                                                                                   

  render() {                                                                                                                                                                                          
    const { showChild } = this.state;                                                                                                                                                                 
    return (                                                                                                                                                                                          
      <View>                                                                                                                                                                                          
        <AnimatedOpacity visible={this.state.showChild} />                                                                                                                                            
        <TouchableOpacity onPress={() => this.setState({ showChild: !showChild })}>                                                                                                                   
          <Text>{showChild ? 'Hide' : 'Show' } greeting</Text>                                                                                                                                        
        </TouchableOpacity>                                                                                                                                                                           
      </View>                                                                                                                                                                                         
    );                                                                                                                                                                                                 
  }                                                                                                                                                                                                   

}                                                                                                                                                                                                     

// This is your animated Component                                                                                                                                                                                                   
class AnimatedOpacity extends React.Component {                                                                                                                                                       

  constructor(props, ctx) {                                                                                                                                                                           
    super(props, ctx);                                                                                                                                                                                
    this.state = {                                                                                                                                                                                    
      opacityValue: new Animated.Value(props.visible ? 1 : 0),                                                                                                                                                                                                                                                                                                               
    };                                                                                                                                                                                                
  }

  componentWillReceiveProps(nextProps) {                                                                                                                                                              
    if (nextProps.visible !== this.props.visible) {                                                                                                                                                   
      this._animate(nextProps.visible);                                                                                                                                                               
    }                                                                                                                                                                                                 
  }                                                                                                                                                                                                   

  _animate(visible) {                                                                                                                                                                                 
    Animated.timing(this.state.opacityValue, {                                                                                                                                                        
      toValue: visible ? 1 : 0,                                                                                                                                                                       
      duration: 350,                                                                                                                                                                                  
    }).start();                                                                                                                                                       
  }                                                                                                                                                                                                   

  render() {                      
    return (                                                                                                                                                                                          
      <Animated.View style={{ opacity: this.state.opacityValue }}>                                                                                                                                    
        <Text>Hello World</Text>                                                                                                                                                                      
      </Animated.View>                                                                                                                                                                                
    );                                                                                                                                                                                                 

  }                                                                                                                                                                                                   

}                                                                                                                                                                                                     


export { AnimatedOpacityController, AnimatedOpacity };

Now moving to tests

import React from 'react';                                                                                                                                                                            
import renderer from 'react-test-renderer';                                                                                                                                                           
import { shallow } from 'enzyme';                                                                                                                                                                                                                                                                                                                                                                       

import { AnimatedOpacityController, AnimatedOpacity } from '../AnimatedOpacity';                                                                                                                    


jest.mock('Animated', () => {                                                                                                                                                                         
  const ActualAnimated = require.requireActual('Animated');                                                                                                                                           
  return {                                                                                                                                                                                            
    ...ActualAnimated,                                                                                                                                                                                
    timing: (value, config) => {                                                                                                                                                                      
      return {                                                                                                                                                                                        
        start: (callback) => {
          value.setValue(config.toValue);
          callback && callback()
        },                                                                                                                                                  
      };                                                                                                                                                                                              
    },                                                                                                                                                                                                
  };                                                                                                                                                                                                  
});                                                                                                                                                                                                                                                                                                                                                                                                     

it('renders visible', () => {                                                                                                                                                                         
  expect(                                                                                                                                                                                             
    renderer.create(                                                                                                                                                                                  
      <AnimatedOpacity visible={true} />                                                                                                                                                              
    ).toJSON()                                                                                                                                                                                        
  ).toMatchSnapshot();                                                                                                                                                                                
});                                                                                                                                                                                                   

it('renders invisible', () => {                                                                                                                                                                       
  expect(                                                                                                                                                                                             
    renderer.create(                                                                                                                                                                                  
      <AnimatedOpacity visible={false} />                                                                                                                                                             
    ).toJSON()                                                                                                                                                                                        
  ).toMatchSnapshot();                                                                                                                                                                                
});                                                                                                                                                                                                   

it('makes transition', () => {                                                                                                                                                                        
  const component = shallow(<AnimatedOpacityController />);                                                                                                                                           
  expect(renderer.create(component.node).toJSON()).toMatchSnapshot();                                                                                                                                 
  component.find('TouchableOpacity').simulate('press');                                                                                                                                               
  expect(renderer.create(component.node).toJSON()).toMatchSnapshot();                                                                                                                                 
  component.find('TouchableOpacity').simulate('press');                                                                                                                                               
  expect(renderer.create(component.node).toJSON()).toMatchSnapshot();                                                                                                                                 
});                                                                                                                                                                                                   

Now the generated snapshots will have opacity values as expected. If you are using animated a lot you can move you mock to js/config/jest and edit you package.json to use it in all your tests, then any change made to your stub will be available to all tests.

EDITED:

The solution above solves only to go from beginning to end. A more granular solution is:

  1. Don't mock Animated
  2. In jest config make global.requestAnimationFrame = null
  3. Use mockdate do mock the date
  4. Use jest.runTimersToTime for time travel

A time travel function would be

const timeTravel = (ms, step = 100) => {                                                                                                                                                                              

  const tickTravel = v => {                                                                                                                                                                               
    jest.runTimersToTime(v);                                                                                                                                                                              
    const now = Date.now();                                                                                                                                                                               
    MockDate.set(new Date(now + v));                                                                                                                                                                      
  }                                                                                                                                                                                                       

  let done = 0;                                                                                                                                                                                           
  while (ms - done > step) {                                                                                                                                                                               
    tickTravel(step);                                                                                                                                                                                      
    done += step;                                                                                                                                                                                          
  }                                                                                                                                                                                                       
  tickTravel(ms - done);                                                                                                                                                                                  
};    

Breaking steps in small chunks is importante because of Animated internal behavior.

like image 80
Aspirina Avatar answered Oct 10 '22 01:10

Aspirina


Aspirina's EDIT was helpful in resolving this issue, but it didn't get the job done directly. For those that follow, this is how I solved the issue of simulating animation progress:

I'm using Jest - this is my setupTests.js script that bootstraps the test environment

const MockDate = require('mockdate')
const frameTime = 10

global.requestAnimationFrame = (cb) => {
    // Default implementation of requestAnimationFrame calls setTimeout(cb, 0),
    // which will result in a cascade of timers - this generally pisses off test runners
    // like Jest who watch the number of timers created and assume an infinite recursion situation
    // if the number gets too large.
    //
    // Setting the timeout simulates a frame every 1/100th of a second
    setTimeout(cb, frameTime)
}

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()
    }
}

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. Here's an example of how to use it (imagine I have an animation that takes one second to complete):

beforeEach(() => {
    // As part of constructing the Animation, it will grab the
    // current time. Mocking the date right away ensures everyone
    // is starting from the same time
    MockDate.set(0)

    // Need to fake the timers for timeTravel to work
    jest.useFakeTimers()
})

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)
  })
})
like image 11
Matt McCann Avatar answered Oct 10 '22 00:10

Matt McCann


You can mock Animated.View so that it behaves like a regular view while testing.

jest.mock('react-native', () => {
  const rn = jest.requireActual('react-native')
  const spy = jest.spyOn(rn.Animated, 'View', 'get')
  spy.mockImplementation(() => jest.fn(({children}) => children));
  return rn
});

I adapted this from React Transition Group's example of mocking Transition Groups

like image 1
Jared Beach Avatar answered Oct 09 '22 23:10

Jared Beach