Skip to content

Commit 4e44ec3

Browse files
rubennortefacebook-github-bot
authored andcommitted
Add Fantom integration tests for core public APIs
Summary: Adds Fantom `-itest.js` coverage for maintained, public React Native APIs that were previously untested or thinly tested, driving both the JavaScript logic and the underlying cross-platform C++ through the public `react-native` package surface. New test files: - `Animated/__tests__/SpringAnimation-itest.js`, `DecayAnimation-itest.js`, `AnimatedColor-itest.js`, `AnimatedComposition-itest.js` — spring/decay animations (JS and native driver), `Animated.Color` value semantics, and the composition nodes (`add`/`subtract`/`multiply`/`divide`/`modulo`/`diffClamp`/tracking). - `Components/View/__tests__/View-nativeCSSParsing-itest.js` — string-valued CSS (color functions, transform, gradients) parsed via native CSS parsing. - `Pressability/__tests__/Pressability-touch-itest.js` — press in/out/press, long press, press delay, and responder termination via touch events and the deterministic timer mock. - `Interaction/__tests__/PanResponder-itest.js` — grant/move/release/terminate and gesture state across touch sequences. - `LayoutAnimation/__tests__/LayoutAnimation-itest.js` — `configureNext`/`create`/presets applying the resulting layout. - `Network/__tests__/Network-itest.js` — synchronous `XMLHttpRequest` lifecycle and validation. Extended test files: - `Components/TextInput/__tests__/TextInput-itest.js` — `onSelectionChange`/`onSubmitEditing`/`onKeyPress`/`onEndEditing` events. - `Lists/__tests__/FlatList-itest.js` — `onViewableItemsChanged` via `viewabilityConfig` and `viewabilityConfigCallbackPairs`. - `Text/__tests__/Text-itest.js` — `letterSpacing`, `lineHeight`, and `fontVariant` styles. The JS-driver decay test asserts that the animation completes (which exercises `onUpdate`) rather than asserting per-frame movement, since the `requestAnimationFrame`-based decay does not expose stable intermediate frame values under the test runner's virtual clock. This is a test-only change with no runtime or user-facing impact. Changelog: [Internal] Differential Revision: D110784436
1 parent 58ffd18 commit 4e44ec3

12 files changed

Lines changed: 1385 additions & 0 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 type {HostInstance} from 'react-native';
14+
15+
import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance';
16+
import * as Fantom from '@react-native/fantom';
17+
import {createRef} from 'react';
18+
import {Animated, useAnimatedColor} from 'react-native';
19+
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
20+
21+
describe('Animated.Color value semantics', () => {
22+
it('defaults to opaque black', () => {
23+
const color = new Animated.Color();
24+
expect(color.__getValue()).toBe('rgba(0, 0, 0, 1)');
25+
});
26+
27+
it('parses an rgba() string into channels', () => {
28+
const color = new Animated.Color('rgba(255, 128, 0, 1)');
29+
expect(color.__getValue()).toBe('rgba(255, 128, 0, 1)');
30+
expect(color.r.__getValue()).toBe(255);
31+
expect(color.g.__getValue()).toBe(128);
32+
expect(color.b.__getValue()).toBe(0);
33+
expect(color.a.__getValue()).toBe(1);
34+
});
35+
36+
it('parses a hex string into channels', () => {
37+
const color = new Animated.Color('#ff0000');
38+
expect(color.__getValue()).toBe('rgba(255, 0, 0, 1)');
39+
});
40+
41+
it('accepts an rgba object', () => {
42+
const color = new Animated.Color({r: 10, g: 20, b: 30, a: 0.5});
43+
expect(color.__getValue()).toBe('rgba(10, 20, 30, 0.5)');
44+
});
45+
46+
it('accepts individual AnimatedValues for each channel', () => {
47+
const color = new Animated.Color({
48+
r: new Animated.Value(1),
49+
g: new Animated.Value(2),
50+
b: new Animated.Value(3),
51+
a: new Animated.Value(1),
52+
});
53+
expect(color.__getValue()).toBe('rgba(1, 2, 3, 1)');
54+
});
55+
56+
it('updates all channels via setValue and notifies listeners', () => {
57+
const color = new Animated.Color('rgba(0, 0, 0, 1)');
58+
const listener = jest.fn();
59+
color.addListener(listener);
60+
61+
color.setValue({r: 5, g: 6, b: 7, a: 1});
62+
63+
expect(color.__getValue()).toBe('rgba(5, 6, 7, 1)');
64+
expect(listener).toHaveBeenCalled();
65+
});
66+
67+
it('applies and flattens an offset', () => {
68+
const color = new Animated.Color({r: 10, g: 10, b: 10, a: 1});
69+
color.setOffset({r: 5, g: 5, b: 5, a: 0});
70+
expect(color.__getValue()).toBe('rgba(15, 15, 15, 1)');
71+
72+
color.flattenOffset();
73+
expect(color.r.__getValue()).toBe(15);
74+
75+
color.setOffset({r: 1, g: 1, b: 1, a: 0});
76+
color.extractOffset();
77+
// extractOffset moves the base into the offset, leaving the composed value
78+
// unchanged.
79+
expect(color.__getValue()).toBe('rgba(16, 16, 16, 1)');
80+
});
81+
});
82+
83+
describe('Animated.Color rendering', () => {
84+
it('drives backgroundColor and animates to a target color', () => {
85+
let color: Animated.Color = new Animated.Color();
86+
const viewRef = createRef<HostInstance>();
87+
88+
function MyApp() {
89+
const animatedColor = useAnimatedColor('rgba(255, 0, 0, 1)');
90+
color = animatedColor;
91+
return (
92+
<Animated.View
93+
ref={viewRef}
94+
style={{width: 100, height: 100, backgroundColor: animatedColor}}
95+
/>
96+
);
97+
}
98+
99+
const root = Fantom.createRoot();
100+
Fantom.runTask(() => {
101+
root.render(<MyApp />);
102+
});
103+
104+
ensureInstance(viewRef.current, ReactNativeElement);
105+
106+
expect(
107+
root.getRenderedOutput({props: ['backgroundColor']}).toJSX(),
108+
).toEqual(<rn-view backgroundColor="rgba(255, 0, 0, 1)" />);
109+
110+
let finished = false;
111+
Fantom.runTask(() => {
112+
Animated.timing(color, {
113+
toValue: {r: 0, g: 0, b: 255, a: 1},
114+
duration: 100,
115+
useNativeDriver: false,
116+
}).start(result => {
117+
finished = result.finished;
118+
});
119+
});
120+
121+
Fantom.unstable_produceFramesForDuration(200);
122+
Fantom.runWorkLoop();
123+
124+
expect(finished).toBe(true);
125+
expect(color.__getValue()).toBe('rgba(0, 0, 255, 1)');
126+
});
127+
});
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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 type {HostInstance} from 'react-native';
14+
15+
import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance';
16+
import * as Fantom from '@react-native/fantom';
17+
import {createRef} from 'react';
18+
import {Animated, useAnimatedValue} from 'react-native';
19+
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
20+
21+
// Renders an <Animated.View> whose translateX is driven by `makeNode(base)` and
22+
// returns the base AnimatedValue plus the mounted element so tests can mutate the
23+
// base and observe the derived value on the shadow tree.
24+
function renderWithDerivedTranslateX(
25+
makeNode: (base: Animated.Value) => Animated.WithAnimatedValue<number>,
26+
): {base: Animated.Value, element: ReactNativeElement, root: Fantom.Root} {
27+
let base: Animated.Value = new Animated.Value(0);
28+
const viewRef = createRef<HostInstance>();
29+
30+
function MyApp() {
31+
const value = useAnimatedValue(0);
32+
base = value;
33+
return (
34+
<Animated.View
35+
ref={viewRef}
36+
style={[
37+
{width: 100, height: 100},
38+
{transform: [{translateX: makeNode(value)}]},
39+
]}
40+
/>
41+
);
42+
}
43+
44+
const root = Fantom.createRoot();
45+
Fantom.runTask(() => {
46+
root.render(<MyApp />);
47+
});
48+
49+
const element = ensureInstance(viewRef.current, ReactNativeElement);
50+
return {base, element, root};
51+
}
52+
53+
function getTranslateX(root: Fantom.Root): number {
54+
const output = root.getRenderedOutput({props: ['transform']}).toJSONObject();
55+
const transform = JSON.parse(output.props.transform);
56+
return transform[0].translateX;
57+
}
58+
59+
describe('Animated.subtract', () => {
60+
it('computes the difference of two values and reacts to updates', () => {
61+
const {base, root} = renderWithDerivedTranslateX(value =>
62+
Animated.subtract(100, value),
63+
);
64+
65+
expect(getTranslateX(root)).toBe(100);
66+
67+
Fantom.runTask(() => {
68+
base.setValue(30);
69+
});
70+
71+
expect(getTranslateX(root)).toBe(70);
72+
});
73+
});
74+
75+
describe('Animated.divide', () => {
76+
it('computes the quotient of two values and reacts to updates', () => {
77+
const {base, root} = renderWithDerivedTranslateX(value =>
78+
Animated.divide(value, 2),
79+
);
80+
81+
Fantom.runTask(() => {
82+
base.setValue(80);
83+
});
84+
85+
expect(getTranslateX(root)).toBe(40);
86+
});
87+
88+
it('returns 0 instead of Infinity when dividing by zero', () => {
89+
const node = Animated.divide(10, 0);
90+
expect(node.__getValue()).toBe(0);
91+
});
92+
});
93+
94+
describe('Animated.modulo', () => {
95+
it('wraps values into the [0, modulus) range for positive and negative inputs', () => {
96+
const {base, root} = renderWithDerivedTranslateX(value =>
97+
Animated.modulo(value, 10),
98+
);
99+
100+
Fantom.runTask(() => {
101+
base.setValue(25);
102+
});
103+
expect(getTranslateX(root)).toBe(5);
104+
105+
Fantom.runTask(() => {
106+
base.setValue(-3);
107+
});
108+
// ((-3 % 10) + 10) % 10 === 7
109+
expect(getTranslateX(root)).toBe(7);
110+
});
111+
});
112+
113+
describe('Animated.diffClamp', () => {
114+
// `AnimatedDiffClamp.__getValue()` mutates internal accumulator state on every
115+
// call, so it is driven directly here (rather than through the render/commit
116+
// pipeline, which may read the node an unpredictable number of times).
117+
it('clamps the accumulated delta between min and max', () => {
118+
const base = new Animated.Value(0);
119+
const clamped = Animated.diffClamp(base, 0, 20);
120+
121+
expect(clamped.__getValue()).toBe(0);
122+
123+
// Increase beyond max: 0 + 100 clamped to 20.
124+
base.setValue(100);
125+
expect(clamped.__getValue()).toBe(20);
126+
127+
// Decrease by 30: 20 + (70 - 100) = -10, clamped to 0.
128+
base.setValue(70);
129+
expect(clamped.__getValue()).toBe(0);
130+
131+
// Increase by 5: 0 + (75 - 70) = 5, within range.
132+
base.setValue(75);
133+
expect(clamped.__getValue()).toBe(5);
134+
});
135+
});
136+
137+
describe('Animated.add and Animated.multiply', () => {
138+
it('compose additions and multiplications', () => {
139+
const {base, root} = renderWithDerivedTranslateX(value =>
140+
Animated.add(Animated.multiply(value, 2), 10),
141+
);
142+
143+
Fantom.runTask(() => {
144+
base.setValue(20);
145+
});
146+
147+
// 20 * 2 + 10 = 50
148+
expect(getTranslateX(root)).toBe(50);
149+
});
150+
});
151+
152+
describe('Animated tracking (toValue is an AnimatedNode)', () => {
153+
it('follows the target value when animating toward another AnimatedValue', () => {
154+
let follower: Animated.Value = new Animated.Value(0);
155+
let leader: Animated.Value = new Animated.Value(0);
156+
const viewRef = createRef<HostInstance>();
157+
158+
function MyApp() {
159+
const followerValue = useAnimatedValue(0);
160+
const leaderValue = useAnimatedValue(0);
161+
follower = followerValue;
162+
leader = leaderValue;
163+
return (
164+
<Animated.View
165+
ref={viewRef}
166+
style={[
167+
{width: 100, height: 100},
168+
{transform: [{translateX: followerValue}]},
169+
]}
170+
/>
171+
);
172+
}
173+
174+
const root = Fantom.createRoot();
175+
Fantom.runTask(() => {
176+
root.render(<MyApp />);
177+
});
178+
179+
Fantom.runTask(() => {
180+
Animated.timing(follower, {
181+
toValue: leader,
182+
duration: 100,
183+
useNativeDriver: true,
184+
}).start();
185+
});
186+
187+
Fantom.runTask(() => {
188+
leader.setValue(50);
189+
});
190+
191+
Fantom.unstable_produceFramesForDuration(200);
192+
Fantom.runWorkLoop();
193+
194+
expect(getTranslateX(root)).toBeCloseTo(50, 1);
195+
});
196+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 DecayAnimation from '../animations/DecayAnimation';
14+
import * as Fantom from '@react-native/fantom';
15+
import {Animated} from 'react-native';
16+
17+
describe('Animated.decay', () => {
18+
it('builds a native decay config with the provided velocity and deceleration', () => {
19+
const withDefaults = new DecayAnimation({
20+
velocity: 2,
21+
useNativeDriver: false,
22+
});
23+
const defaultConfig = withDefaults.__getNativeAnimationConfig();
24+
expect(defaultConfig.type).toBe('decay');
25+
expect(defaultConfig.velocity).toBe(2);
26+
expect(defaultConfig.deceleration).toBe(0.998);
27+
28+
const custom = new DecayAnimation({
29+
velocity: -1,
30+
deceleration: 0.9,
31+
useNativeDriver: false,
32+
});
33+
const customConfig = custom.__getNativeAnimationConfig();
34+
expect(customConfig.velocity).toBe(-1);
35+
expect(customConfig.deceleration).toBe(0.9);
36+
});
37+
38+
it('runs the JS decay animation to completion', () => {
39+
const value = new Animated.Value(0);
40+
41+
let finished = false;
42+
Fantom.runTask(() => {
43+
Animated.decay(value, {
44+
velocity: 1,
45+
useNativeDriver: false,
46+
}).start(result => {
47+
finished = result.finished;
48+
});
49+
});
50+
51+
// Drive the `requestAnimationFrame`-based JS decay to rest. Fantom's virtual
52+
// clock does not deterministically expose intermediate frame values for the
53+
// JS driver, so we assert that the animation completes (which exercises
54+
// `onUpdate`) rather than asserting per-frame movement.
55+
Fantom.unstable_produceFramesForDuration(3000);
56+
57+
expect(finished).toBe(true);
58+
});
59+
});

0 commit comments

Comments
 (0)