alexmngn
alexmngn

Reputation: 9587

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.

Upvotes: 18

Views: 23787

Answers (6)

Matt McCann
Matt McCann

Reputation: 325

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

Long-form write-up with more complete code samples available here

Upvotes: 11

Lambda
Lambda

Reputation: 21

Now you can use Jest to time travel animations for React Native components. Nowadays it is possible to remove the MockDate package suggested in other answers, as Jest supports this out-of-the-box by itself. I found this because MockDate didn't work with my babel setup.

Here's my modified setup:

export const withAnimatedTimeTravelEnabled = () => {
  beforeEach(() => {
    jest.useFakeTimers()
    jest.setSystemTime(new Date(0))
  })
  afterEach(() => {
    jest.useRealTimers()
  })
}

const frameTime = 10
export const timeTravel = (time = frameTime) => {
  const tickTravel = () => {
    const now = Date.now()
    jest.setSystemTime(new Date(now + frameTime))
    jest.advanceTimersByTime(frameTime)
  }
  // Step through each of the frames
  const frames = time / frameTime
  for (let i = 0; i < frames; i++) {
    tickTravel()
  }
}

To clarify:

  • Call withAnimatedTimeTravelEnabled in your test script's describe block (or root block) to register Jest timers for the tests.
  • Call timeTravel in your test script after an animation has been triggered

Upvotes: 1

Alex Aymkin
Alex Aymkin

Reputation: 518

You can mock Animated.createAnimatedComponent in the following way

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

Upvotes: 0

Jared Beach
Jared Beach

Reputation: 3133

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

Upvotes: 6

Daan van Hasselt
Daan van Hasselt

Reputation: 1

In my case, I am not using Animated.View at all. But instead, I have a component that uses requestAnimationFrame. The callback actually makes use of the time argument so I had to pass the current time to the callback function when replacing requestAnimationFrame like so:

global.requestAnimationFrame = (cb) => {
    setTimeout(() => cb(Date.now()), frameTime)
}

Upvotes: 0

Aspirina
Aspirina

Reputation: 686

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.

Upvotes: 26

Related Questions