UNPKG

6.68 kBPlain TextView Raw
1/* eslint-disable @typescript-eslint/no-namespace */
2'use strict';
3
4import type { ReactTestInstance } from 'react-test-renderer';
5import type {
6 AnimatedComponentProps,
7 IAnimatedComponentInternal,
8 InitialComponentProps,
9} from '../createAnimatedComponent/commonTypes';
10import { isJest } from './PlatformChecker';
11import type { DefaultStyle } from './hook/commonTypes';
12
13declare global {
14 namespace jest {
15 interface Matchers<R> {
16 toHaveAnimatedStyle(
17 style: Record<string, unknown>[] | Record<string, unknown>,
18 config?: {
19 shouldMatchAllProps?: boolean;
20 }
21 ): R;
22 }
23 }
24}
25
26const defaultFramerateConfig = {
27 fps: 60,
28};
29
30const getCurrentStyle = (component: TestComponent): DefaultStyle => {
31 const styleObject = component.props.style;
32 let currentStyle = {};
33 if (Array.isArray(styleObject)) {
34 styleObject.forEach((style) => {
35 currentStyle = {
36 ...currentStyle,
37 ...style,
38 };
39 });
40 } else {
41 currentStyle = {
42 ...styleObject,
43 ...component.props.jestAnimatedStyle?.value,
44 };
45 }
46 return currentStyle;
47};
48
49const checkEqual = <Value>(current: Value, expected: Value) => {
50 if (Array.isArray(expected)) {
51 if (!Array.isArray(current) || expected.length !== current.length) {
52 return false;
53 }
54 for (let i = 0; i < current.length; i++) {
55 if (!checkEqual(current[i], expected[i])) {
56 return false;
57 }
58 }
59 } else if (typeof current === 'object' && current) {
60 if (typeof expected !== 'object' || !expected) {
61 return false;
62 }
63 for (const property in expected) {
64 if (!checkEqual(current[property], expected[property])) {
65 return false;
66 }
67 }
68 } else {
69 return current === expected;
70 }
71 return true;
72};
73
74const findStyleDiff = (
75 current: DefaultStyle,
76 expected: DefaultStyle,
77 shouldMatchAllProps?: boolean
78) => {
79 const diffs = [];
80 let isEqual = true;
81 let property: keyof DefaultStyle;
82 for (property in expected) {
83 if (!checkEqual(current[property], expected[property])) {
84 isEqual = false;
85 diffs.push({
86 property,
87 current: current[property],
88 expect: expected[property],
89 });
90 }
91 }
92
93 if (
94 shouldMatchAllProps &&
95 Object.keys(current).length !== Object.keys(expected).length
96 ) {
97 isEqual = false;
98 // eslint-disable-next-line @typescript-eslint/no-shadow
99 let property: keyof DefaultStyle;
100 for (property in current) {
101 if (expected[property] === undefined) {
102 diffs.push({
103 property,
104 current: current[property],
105 expect: expected[property],
106 });
107 }
108 }
109 }
110
111 return { isEqual, diffs };
112};
113
114const compareStyle = (
115 component: TestComponent,
116 expectedStyle: DefaultStyle,
117 config: ToHaveAnimatedStyleConfig
118) => {
119 if (!component.props.style) {
120 return { message: () => `Component doesn't have a style.`, pass: false };
121 }
122 const { shouldMatchAllProps } = config;
123 const currentStyle = getCurrentStyle(component);
124 const { isEqual, diffs } = findStyleDiff(
125 currentStyle,
126 expectedStyle,
127 shouldMatchAllProps
128 );
129
130 if (isEqual) {
131 return { message: () => 'ok', pass: true };
132 }
133
134 const currentStyleStr = JSON.stringify(currentStyle);
135 const expectedStyleStr = JSON.stringify(expectedStyle);
136 const differences = diffs
137 .map(
138 (diff) =>
139 `- '${diff.property}' should be ${JSON.stringify(
140 diff.expect
141 )}, but is ${JSON.stringify(diff.current)}`
142 )
143 .join('\n');
144
145 return {
146 message: () =>
147 `Expected: ${expectedStyleStr}\nReceived: ${currentStyleStr}\n\nDifferences:\n${differences}`,
148 pass: false,
149 };
150};
151
152let frameTime = Math.round(1000 / defaultFramerateConfig.fps);
153
154const beforeTest = () => {
155 jest.useFakeTimers();
156};
157
158const afterTest = () => {
159 jest.runOnlyPendingTimers();
160 jest.useRealTimers();
161};
162
163export const withReanimatedTimer = (animationTest: () => void) => {
164 console.warn(
165 'This method is deprecated, you should define your own before and after test hooks to enable jest.useFakeTimers(). Check out the documentation for details on testing'
166 );
167 beforeTest();
168 animationTest();
169 afterTest();
170};
171
172export const advanceAnimationByTime = (time = frameTime) => {
173 console.warn(
174 'This method is deprecated, use jest.advanceTimersByTime directly'
175 );
176 jest.advanceTimersByTime(time);
177 jest.runOnlyPendingTimers();
178};
179
180export const advanceAnimationByFrame = (count: number) => {
181 console.warn(
182 'This method is deprecated, use jest.advanceTimersByTime directly'
183 );
184 jest.advanceTimersByTime(count * frameTime);
185 jest.runOnlyPendingTimers();
186};
187
188const requireFunction = isJest()
189 ? require
190 : () => {
191 throw new Error(
192 '[Reanimated] `setUpTests` is available only in Jest environment.'
193 );
194 };
195
196type ToHaveAnimatedStyleConfig = {
197 shouldMatchAllProps?: boolean;
198};
199
200export const setUpTests = (userFramerateConfig = {}) => {
201 let expect: jest.Expect = (global as typeof global & { expect: jest.Expect })
202 .expect;
203 if (expect === undefined) {
204 const expectModule = requireFunction('expect');
205 expect = expectModule;
206 // Starting from Jest 28, "expect" package uses named exports instead of default export.
207 // So, requiring "expect" package doesn't give direct access to "expect" function anymore.
208 // It gives access to the module object instead.
209 // We use this info to detect if the project uses Jest 28 or higher.
210 if (typeof expect === 'object') {
211 const jestGlobals = requireFunction('@jest/globals');
212 expect = jestGlobals.expect;
213 }
214 if (expect === undefined || expect.extend === undefined) {
215 expect = expectModule.default;
216 }
217 }
218
219 const framerateConfig = {
220 ...defaultFramerateConfig,
221 ...userFramerateConfig,
222 };
223 frameTime = Math.round(1000 / framerateConfig.fps);
224
225 expect.extend({
226 toHaveAnimatedStyle(
227 component: React.Component<
228 AnimatedComponentProps<InitialComponentProps>
229 > &
230 IAnimatedComponentInternal,
231 expectedStyle: DefaultStyle,
232 config: ToHaveAnimatedStyleConfig = {}
233 ) {
234 return compareStyle(component, expectedStyle, config);
235 },
236 });
237};
238
239type TestComponent = React.Component<
240 AnimatedComponentProps<InitialComponentProps> & {
241 jestAnimatedStyle?: { value: DefaultStyle };
242 }
243>;
244
245export const getAnimatedStyle = (component: ReactTestInstance) => {
246 return getCurrentStyle(
247 // This type assertion is needed to get type checking in the following
248 // functions since `ReactTestInstance` has its `props` defined as `any`.
249 component as unknown as TestComponent
250 );
251};