Reputation: 9587
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
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
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:
withAnimatedTimeTravelEnabled
in your test script's describe block (or root block) to register Jest timers for the tests.timeTravel
in your test script after an animation has been triggeredUpvotes: 1
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
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
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
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:
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.
Upvotes: 26