Skip to content

Commit e81b645

Browse files
rubennortefacebook-github-bot
authored andcommitted
Add Fantom integration tests for core public APIs (#57466)
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. To make JavaScript-driven animations deterministic under the test runner, this also introduces a small timing indirection in `Animated`: - New module `Animated/AnimationTimingUtils.js` exports `getCurrentAnimationTime` (returns `Date.now()` by default) and `setAnimationTimeProvider` (overrides the time source; pass `null` to restore the default). - `TimingAnimation`, `SpringAnimation` and `DecayAnimation` now read the current time from `getCurrentAnimationTime()` instead of calling `Date.now()` directly. The default behavior is unchanged. With this, a test can override the provider with a clock that advances one frame per read, which drives the JS-driver animation at the same cadence as the native frame clock. The spring and decay tests use this to verify the actual animation curve (overshoot for an underdamped spring, exponential deceleration for decay), not just the final value, and to run a single unified code path for both the JS and native drivers. New test files: - `Animated/__tests__/SpringAnimation-itest.js`, `DecayAnimation-itest.js`, `AnimatedColor-itest.js`, `AnimatedComposition-itest.js` — spring/decay curve shape (both drivers), `Animated.Color` value semantics, and the composition nodes (`add`/`subtract`/`multiply`/`divide`/`modulo`/`diffClamp`/tracking). `AnimatedFantomTestUtils.js` holds the shared trajectory helper. - `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` across all `viewabilityConfig` options (`itemVisiblePercentThreshold`, `viewAreaCoveragePercentThreshold`, `minimumViewTime`, `waitForInteraction`). - `Text/__tests__/Text-itest.js` — `letterSpacing`, `lineHeight`, and `fontVariant` styles. The runtime behavior of `Animated` is unchanged; the new timing indirection defaults to `Date.now()`. Changelog: [Internal] Differential Revision: D110784436
1 parent cb0cf07 commit e81b645

17 files changed

Lines changed: 1996 additions & 28 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
'use strict';
12+
13+
export type AnimationTimeProvider = () => number;
14+
15+
const defaultAnimationTimeProvider: AnimationTimeProvider = () => Date.now();
16+
17+
let animationTimeProvider: AnimationTimeProvider = defaultAnimationTimeProvider;
18+
19+
/**
20+
* Returns the current time, in milliseconds, used to drive JavaScript-based
21+
* animations (timing, spring and decay).
22+
*
23+
* Defaults to `Date.now()`. The value can be overridden with
24+
* `setAnimationTimeProvider`, e.g. to drive animations from a controlled clock
25+
* in tests.
26+
*/
27+
export function getCurrentAnimationTime(): number {
28+
return animationTimeProvider();
29+
}
30+
31+
/**
32+
* Overrides the time source used by `getCurrentAnimationTime`. Pass `null` to restore
33+
* the default `Date.now()`-based provider.
34+
*/
35+
export function setAnimationTimeProvider(
36+
provider: ?AnimationTimeProvider,
37+
): void {
38+
animationTimeProvider = provider ?? defaultAnimationTimeProvider;
39+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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 {DRIVERS} from './AnimatedFantomTestUtils';
14+
import * as Fantom from '@react-native/fantom';
15+
import * as React from 'react';
16+
import {Animated} from 'react-native';
17+
18+
// Renders `color` as a View's backgroundColor so it can be observed through the
19+
// public rendered output rather than private state.
20+
function renderColor(color: Animated.Color): Fantom.Root {
21+
const root = Fantom.createRoot();
22+
Fantom.runTask(() => {
23+
root.render(
24+
<Animated.View style={{width: 10, height: 10, backgroundColor: color}} />,
25+
);
26+
});
27+
return root;
28+
}
29+
30+
function mountedBackgroundColor(color: Animated.Color): string {
31+
return renderColor(color)
32+
.getRenderedOutput({props: ['backgroundColor']})
33+
.toJSONObject().props.backgroundColor;
34+
}
35+
36+
describe('Animated.Color', () => {
37+
it('defaults to opaque black', () => {
38+
expect(mountedBackgroundColor(new Animated.Color())).toBe(
39+
'rgba(0, 0, 0, 1)',
40+
);
41+
});
42+
43+
it('parses an rgba() string', () => {
44+
expect(
45+
mountedBackgroundColor(new Animated.Color('rgba(255, 128, 0, 1)')),
46+
).toBe('rgba(255, 128, 0, 1)');
47+
});
48+
49+
it('parses a hex string', () => {
50+
expect(mountedBackgroundColor(new Animated.Color('#ff0000'))).toBe(
51+
'rgba(255, 0, 0, 1)',
52+
);
53+
});
54+
55+
it('accepts an rgba object', () => {
56+
// The rendered color quantizes alpha to 8 bits (0.5 -> 128/255).
57+
expect(
58+
mountedBackgroundColor(new Animated.Color({r: 10, g: 20, b: 30, a: 0.5})),
59+
).toBe('rgba(10, 20, 30, 0.501961)');
60+
});
61+
62+
it('accepts individual AnimatedValues for each channel', () => {
63+
expect(
64+
mountedBackgroundColor(
65+
new Animated.Color({
66+
r: new Animated.Value(1),
67+
g: new Animated.Value(2),
68+
b: new Animated.Value(3),
69+
a: new Animated.Value(1),
70+
}),
71+
),
72+
).toBe('rgba(1, 2, 3, 1)');
73+
});
74+
75+
it('updates all channels via setValue', () => {
76+
const color = new Animated.Color('rgba(0, 0, 0, 1)');
77+
const root = renderColor(color);
78+
79+
Fantom.runTask(() => {
80+
color.setValue({r: 5, g: 6, b: 7, a: 1});
81+
});
82+
83+
expect(
84+
root.getRenderedOutput({props: ['backgroundColor']}).toJSX(),
85+
).toEqual(<rn-view backgroundColor="rgba(5, 6, 7, 1)" />);
86+
});
87+
88+
it('applies an offset on top of the base value', () => {
89+
const color = new Animated.Color({r: 10, g: 10, b: 10, a: 1});
90+
const root = renderColor(color);
91+
92+
// `setOffset` on its own does not flush to a connected view (unlike the
93+
// native driver, which does); the following value update flushes the
94+
// composed color, which includes the offset (base 20 + offset 5).
95+
Fantom.runTask(() => {
96+
color.setOffset({r: 5, g: 5, b: 5, a: 0});
97+
color.setValue({r: 20, g: 20, b: 20, a: 1});
98+
});
99+
100+
expect(
101+
root.getRenderedOutput({props: ['backgroundColor']}).toJSX(),
102+
).toEqual(<rn-view backgroundColor="rgba(25, 25, 25, 1)" />);
103+
});
104+
105+
it('flattenOffset and extractOffset preserve the composed value', () => {
106+
const color = new Animated.Color({r: 10, g: 10, b: 10, a: 1});
107+
renderColor(color);
108+
109+
let afterFlatten: string = '';
110+
let afterExtract: string = '';
111+
Fantom.runTask(() => {
112+
color.setOffset({r: 5, g: 5, b: 5, a: 0});
113+
// Merges the offset (5) into the base (10) -> base 15, offset 0.
114+
color.flattenOffset();
115+
color.stopAnimation(value => {
116+
afterFlatten = String(value);
117+
});
118+
// Moves the base (15) into the offset -> base 0, offset 15.
119+
color.extractOffset();
120+
color.stopAnimation(value => {
121+
afterExtract = String(value);
122+
});
123+
});
124+
125+
expect(afterFlatten).toBe('rgba(15, 15, 15, 1)');
126+
expect(afterExtract).toBe('rgba(15, 15, 15, 1)');
127+
});
128+
129+
it('resetAnimation restores the value and reports it to the callback', () => {
130+
const color = new Animated.Color('rgba(1, 2, 3, 1)');
131+
renderColor(color);
132+
133+
let reported: string = '';
134+
Fantom.runTask(() => {
135+
color.resetAnimation(value => {
136+
reported = String(value);
137+
});
138+
});
139+
140+
expect(reported).toBe('rgba(1, 2, 3, 1)');
141+
});
142+
143+
it('updates a native-driven color via setValue', () => {
144+
const color = new Animated.Color('rgba(0, 0, 0, 1)', {
145+
useNativeDriver: true,
146+
});
147+
renderColor(color);
148+
149+
// A native-driven color applies via direct manipulation rather than the
150+
// committed tree, so observe the value through the animation callback.
151+
let reported: string = '';
152+
Fantom.runTask(() => {
153+
color.setValue({r: 5, g: 6, b: 7, a: 1});
154+
color.stopAnimation(value => {
155+
reported = String(value);
156+
});
157+
});
158+
Fantom.unstable_produceFramesForDuration(16);
159+
Fantom.runWorkLoop();
160+
161+
expect(reported).toBe('rgba(5, 6, 7, 1)');
162+
});
163+
164+
for (const {name, useNativeDriver} of DRIVERS) {
165+
it(`animates to a target color (${name})`, () => {
166+
const color = new Animated.Color('rgba(255, 0, 0, 1)');
167+
const root = renderColor(color);
168+
169+
expect(
170+
root.getRenderedOutput({props: ['backgroundColor']}).toJSX(),
171+
).toEqual(<rn-view backgroundColor="rgba(255, 0, 0, 1)" />);
172+
173+
let finished = false;
174+
Fantom.runTask(() => {
175+
Animated.timing(color, {
176+
toValue: {r: 0, g: 0, b: 255, a: 1},
177+
duration: 100,
178+
useNativeDriver,
179+
}).start(result => {
180+
finished = result.finished;
181+
});
182+
});
183+
184+
Fantom.unstable_produceFramesForDuration(200);
185+
Fantom.runWorkLoop();
186+
187+
// The final driven color is flushed to the committed tree and observed
188+
// through the public rendered output.
189+
expect(finished).toBe(true);
190+
expect(
191+
root.getRenderedOutput({props: ['backgroundColor']}).toJSX(),
192+
).toEqual(<rn-view backgroundColor="rgba(0, 0, 255, 1)" />);
193+
});
194+
}
195+
});

0 commit comments

Comments
 (0)