Jest test Animated.View for React-Native app
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
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)
})
})
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:
- Don't mock Animated
- In jest config make
global.requestAnimationFrame = null
- Use mockdate do mock the date
- 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.