main
js 416 lines 10.9 KB
Raw
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 * @emails react-core
8 * @jest-environment node
9 */
10 'use strict';
11
12 let React;
13 let ReactNoop;
14 let ReactNoopServer;
15 let Scheduler;
16 let act;
17 let advanceTimersByTime;
18 let assertLog;
19 let serverAct;
20 let waitFor;
21
22 describe('ReactOwnerStacks', () => {
23 beforeEach(function () {
24 let time = 10;
25 advanceTimersByTime = timeMS => {
26 jest.advanceTimersByTime(timeMS);
27 time += timeMS;
28 };
29
30 jest.spyOn(performance, 'timeOrigin', 'get').mockReturnValue(time);
31 jest.spyOn(performance, 'now').mockImplementation(() => {
32 return time++;
33 });
34
35 jest.resetModules();
36 React = require('react');
37 ReactNoop = require('react-noop-renderer');
38 ReactNoopServer = require('react-noop-renderer/server');
39 Scheduler = require('scheduler');
40 act = require('internal-test-utils').act;
41 assertLog = require('internal-test-utils').assertLog;
42 serverAct = require('internal-test-utils').serverAct;
43 waitFor = require('internal-test-utils').waitFor;
44 });
45
46 function normalizeCodeLocInfo(str) {
47 return (
48 str &&
49 str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
50 return '\n in ' + name + ' (at **)';
51 })
52 );
53 }
54
55 it('behavior in production', () => {
56 if (!__DEV__) {
57 if (gate('fb')) {
58 expect(React).toHaveProperty('captureOwnerStack', undefined);
59 } else {
60 expect(React).not.toHaveProperty('captureOwnerStack');
61 }
62 }
63 });
64
65 // @gate __DEV__
66 it('can get the component owner stacks during rendering in dev', async () => {
67 let stack;
68
69 function Foo() {
70 return <Bar />;
71 }
72 function Bar() {
73 return (
74 <div>
75 <Baz />
76 </div>
77 );
78 }
79 function Baz() {
80 stack = React.captureOwnerStack();
81 return <span>hi</span>;
82 }
83
84 await act(() => {
85 ReactNoop.render(
86 <div>
87 <Foo />
88 </div>,
89 );
90 });
91
92 expect(normalizeCodeLocInfo(stack)).toBe(
93 '\n in Bar (at **)' + '\n in Foo (at **)',
94 );
95 });
96
97 it('returns null outside of render', async () => {
98 // Awkward to gate since some builds will have `captureOwnerStack` return null in prod
99 if (__DEV__) {
100 expect(React.captureOwnerStack()).toBe(null);
101
102 await act(() => {
103 ReactNoop.render(<div />);
104 });
105
106 expect(React.captureOwnerStack()).toBe(null);
107 }
108 });
109
110 // @gate __DEV__
111 it('cuts off at the owner stack limit', async () => {
112 function App({siblingsBeforeStackOne}) {
113 const children = [];
114 for (
115 let i = 0;
116 i <
117 siblingsBeforeStackOne -
118 // <App /> callsite
119 1 -
120 // Stop so that OwnerStackOne will be right before cutoff
121 1;
122 i++
123 ) {
124 children.push(<Component key={i} />);
125 }
126 children.push(<OwnerStackOne key="stackOne" />);
127 children.push(<OwnerStackTwo key="stackTwo" />);
128
129 return children;
130 }
131
132 function Component() {
133 return null;
134 }
135
136 let stackOne;
137 function OwnerStackOne() {
138 stackOne = React.captureOwnerStack();
139 }
140
141 let stackTwo;
142 function OwnerStackTwo() {
143 stackTwo = React.captureOwnerStack();
144 }
145
146 await act(() => {
147 ReactNoop.render(
148 <App
149 key="one"
150 // Should be the value with of `ownerStackLimit` with `__VARIANT__` so that we see the cutoff
151 siblingsBeforeStackOne={500}
152 />,
153 );
154 });
155
156 expect({
157 pendingTimers: jest.getTimerCount(),
158 stackOne: normalizeCodeLocInfo(stackOne),
159 stackTwo: normalizeCodeLocInfo(stackTwo),
160 }).toEqual({
161 pendingTimers: 0,
162 stackOne: '\n in App (at **)',
163 stackTwo: __VARIANT__
164 ? // captured right after cutoff
165 '\n in UnknownOwner (at **)'
166 : // We never hit the limit outside __VARIANT__
167 '\n in App (at **)',
168 });
169
170 await act(() => {
171 ReactNoop.render(
172 <App
173 // TODO: Owner Stacks should update on re-render.
174 key="two"
175 siblingsBeforeStackOne={0}
176 />,
177 );
178 });
179
180 expect({
181 pendingTimers: jest.getTimerCount(),
182 stackOne: normalizeCodeLocInfo(stackOne),
183 stackTwo: normalizeCodeLocInfo(stackTwo),
184 }).toEqual({
185 pendingTimers: 0,
186 stackOne: __VARIANT__
187 ? // We re-rendered immediately so not enough time has ellapsed to reset the limit.
188 '\n in UnknownOwner (at **)'
189 : // We never hit the limit outside __VARIANT__
190 '\n in App (at **)',
191 stackTwo: __VARIANT__
192 ? // We re-rendered immediately so not enough time has ellapsed to reset the limit.
193 '\n in UnknownOwner (at **)'
194 : // We never hit the limit outside __VARIANT__
195 '\n in App (at **)',
196 });
197
198 // advance time so that we reset the limit
199 advanceTimersByTime(1001);
200
201 await act(() => {
202 ReactNoop.render(
203 <App
204 // TODO: Owner Stacks should update on re-render.
205 key="three"
206 // We reset after <App /> so we need to render one more
207 // to have similar cutoff as the initial render (key="one")
208 siblingsBeforeStackOne={501}
209 />,
210 );
211 });
212
213 expect({
214 pendingTimers: jest.getTimerCount(),
215 stackOne: normalizeCodeLocInfo(stackOne),
216 stackTwo: normalizeCodeLocInfo(stackTwo),
217 }).toEqual({
218 pendingTimers: 0,
219 stackOne: '\n in App (at **)',
220 stackTwo: __VARIANT__
221 ? // captured right after cutoff
222 '\n in UnknownOwner (at **)'
223 : // We never hit the limit outside __VARIANT__
224 '\n in App (at **)',
225 });
226 });
227
228 // @gate __DEV__
229 it('Fiber: resets the owner stack limit periodically', async () => {
230 function App({siblingsBeforeStackOne, timeout}) {
231 const children = [];
232 for (
233 let i = 0;
234 i <
235 siblingsBeforeStackOne -
236 // <App /> callsite
237 1 -
238 // Stop so that OwnerStackOne will be right before cutoff
239 1;
240 i++
241 ) {
242 children.push(<Component key={i} />);
243 }
244 children.push(<OwnerStackOne key="stackOne" />);
245 children.push(<OwnerStackDelayed key="stackTwo" timeout={timeout} />);
246
247 return children;
248 }
249
250 function Component() {
251 return null;
252 }
253
254 let stackOne;
255 function OwnerStackOne() {
256 Scheduler.log('render OwnerStackOne');
257 stackOne = React.captureOwnerStack();
258 }
259
260 let stackTwo;
261 function OwnerStackTwo() {
262 Scheduler.log('render OwnerStackTwo');
263 stackTwo = React.captureOwnerStack();
264 }
265 function OwnerStackDelayed({timeout}) {
266 Scheduler.log('render OwnerStackDelayed');
267 React.use(timeout);
268 return <OwnerStackTwo />;
269 }
270
271 React.startTransition(() => {
272 ReactNoop.render(
273 <App
274 key="one"
275 // Should be the value with of `ownerStackLimit` with `__VARIANT__` so that we see the cutoff
276 siblingsBeforeStackOne={500}
277 timeout={
278 new Promise(resolve =>
279 setTimeout(
280 resolve,
281 // Must be greater or equal then the reset interval
282 1000,
283 ),
284 )
285 }
286 />,
287 );
288 });
289
290 await waitFor(['render OwnerStackOne', 'render OwnerStackDelayed']);
291
292 expect({
293 pendingTimers: jest.getTimerCount(),
294 stackOne: normalizeCodeLocInfo(stackOne),
295 stackTwo: normalizeCodeLocInfo(stackTwo),
296 }).toEqual({
297 // 1 for the timeout
298 pendingTimers: 1,
299 stackOne: '\n in App (at **)',
300 stackTwo: undefined,
301 });
302
303 // resolve `timeout` Promise
304 advanceTimersByTime(1000);
305
306 await waitFor(['render OwnerStackDelayed', 'render OwnerStackTwo']);
307
308 expect({
309 pendingTimers: jest.getTimerCount(),
310 stackOne: normalizeCodeLocInfo(stackOne),
311 stackTwo: normalizeCodeLocInfo(stackTwo),
312 }).toEqual({
313 pendingTimers: 0,
314 stackOne: '\n in App (at **)',
315 stackTwo: __VARIANT__
316 ? // We don't reset in Fiber until we start a new render.
317 // Here we just continued after a ping.
318 '\n in UnknownOwner (at **)' + '\n in UnknownOwner (at **)'
319 : // We never hit the limit outside __VARIANT__
320 '\n in OwnerStackDelayed (at **)' + '\n in App (at **)',
321 });
322 });
323
324 // @gate __DEV__
325 it('Fizz: resets the owner stack limit periodically', async () => {
326 function App({siblingsBeforeStackOne, timeout}) {
327 const children = [];
328 for (
329 let i = 0;
330 i <
331 siblingsBeforeStackOne -
332 // <App /> callsite
333 1 -
334 // Stop so that OwnerStackOne will be right before cutoff
335 1;
336 i++
337 ) {
338 children.push(<Component key={i} />);
339 }
340 children.push(<OwnerStackOne key="stackOne" />);
341 children.push(<OwnerStackDelayed key="stackTwo" timeout={timeout} />);
342
343 return children;
344 }
345
346 function Component() {
347 return null;
348 }
349
350 let stackOne;
351 function OwnerStackOne() {
352 Scheduler.log('render OwnerStackOne');
353 stackOne = React.captureOwnerStack();
354 }
355
356 let stackTwo;
357 function OwnerStackTwo() {
358 Scheduler.log('render OwnerStackTwo');
359 stackTwo = React.captureOwnerStack();
360 }
361 function OwnerStackDelayed({timeout}) {
362 Scheduler.log('render OwnerStackDelayed');
363 React.use(timeout);
364 return <OwnerStackTwo />;
365 }
366
367 ReactNoopServer.render(
368 <App
369 key="one"
370 // Should be the value with of `ownerStackLimit` with `__VARIANT__` so that we see the cutoff
371 siblingsBeforeStackOne={500}
372 timeout={
373 new Promise(resolve =>
374 setTimeout(
375 resolve,
376 // Must be greater or equal then the reset interval
377 1000,
378 ),
379 )
380 }
381 />,
382 );
383
384 assertLog(['render OwnerStackOne', 'render OwnerStackDelayed']);
385
386 expect({
387 pendingTimers: jest.getTimerCount(),
388 stackOne: normalizeCodeLocInfo(stackOne),
389 stackTwo: normalizeCodeLocInfo(stackTwo),
390 }).toEqual({
391 // 1 for the timeout
392 pendingTimers: 1,
393 stackOne: '\n in App (at **)',
394 stackTwo: undefined,
395 });
396
397 await serverAct(() => {
398 advanceTimersByTime(1000);
399 });
400
401 expect({
402 pendingTimers: jest.getTimerCount(),
403 stackOne: normalizeCodeLocInfo(stackOne),
404 stackTwo: normalizeCodeLocInfo(stackTwo),
405 }).toEqual({
406 pendingTimers: 0,
407 stackOne: '\n in App (at **)',
408 stackTwo: __VARIANT__
409 ? // We don't reset in Fiber until we start a new render.
410 // Here we just continued after a ping.
411 '\n in UnknownOwner (at **)' + '\n in UnknownOwner (at **)'
412 : // We never hit the limit outside __VARIANT__
413 '\n in OwnerStackDelayed (at **)' + '\n in App (at **)',
414 });
415 });
416 });