Skip to content

Commit 8f15ba3

Browse files
committed
test
1 parent 9f588a3 commit 8f15ba3

File tree

1 file changed

+349
-0
lines changed

1 file changed

+349
-0
lines changed
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its 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+
* @emails react-core
8+
* @jest-environment node
9+
*/
10+
11+
let React;
12+
let ReactNoop;
13+
let Scheduler;
14+
let act;
15+
16+
let getCacheForType;
17+
let useState;
18+
let useTransition;
19+
let Suspense;
20+
let TracingMarker;
21+
let startTransition;
22+
23+
let caches;
24+
let seededCache;
25+
26+
describe('ReactInteractionTracing', () => {
27+
beforeEach(() => {
28+
jest.resetModules();
29+
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
30+
ReactFeatureFlags.enableTransitionTracing = true;
31+
32+
React = require('react');
33+
ReactNoop = require('react-noop-renderer');
34+
Scheduler = require('scheduler');
35+
36+
act = require('jest-react').act;
37+
38+
useState = React.useState;
39+
useTransition = React.useTransition;
40+
startTransition = React.startTransition;
41+
Suspense = React.Suspense;
42+
TracingMarker = React.unstable_TracingMarker;
43+
44+
getCacheForType = React.unstable_getCacheForType;
45+
46+
caches = [];
47+
seededCache = null;
48+
});
49+
50+
const onTransitionStart = jest.fn();
51+
const onTransitionProgress = jest.fn();
52+
const onTransitionIncomplete = jest.fn();
53+
const onTransitionComplete = jest.fn();
54+
55+
const onMarkerProgress = jest.fn();
56+
const onMarkerIncomplete = jest.fn();
57+
const onMarkerComplete = jest.fn();
58+
59+
const transitionCallbacks = {
60+
onTransitionStart: (name, startTime) => {
61+
onTransitionStart({name, startTime});
62+
},
63+
onTransitionProgress: (name, startTime, currentTime, pending) => {
64+
onTransitionProgress({
65+
name,
66+
startTime,
67+
currentTime,
68+
pending,
69+
});
70+
},
71+
onTransitionIncomplete: (name, startTime, deletions) => {
72+
onTransitionIncomplete({
73+
name,
74+
startTime,
75+
deletions,
76+
});
77+
},
78+
onTransitionComplete: (name, startTime, endTime) => {
79+
onTransitionComplete({
80+
name,
81+
startTime,
82+
endTime,
83+
});
84+
},
85+
onMarkerProgress: (name, marker, startTime, currentTime, pending) => {
86+
onMarkerProgress({
87+
name,
88+
marker,
89+
startTime,
90+
currentTime,
91+
pending,
92+
});
93+
},
94+
onMarkerIncomplete: (name, marker, startTime, deletions) => {
95+
onMarkerIncomplete({
96+
name,
97+
marker,
98+
startTime,
99+
deletions,
100+
});
101+
},
102+
onMarkerComplete: (name, marker, startTime, endTime) => {
103+
onMarkerComplete({
104+
name,
105+
marker,
106+
startTime,
107+
endTime,
108+
});
109+
},
110+
};
111+
112+
function createTextCache() {
113+
if (seededCache !== null) {
114+
const cache = seededCache;
115+
seededCache = null;
116+
return cache;
117+
}
118+
119+
const data = new Map();
120+
const cache = {
121+
data,
122+
resolve(text) {
123+
const record = data.get(text);
124+
125+
if (record === undefined) {
126+
const newRecord = {
127+
status: 'resolved',
128+
value: text,
129+
};
130+
data.set(text, newRecord);
131+
} else if (record.status === 'pending') {
132+
const thenable = record.value;
133+
record.status = 'resolved';
134+
record.value = text;
135+
thenable.pings.forEach(t => t());
136+
}
137+
},
138+
reject(text, error) {
139+
const record = data.get(text);
140+
if (record === undefined) {
141+
const newRecord = {
142+
status: 'rejected',
143+
value: error,
144+
};
145+
data.set(text, newRecord);
146+
} else if (record.status === 'pending') {
147+
const thenable = record.value;
148+
record.status = 'rejected';
149+
record.value = error;
150+
thenable.pings.forEach(t => t());
151+
}
152+
},
153+
};
154+
caches.push(cache);
155+
return cache;
156+
}
157+
158+
function readText(text) {
159+
const textCache = getCacheForType(createTextCache);
160+
const record = textCache.data.get(text);
161+
if (record !== undefined) {
162+
switch (record.status) {
163+
case 'pending':
164+
Scheduler.unstable_yieldValue(`Suspend [${text}]`);
165+
throw record.value;
166+
case 'rejected':
167+
Scheduler.unstable_yieldValue(`Error [${text}]`);
168+
throw record.value;
169+
case 'resolved':
170+
return record.value;
171+
}
172+
} else {
173+
Scheduler.unstable_yieldValue(`Suspend [${text}]`);
174+
175+
const thenable = {
176+
pings: [],
177+
then(resolve) {
178+
if (newRecord.status === 'pending') {
179+
thenable.pings.push(resolve);
180+
} else {
181+
Promise.resolve().then(() => resolve(newRecord.value));
182+
}
183+
},
184+
};
185+
186+
const newRecord = {
187+
status: 'pending',
188+
value: thenable,
189+
};
190+
textCache.data.set(text, newRecord);
191+
192+
throw thenable;
193+
}
194+
}
195+
196+
function AsyncText({text}) {
197+
const fullText = readText(text);
198+
Scheduler.unstable_yieldValue(fullText);
199+
return fullText;
200+
}
201+
202+
function Text({text}) {
203+
Scheduler.unstable_yieldValue(text);
204+
return text;
205+
}
206+
207+
function seedNextTextCache(text) {
208+
if (seededCache === null) {
209+
seededCache = createTextCache();
210+
}
211+
seededCache.resolve(text);
212+
}
213+
214+
function resolveMostRecentTextCache(text) {
215+
if (caches.length === 0) {
216+
throw Error('Cache does not exist');
217+
} else {
218+
// Resolve the most recently created cache. An older cache can by
219+
// resolved with `caches[index].resolve(text)`.
220+
caches[caches.length - 1].resolve(text);
221+
}
222+
}
223+
224+
const resolveText = resolveMostRecentTextCache;
225+
226+
function rejectMostRecentTextCache(text, error) {
227+
if (caches.length === 0) {
228+
throw Error('Cache does not exist.');
229+
} else {
230+
// Resolve the most recently created cache. An older cache can by
231+
// resolved with `caches[index].reject(text, error)`.
232+
caches[caches.length - 1].reject(text, error);
233+
}
234+
}
235+
236+
const rejectText = rejectMostRecentTextCache;
237+
238+
function advanceTimers(ms) {
239+
// Note: This advances Jest's virtual time but not React's. Use
240+
// ReactNoop.expire for that.
241+
if (typeof ms !== 'number') {
242+
throw new Error('Must specify ms');
243+
}
244+
jest.advanceTimersByTime(ms);
245+
// Wait until the end of the current tick
246+
// We cannot use a timer since we're faking them
247+
return Promise.resolve().then(() => {});
248+
}
249+
250+
fit('should correctly trace interactions for async roots', async () => {
251+
let navigateToPageTwo;
252+
function App() {
253+
const [navigate, setNavigate] = useState(false);
254+
navigateToPageTwo = () => {
255+
setNavigate(true);
256+
};
257+
258+
return (
259+
<div>
260+
{navigate ? (
261+
<TracingMarker name={'page loaded'}>
262+
<Suspense
263+
fallback={<Text text="Loading..." />}
264+
name="suspense page">
265+
<AsyncText text="Page Two" />
266+
</Suspense>
267+
</TracingMarker>
268+
) : (
269+
<Text text="Page One" />
270+
)}
271+
</div>
272+
);
273+
}
274+
275+
const root = ReactNoop.createRoot({transitionCallbacks});
276+
await act(async () => {
277+
root.render(<App />);
278+
ReactNoop.expire(1000);
279+
await advanceTimers(1000);
280+
281+
expect(Scheduler).toFlushAndYield(['Page One']);
282+
});
283+
284+
await act(async () => {
285+
startTransition(() => navigateToPageTwo(), {name: 'page transition'});
286+
287+
ReactNoop.expire(1000);
288+
await advanceTimers(1000);
289+
290+
expect(Scheduler).toFlushAndYield(['Suspend [Page Two]', 'Loading...']);
291+
292+
ReactNoop.expire(1000);
293+
await advanceTimers(1000);
294+
await resolveText('Page Two');
295+
296+
expect(Scheduler).toFlushAndYield(['Page Two']);
297+
});
298+
299+
expect(onTransitionStart).toHaveBeenCalledTimes(1);
300+
expect(onTransitionStart.mock.calls[0][0]).toEqual({
301+
name: 'page transition',
302+
startTime: 1000,
303+
});
304+
305+
expect(onTransitionProgress).toHaveBeenCalledTimes(2);
306+
expect(onTransitionProgress.mock.calls[0][0]).toEqual({
307+
name: 'page transition',
308+
startTime: 1000,
309+
currentTime: 2000,
310+
pending: [{name: 'suspense page'}],
311+
});
312+
expect(onTransitionProgress.mock.calls[1][0]).toEqual({
313+
name: 'page transition',
314+
startTime: 1000,
315+
currentTime: 3000,
316+
pending: [],
317+
});
318+
319+
expect(onMarkerProgress).toHaveBeenCalledTimes(2);
320+
expect(onMarkerProgress.mock.calls[0][0]).toEqual({
321+
name: 'page transition',
322+
marker: 'page loaded',
323+
startTime: 1000,
324+
currentTime: 2000,
325+
pending: [{name: 'suspense page'}],
326+
});
327+
expect(onMarkerProgress.mock.calls[1][0]).toEqual({
328+
name: 'page transition',
329+
marker: 'page loaded',
330+
startTime: 1000,
331+
currentTime: 3000,
332+
pending: [],
333+
});
334+
335+
expect(onTransitionComplete).toHaveBeenCalledTimes(1);
336+
expect(onTransitionComplete.mock.calls[0][0]).toEqual({
337+
name: 'page transition',
338+
startTime: 1000,
339+
endTime: 3000,
340+
});
341+
expect(onMarkerComplete).toHaveBeenCalledTimes(1);
342+
expect(onMarkerComplete.mock.calls[0][0]).toEqual({
343+
name: 'page transition',
344+
marker: 'page loaded',
345+
startTime: 1000,
346+
endTime: 3000,
347+
});
348+
});
349+
});

0 commit comments

Comments
 (0)