Skip to content

Commit 1243679

Browse files
rubennortefacebook-github-bot
authored andcommitted
Export existing Fantom tests (2nd attempt) (#48118)
Summary: Pull Request resolved: #48118 Changelog: [internal] This is a re-land of #48085 Reviewed By: rshest Differential Revision: D66820308 fbshipit-source-id: b0ccd4b52965988015422ebdb8cd1172d1f5e9db
1 parent 18ebea5 commit 1243679

File tree

10 files changed

+5157
-0
lines changed

10 files changed

+5157
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
* @oncall react_native
10+
*/
11+
12+
import '../../../Core/InitializeCore.js';
13+
import * as ReactNativeTester from '../../../../src/private/__tests__/ReactNativeTester';
14+
import TextInput from '../TextInput';
15+
import * as React from 'react';
16+
import {useEffect, useLayoutEffect, useRef} from 'react';
17+
18+
describe('TextInput', () => {
19+
it('creates view before dispatching view command from ref function', () => {
20+
const root = ReactNativeTester.createRoot();
21+
22+
ReactNativeTester.runTask(() => {
23+
root.render(
24+
<TextInput
25+
ref={node => {
26+
if (node) {
27+
node.focus();
28+
}
29+
}}
30+
/>,
31+
);
32+
});
33+
34+
const mountingLogs = root.getMountingLogs();
35+
36+
expect(mountingLogs.length).toBe(2);
37+
expect(mountingLogs[0]).toBe('create view type: `AndroidTextInput`');
38+
expect(mountingLogs[1]).toBe(
39+
'dispatch command `focus` on component `AndroidTextInput`',
40+
);
41+
});
42+
43+
it('creates view before dispatching view command from useLayoutEffect', () => {
44+
const root = ReactNativeTester.createRoot();
45+
46+
function Component() {
47+
const textInputRef = useRef<null | React.ElementRef<typeof TextInput>>(
48+
null,
49+
);
50+
51+
useLayoutEffect(() => {
52+
textInputRef.current?.focus();
53+
});
54+
55+
return <TextInput ref={textInputRef} />;
56+
}
57+
ReactNativeTester.runTask(() => {
58+
root.render(<Component />);
59+
});
60+
61+
const mountingLogs = root.getMountingLogs();
62+
63+
expect(mountingLogs.length).toBe(2);
64+
expect(mountingLogs[0]).toBe('create view type: `AndroidTextInput`');
65+
expect(mountingLogs[1]).toBe(
66+
'dispatch command `focus` on component `AndroidTextInput`',
67+
);
68+
});
69+
70+
it('creates view before dispatching view command from useEffect', () => {
71+
const root = ReactNativeTester.createRoot();
72+
73+
function Component() {
74+
const textInputRef = useRef<null | React.ElementRef<typeof TextInput>>(
75+
null,
76+
);
77+
78+
useEffect(() => {
79+
textInputRef.current?.focus();
80+
});
81+
82+
return <TextInput ref={textInputRef} />;
83+
}
84+
ReactNativeTester.runTask(() => {
85+
root.render(<Component />);
86+
});
87+
88+
const mountingLogs = root.getMountingLogs();
89+
90+
expect(mountingLogs.length).toBe(2);
91+
expect(mountingLogs[0]).toBe('create view type: `AndroidTextInput`');
92+
expect(mountingLogs[1]).toBe(
93+
'dispatch command `focus` on component `AndroidTextInput`',
94+
);
95+
});
96+
});
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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+
* @oncall react_native
10+
*/
11+
12+
import '../../Core/InitializeCore.js';
13+
import * as ReactNativeTester from '../../../src/private/__tests__/ReactNativeTester';
14+
import View from '../../Components/View/View';
15+
import * as React from 'react';
16+
import {Suspense, startTransition} from 'react';
17+
18+
let resolveFunction: (() => void) | null = null;
19+
20+
// This is a workaround for a bug to get the demo running.
21+
// TODO: replace with real implementation when the bug is fixed.
22+
// $FlowFixMe: [missing-local-annot]
23+
function use(promise) {
24+
if (promise.status === 'fulfilled') {
25+
return promise.value;
26+
} else if (promise.status === 'rejected') {
27+
throw promise.reason;
28+
} else if (promise.status === 'pending') {
29+
throw promise;
30+
} else {
31+
promise.status = 'pending';
32+
promise.then(
33+
result => {
34+
promise.status = 'fulfilled';
35+
promise.value = result;
36+
},
37+
reason => {
38+
promise.status = 'rejected';
39+
promise.reason = reason;
40+
},
41+
);
42+
throw promise;
43+
}
44+
}
45+
46+
type SquareData = {
47+
color: 'red' | 'green',
48+
};
49+
50+
enum SquareId {
51+
Green = 'green-square',
52+
Red = 'red-square',
53+
}
54+
55+
async function getGreenSquareData(): Promise<SquareData> {
56+
await new Promise(resolve => {
57+
resolveFunction = resolve;
58+
});
59+
return {
60+
color: 'green',
61+
};
62+
}
63+
64+
async function getRedSquareData(): Promise<SquareData> {
65+
await new Promise(resolve => {
66+
resolveFunction = resolve;
67+
});
68+
return {
69+
color: 'red',
70+
};
71+
}
72+
73+
const cache = new Map<SquareId, SquareData>();
74+
75+
async function getData(squareId: SquareId): Promise<SquareData> {
76+
switch (squareId) {
77+
case SquareId.Green:
78+
return await getGreenSquareData();
79+
case SquareId.Red:
80+
return await getRedSquareData();
81+
}
82+
}
83+
84+
async function fetchData(squareId: SquareId): Promise<SquareData> {
85+
const data = await getData(squareId);
86+
cache.set(squareId, data);
87+
return data;
88+
}
89+
90+
function Square(props: {squareId: SquareId}) {
91+
let data = cache.get(props.squareId);
92+
if (data == null) {
93+
data = use(fetchData(props.squareId));
94+
}
95+
return <View key={data.color} nativeID={'square with data: ' + data.color} />;
96+
}
97+
98+
function GreenSquare() {
99+
return <Square squareId={SquareId.Green} />;
100+
}
101+
102+
function RedSquare() {
103+
return <Square squareId={SquareId.Red} />;
104+
}
105+
106+
function Fallback() {
107+
return <View nativeID="suspense fallback" />;
108+
}
109+
110+
describe('Suspense', () => {
111+
it('shows fallback if data is not available', () => {
112+
cache.clear();
113+
const root = ReactNativeTester.createRoot();
114+
115+
ReactNativeTester.runTask(() => {
116+
root.render(
117+
<Suspense fallback={<Fallback />}>
118+
<GreenSquare />
119+
</Suspense>,
120+
);
121+
});
122+
123+
let mountingLogs = root.getMountingLogs();
124+
125+
expect(mountingLogs.length).toBe(1);
126+
expect(mountingLogs[0]).toBe(
127+
'create view type: `View` nativeId: `suspense fallback`',
128+
);
129+
130+
expect(resolveFunction).not.toBeNull();
131+
ReactNativeTester.runTask(() => {
132+
resolveFunction?.();
133+
resolveFunction = null;
134+
});
135+
136+
mountingLogs = root.getMountingLogs();
137+
138+
expect(mountingLogs.length).toBe(1);
139+
expect(mountingLogs[0]).toBe(
140+
'create view type: `View` nativeId: `square with data: green`',
141+
);
142+
143+
ReactNativeTester.runTask(() => {
144+
root.render(
145+
<Suspense fallback={<Fallback />}>
146+
<RedSquare />
147+
</Suspense>,
148+
);
149+
});
150+
151+
mountingLogs = root.getMountingLogs();
152+
153+
expect(mountingLogs.length).toBe(1);
154+
expect(mountingLogs[0]).toBe(
155+
'create view type: `View` nativeId: `suspense fallback`',
156+
);
157+
158+
expect(resolveFunction).not.toBeNull();
159+
ReactNativeTester.runTask(() => {
160+
resolveFunction?.();
161+
resolveFunction = null;
162+
});
163+
164+
mountingLogs = root.getMountingLogs();
165+
166+
expect(mountingLogs.length).toBe(1);
167+
expect(mountingLogs[0]).toBe(
168+
'create view type: `View` nativeId: `square with data: red`',
169+
);
170+
171+
ReactNativeTester.runTask(() => {
172+
root.render(
173+
<Suspense fallback={<Fallback />}>
174+
<GreenSquare />
175+
</Suspense>,
176+
);
177+
});
178+
179+
mountingLogs = root.getMountingLogs();
180+
181+
expect(mountingLogs.length).toBe(1);
182+
expect(mountingLogs[0]).toBe(
183+
'create view type: `View` nativeId: `square with data: green`',
184+
);
185+
186+
expect(resolveFunction).toBeNull();
187+
188+
root.destroy();
189+
});
190+
191+
// TODO(T207868872): this test only succeeds with enableFabricCompleteRootInCommitPhase enabled.
192+
// enableFabricCompleteRootInCommitPhase is hardcoded to true in the testing environment.
193+
it('shows stale data while transition is happening', () => {
194+
cache.clear();
195+
cache.set(SquareId.Green, {color: 'green'});
196+
197+
const root = ReactNativeTester.createRoot();
198+
199+
function App(props: {color: 'red' | 'green'}) {
200+
return (
201+
<Suspense fallback={<Fallback />}>
202+
{props.color === 'green' ? <GreenSquare /> : <RedSquare />}
203+
</Suspense>
204+
);
205+
}
206+
207+
ReactNativeTester.runTask(() => {
208+
root.render(<App color="green" />);
209+
});
210+
211+
let mountingLogs = root.getMountingLogs();
212+
213+
expect(mountingLogs.length).toBe(1);
214+
expect(mountingLogs[0]).toBe(
215+
'create view type: `View` nativeId: `square with data: green`',
216+
);
217+
218+
expect(resolveFunction).toBeNull();
219+
ReactNativeTester.runTask(() => {
220+
startTransition(() => {
221+
root.render(<App color="red" />);
222+
});
223+
});
224+
225+
mountingLogs = root.getMountingLogs();
226+
227+
// Green square is still mounted. Fallback is not shown to the user.
228+
expect(mountingLogs.length).toBe(0);
229+
230+
expect(resolveFunction).not.toBeNull();
231+
ReactNativeTester.runTask(() => {
232+
resolveFunction?.();
233+
resolveFunction = null;
234+
});
235+
236+
mountingLogs = root.getMountingLogs();
237+
238+
expect(mountingLogs.length).toBe(1);
239+
expect(mountingLogs[0]).toBe(
240+
'create view type: `View` nativeId: `square with data: red`',
241+
);
242+
243+
root.destroy();
244+
});
245+
});

0 commit comments

Comments
 (0)