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.
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:
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.
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:
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.
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:
global.requestAnimationFrame = null
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.
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)
})
})
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With