Search code examples
javascriptunit-testingreact-nativejestjs

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.


Solution

  • 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.