Skip to content

Commit 17a89d7

Browse files
rubennortemeta-codesync[bot]
authored andcommitted
Migrate Animated Jest tests to Fantom
Summary: Migrates 10 Animated unit tests from regular Jest (`-test.js`) to Fantom (`-itest.js`) so they run on Hermes in the real React Native runtime. Migrated: `Easing`, `bezier`, `Interpolation`, `AnimatedObject`, `AnimatedValue`, `AnimatedMock`, `TimingAnimation`, `createAnimatedPropsHook`, `createAnimatedPropsMemoHook`. The two `Libraries/Animated/nodes/AnimatedProps` cases were folded into the existing `AnimatedProps-itest.js` (importing the same module), with no loss of coverage. Adaptations (no behavioral coverage weakened): - Drove animation timing with Fantom scheduling instead of jest fake timers, and asserted against the real native animated backend rather than mocking it. - Replaced `react-test-renderer` rendering with Fantom `createRoot` + `runTask`, asserting real Fabric output / element refs. Four Animated tests intentionally remain on Jest because they depend on capabilities Fantom does not provide (jest fake timers and/or module mocks of the native animated module): `Animated`, `Animated-web`, `AnimatedNative`, and `NativeAnimatedAllowlist`. Changelog: [Internal] Differential Revision: D108759083
1 parent cfaf090 commit 17a89d7

12 files changed

Lines changed: 161 additions & 144 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
13+
import AnimatedImplementation from '../AnimatedImplementation';
14+
import AnimatedMock from '../AnimatedMock';
15+
16+
describe('Animated Mock', () => {
17+
it('matches implementation keys', () => {
18+
expect(Object.keys(AnimatedMock)).toEqual(
19+
Object.keys(AnimatedImplementation),
20+
);
21+
});
22+
it('matches implementation params', () => {
23+
Object.keys(AnimatedImplementation).forEach(key => {
24+
if (AnimatedImplementation[key].length !== AnimatedMock[key].length) {
25+
throw new Error(
26+
'key ' +
27+
key +
28+
' had different lengths: ' +
29+
JSON.stringify(
30+
{
31+
impl: {
32+
len: AnimatedImplementation[key].length,
33+
type: typeof AnimatedImplementation[key],
34+
val: AnimatedImplementation[key].toString(),
35+
},
36+
mock: {
37+
len: AnimatedMock[key].length,
38+
type: typeof AnimatedMock[key],
39+
val: AnimatedMock[key].toString(),
40+
},
41+
},
42+
null,
43+
2,
44+
),
45+
);
46+
}
47+
});
48+
});
49+
});

packages/react-native/Libraries/Animated/__tests__/AnimatedMock-test.js

Lines changed: 0 additions & 52 deletions
This file was deleted.

packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js renamed to packages/react-native/Libraries/Animated/__tests__/AnimatedObject-itest.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,13 @@
88
* @format
99
*/
1010

11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
13+
import Animated from '../Animated';
14+
import AnimatedObject from '../nodes/AnimatedObject';
1115
import nullthrows from 'nullthrows';
1216

1317
describe('AnimatedObject', () => {
14-
let Animated;
15-
let AnimatedObject;
16-
17-
beforeEach(() => {
18-
jest.resetModules();
19-
20-
Animated = require('../Animated').default;
21-
AnimatedObject = require('../nodes/AnimatedObject').default;
22-
});
23-
2418
it('should get the proper value', () => {
2519
const anim = new Animated.Value(0);
2620
const translateAnim = anim.interpolate({

packages/react-native/Libraries/Animated/__tests__/AnimatedProps-test.js

Lines changed: 0 additions & 30 deletions
This file was deleted.

packages/react-native/Libraries/Animated/__tests__/AnimatedValue-test.js renamed to packages/react-native/Libraries/Animated/__tests__/AnimatedValue-itest.js

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,33 @@
88
* @format
99
*/
1010

11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
13+
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
14+
import AnimatedValue from '../nodes/AnimatedValue';
15+
1116
describe('AnimatedValue', () => {
12-
let NativeAnimatedHelper;
13-
let AnimatedValue;
17+
// Fantom uses the real native animated module and does not support
18+
// `jest.spyOn`, so we wrap the relevant `NativeAnimatedHelper.API` methods
19+
// with call-through mocks that count invocations and restore them afterwards.
20+
const restoreAPI: Array<() => void> = [];
21+
22+
function spyOnAPI(name: string) {
23+
// $FlowFixMe[invalid-computed-prop]
24+
const original = NativeAnimatedHelper.API[name];
25+
const spy = jest.fn((...args: Array<unknown>) =>
26+
original.apply(NativeAnimatedHelper.API, args),
27+
);
28+
// $FlowFixMe[prop-missing]
29+
// $FlowFixMe[cannot-write]
30+
NativeAnimatedHelper.API[name] = spy;
31+
restoreAPI.push(() => {
32+
// $FlowFixMe[prop-missing]
33+
// $FlowFixMe[cannot-write]
34+
NativeAnimatedHelper.API[name] = original;
35+
});
36+
return spy;
37+
}
1438

1539
function createNativeAnimatedValue(): AnimatedValue {
1640
return new AnimatedValue(0, {useNativeDriver: true});
@@ -32,31 +56,17 @@ describe('AnimatedValue', () => {
3256
}
3357

3458
beforeEach(() => {
35-
jest.resetModules();
36-
37-
jest.mock('../NativeAnimatedTurboModule', () => ({
38-
__esModule: true,
39-
default: {
40-
addListener: jest.fn(),
41-
createAnimatedNode: jest.fn(),
42-
dropAnimatedNode: jest.fn(),
43-
removeListeners: jest.fn(),
44-
startListeningToAnimatedNodeValue: jest.fn(),
45-
stopListeningToAnimatedNodeValue: jest.fn(),
46-
extractAnimatedNodeOffset: jest.fn(),
47-
// ...
48-
},
49-
}));
50-
51-
NativeAnimatedHelper =
52-
require('../../../src/private/animated/NativeAnimatedHelper').default;
53-
AnimatedValue = require('../nodes/AnimatedValue').default;
54-
55-
jest.spyOn(NativeAnimatedHelper.API, 'createAnimatedNode');
56-
jest.spyOn(NativeAnimatedHelper.API, 'dropAnimatedNode');
57-
jest.spyOn(NativeAnimatedHelper.API, 'startListeningToAnimatedNodeValue');
58-
jest.spyOn(NativeAnimatedHelper.API, 'setWaitingForIdentifier');
59-
jest.spyOn(NativeAnimatedHelper.API, 'unsetWaitingForIdentifier');
59+
spyOnAPI('createAnimatedNode');
60+
spyOnAPI('dropAnimatedNode');
61+
spyOnAPI('startListeningToAnimatedNodeValue');
62+
spyOnAPI('setWaitingForIdentifier');
63+
spyOnAPI('unsetWaitingForIdentifier');
64+
});
65+
66+
afterEach(() => {
67+
while (restoreAPI.length > 0) {
68+
restoreAPI.pop()?.();
69+
}
6070
});
6171

6272
it('emits update events for listeners added', () => {
@@ -217,15 +227,22 @@ describe('AnimatedValue', () => {
217227

218228
emitMockUpdate(node, 123, 50);
219229

220-
const spy = jest.spyOn(node, '__onAnimatedValueUpdateReceived');
230+
// $FlowFixMe[method-unbinding]
231+
const original = node.__onAnimatedValueUpdateReceived;
232+
const spy = jest.fn((...args: Array<unknown>) =>
233+
original.apply(node, args),
234+
);
235+
// $FlowFixMe[cannot-write]
236+
node.__onAnimatedValueUpdateReceived = spy;
221237

222238
const mockValue = 100;
223239
const mockOffset = 50;
224240

225241
emitMockUpdate(node, mockValue, mockOffset);
226242

227243
expect(spy).toHaveBeenCalledWith(mockValue, mockOffset);
228-
spy.mockRestore();
244+
// $FlowFixMe[cannot-write]
245+
node.__onAnimatedValueUpdateReceived = original;
229246
});
230247
});
231248
});

packages/react-native/Libraries/Animated/__tests__/Easing-test.js renamed to packages/react-native/Libraries/Animated/__tests__/Easing-itest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @format
99
*/
1010

11-
'use strict';
11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1212

1313
import Easing from '../Easing';
1414

packages/react-native/Libraries/Animated/__tests__/Interpolation-test.js renamed to packages/react-native/Libraries/Animated/__tests__/Interpolation-itest.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* @format
99
*/
1010

11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
1113
import type {
1214
InterpolationConfigSupportedOutputType,
1315
InterpolationConfigType,
@@ -33,6 +35,13 @@ function createInterpolation<T extends InterpolationConfigSupportedOutputType>(
3335
}
3436

3537
describe('Interpolation', () => {
38+
const originalConsoleWarn = console.warn;
39+
40+
afterEach(() => {
41+
// $FlowFixMe[cannot-write]
42+
console.warn = originalConsoleWarn;
43+
});
44+
3645
it('should work with defaults', () => {
3746
const interpolation = createInterpolation({
3847
inputRange: [0, 1],
@@ -365,7 +374,10 @@ describe('Interpolation', () => {
365374
});
366375

367376
it('should work with PlatformColor', () => {
368-
jest.spyOn(console, 'warn').mockImplementationOnce(() => {});
377+
const mockWarn = jest.fn();
378+
// $FlowFixMe[cannot-write]
379+
console.warn = mockWarn;
380+
369381
const interpolation = createInterpolation({
370382
inputRange: [0, 1],
371383
outputRange: [
@@ -381,28 +393,28 @@ describe('Interpolation', () => {
381393
expect(interpolation(2 / 3)).toStrictEqual(
382394
PlatformColor('@android:color/white'),
383395
);
384-
expect(console.warn).toBeCalledWith(
396+
expect(mockWarn).toBeCalledWith(
385397
'PlatformColor interpolation should happen natively, here we fallback to the closest color',
386398
);
387399
expect(interpolation(1)).toStrictEqual(
388400
PlatformColor('@android:color/darker_gray'),
389401
);
390402
});
391403

392-
it.each([
404+
for (const [label, outputRange, expected] of [
393405
['radians', ['1rad', '2rad'], [1, 2]],
394406
['degrees', ['90deg', '180deg'], [Math.PI / 2, Math.PI]],
395407
['numbers', [1024, Math.PI], [1024, Math.PI]],
396408
['unknown', ['5foo', '10foo'], ['5foo', '10foo']],
397-
])(
398-
'should convert %s to numbers in the native config',
399-
(_, outputRange, expected) => {
409+
]) {
410+
it(`should convert ${label} to numbers in the native config`, () => {
400411
const config = new AnimatedInterpolation(
401412
// $FlowFixMe[incompatible-type]
402413
{},
414+
// $FlowFixMe[incompatible-call]
403415
{inputRange: [0, 1], outputRange},
404416
).__getNativeConfig();
405417
expect(config.outputRange).toEqual(expected);
406-
},
407-
);
418+
});
419+
}
408420
});

packages/react-native/Libraries/Animated/__tests__/TimingAnimation-test.js renamed to packages/react-native/Libraries/Animated/__tests__/TimingAnimation-itest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @format
99
*/
1010

11-
'use strict';
11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1212

1313
import TimingAnimation from '../animations/TimingAnimation';
1414

packages/react-native/Libraries/Animated/__tests__/bezier-test.js renamed to packages/react-native/Libraries/Animated/__tests__/bezier-itest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* @copyright 2014-2015 Gaetan Renaudeau. MIT License.
1515
*/
1616

17-
'use strict';
17+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1818

1919
import bezier from '../bezier';
2020

packages/react-native/src/private/animated/__tests__/AnimatedProps-itest.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1313
import * as Fantom from '@react-native/fantom';
1414
import * as React from 'react';
1515
import {Animated} from 'react-native';
16+
import AnimatedProps from 'react-native/Libraries/Animated/nodes/AnimatedProps';
1617
import NativeAnimatedHelper from 'react-native/src/private/animated/NativeAnimatedHelper';
1718

1819
function mockNativeAnimatedHelperAPI() {
@@ -58,3 +59,22 @@ test('connects and disconnects views', () => {
5859
// TODO: investigate why previous task enqueues more tasks.
5960
Fantom.runWorkLoop();
6061
});
62+
63+
describe('AnimatedProps#__getValue', () => {
64+
function getValue(inputProps: {[string]: unknown}) {
65+
const animatedProps = new AnimatedProps(inputProps, jest.fn());
66+
return animatedProps.__getValue();
67+
}
68+
69+
test('returns original `style` if it has no nodes', () => {
70+
const style = {color: 'red'};
71+
expect(getValue({style}).style).toBe(style);
72+
});
73+
74+
test('returns original `style` for invalid style values', () => {
75+
const values = [undefined, null, function () {}, true, 123, 'foo'];
76+
for (const value of values) {
77+
expect(getValue({style: value})).toEqual({style: value});
78+
}
79+
});
80+
});

0 commit comments

Comments
 (0)