main
js 193 lines 5.57 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
11 'use strict';
12
13 if (typeof Blob === 'undefined') {
14 global.Blob = require('buffer').Blob;
15 }
16 if (typeof File === 'undefined' || typeof FormData === 'undefined') {
17 global.File = require('undici').File;
18 global.FormData = require('undici').FormData;
19 }
20
21 function normalizeCodeLocInfo(str) {
22 return (
23 str &&
24 str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
25 const dot = name.lastIndexOf('.');
26 if (dot !== -1) {
27 name = name.slice(dot + 1);
28 }
29 return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
30 })
31 );
32 }
33
34 let ReactServer;
35 let ReactNoopFlightServer;
36 let Scheduler;
37 let advanceTimersByTime;
38 let assertLog;
39 let assertConsoleErrorDev;
40
41 describe('ReactFlight', () => {
42 beforeEach(() => {
43 // Mock performance.now for timing tests
44 let time = 0;
45 advanceTimersByTime = timeMS => {
46 time += timeMS;
47 jest.advanceTimersByTime(timeMS);
48 };
49 jest.spyOn(performance, 'timeOrigin', 'get').mockReturnValue(time);
50 jest.spyOn(performance, 'now').mockImplementation(() => {
51 return time++;
52 });
53
54 jest.resetModules();
55 jest.mock('react', () => require('react/react.react-server'));
56 ReactServer = require('react');
57 ReactNoopFlightServer = require('react-noop-renderer/flight-server');
58 Scheduler = require('scheduler');
59 const InternalTestUtils = require('internal-test-utils');
60 assertLog = InternalTestUtils.assertLog;
61 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
62 });
63
64 afterEach(() => {
65 jest.restoreAllMocks();
66 });
67
68 // @gate __DEV__
69 it('resets the owner stack limit periodically', async () => {
70 function App({siblingsBeforeStackOne, timeout}) {
71 const children = [];
72 for (
73 let i = 0;
74 i <
75 siblingsBeforeStackOne -
76 // <App /> callsite
77 1 -
78 // Stop so that OwnerStackOne will be right before cutoff
79 1;
80 i++
81 ) {
82 children.push(ReactServer.createElement(Component, {key: i}));
83 }
84 children.push(
85 ReactServer.createElement(OwnerStackOne, {key: 'stackOne'}),
86 );
87 children.push(
88 ReactServer.createElement(OwnerStackDelayed, {
89 key: 'stackTwo',
90 timeout,
91 }),
92 );
93
94 return children;
95 }
96
97 function Component() {
98 return null;
99 }
100
101 let stackOne;
102 function OwnerStackOne() {
103 Scheduler.log('render OwnerStackOne');
104 stackOne = ReactServer.captureOwnerStack();
105 }
106
107 let stackTwo;
108 function OwnerStackTwo() {
109 Scheduler.log('render OwnerStackTwo');
110 stackTwo = ReactServer.captureOwnerStack();
111 }
112 function OwnerStackDelayed({timeout}) {
113 Scheduler.log('render OwnerStackDelayed');
114
115 // Owner Stacks start fresh after `await`.
116 // We need to sync delay to observe the reset limit behavior.
117 // TODO: Is that the right behavior? If you do stack + Ownst Stack you'd get `OwnerStackTwo` twice.
118 jest.advanceTimersByTime(timeout);
119
120 return ReactServer.createElement(OwnerStackTwo, {});
121 }
122
123 ReactNoopFlightServer.render(
124 ReactServer.createElement(App, {
125 key: 'one',
126 // Should be the value with of `ownerStackLimit` with `__VARIANT__` so that we see the cutoff
127 siblingsBeforeStackOne: 500,
128 // Must be greater or equal then the reset interval
129 timeout: 1000,
130 }),
131 );
132
133 assertLog([
134 'render OwnerStackOne',
135 'render OwnerStackDelayed',
136 'render OwnerStackTwo',
137 ]);
138
139 expect({
140 pendingTimers: jest.getTimerCount(),
141 stackOne: normalizeCodeLocInfo(stackOne),
142 stackTwo: normalizeCodeLocInfo(stackTwo),
143 }).toEqual({
144 pendingTimers: 0,
145 stackOne: '\n in App (at **)',
146 stackTwo: __VARIANT__
147 ? // Didn't advance timers yet to reset
148 '\n in UnknownOwner (at **)' + '\n in UnknownOwner (at **)'
149 : // We never hit the limit outside __VARIANT__
150 '\n in OwnerStackDelayed (at **)' + '\n in App (at **)',
151 });
152
153 // Ensure we reset the limit after the timeout
154 advanceTimersByTime(1000);
155 ReactNoopFlightServer.render(
156 ReactServer.createElement(App, {
157 key: 'two',
158 siblingsBeforeStackOne: 0,
159 timeout: 0,
160 }),
161 );
162
163 expect({
164 pendingTimers: jest.getTimerCount(),
165 stackOne: normalizeCodeLocInfo(stackOne),
166 stackTwo: normalizeCodeLocInfo(stackTwo),
167 }).toEqual({
168 pendingTimers: 0,
169 stackOne: '\n in App (at **)',
170 stackTwo: '\n in OwnerStackDelayed (at **)' + '\n in App (at **)',
171 });
172 });
173
174 it('logs an error when prod elements are rendered', async () => {
175 const element = ReactServer.createElement('span', {
176 key: 'one',
177 children: 'Free!',
178 });
179 ReactNoopFlightServer.render(
180 // bad clone
181 {...element},
182 );
183
184 assertConsoleErrorDev([
185 'Attempted to render <span key="one"> without development properties. This is not supported. It can happen if:' +
186 '\n- The element is created with a production version of React but rendered in development.' +
187 '\n- The element was cloned with a custom function instead of `React.cloneElement`.\n' +
188 "The props of this element may help locate this element: { children: 'Free!', [key]: [Getter] }",
189 "TypeError: Cannot read properties of undefined (reading 'stack')" +
190 '\n in <stack>',
191 ]);
192 });
193 });