main
js 339 lines 8.87 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 * @flow
8 */
9
10 import {getVersionedRenderImplementation} from './utils';
11
12 describe('Fast Refresh', () => {
13 let React;
14 let ReactFreshRuntime;
15 let act;
16 let babel;
17 let exportsObj;
18 let freshPlugin;
19 let store;
20 let withErrorsOrWarningsIgnored;
21
22 beforeEach(() => {
23 global.IS_REACT_ACT_ENVIRONMENT = true;
24
25 exportsObj = undefined;
26
27 babel = require('@babel/core');
28 freshPlugin = require('react-refresh/babel');
29
30 store = global.store;
31
32 React = require('react');
33
34 ReactFreshRuntime = require('react-refresh/runtime');
35 ReactFreshRuntime.injectIntoGlobalHook(global);
36
37 const utils = require('./utils');
38 act = utils.act;
39 withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
40 });
41
42 const {render: renderImplementation, getContainer} =
43 getVersionedRenderImplementation();
44
45 function execute(source) {
46 const compiled = babel.transform(source, {
47 babelrc: false,
48 presets: ['@babel/react'],
49 plugins: [
50 [freshPlugin, {skipEnvCheck: true}],
51 '@babel/plugin-transform-modules-commonjs',
52 '@babel/plugin-transform-destructuring',
53 ].filter(Boolean),
54 }).code;
55 exportsObj = {};
56 // eslint-disable-next-line no-new-func
57 new Function(
58 'global',
59 'React',
60 'exports',
61 '$RefreshReg$',
62 '$RefreshSig$',
63 compiled,
64 )(global, React, exportsObj, $RefreshReg$, $RefreshSig$);
65 // Module systems will register exports as a fallback.
66 // This is useful for cases when e.g. a class is exported,
67 // and we don't want to propagate the update beyond this module.
68 $RefreshReg$(exportsObj.default, 'exports.default');
69 return exportsObj.default;
70 }
71
72 function render(source) {
73 const Component = execute(source);
74 act(() => {
75 renderImplementation(<Component />);
76 });
77 // Module initialization shouldn't be counted as a hot update.
78 expect(ReactFreshRuntime.performReactRefresh()).toBe(null);
79 }
80
81 function patch(source) {
82 const prevExports = exportsObj;
83 execute(source);
84 const nextExports = exportsObj;
85
86 // Check if exported families have changed.
87 // (In a real module system we'd do this for *all* exports.)
88 // For example, this can happen if you convert a class to a function.
89 // Or if you wrap something in a HOC.
90 const didExportsChange =
91 ReactFreshRuntime.getFamilyByType(prevExports.default) !==
92 ReactFreshRuntime.getFamilyByType(nextExports.default);
93 if (didExportsChange) {
94 // In a real module system, we would propagate such updates upwards,
95 // and re-execute modules that imported this one. (Just like if we edited them.)
96 // This makes adding/removing/renaming exports re-render references to them.
97 // Here, we'll just force a re-render using the newer type to emulate this.
98 const NextComponent = nextExports.default;
99 act(() => {
100 renderImplementation(<NextComponent />);
101 });
102 }
103 act(() => {
104 const result = ReactFreshRuntime.performReactRefresh();
105 if (!didExportsChange) {
106 // Normally we expect that some components got updated in our tests.
107 expect(result).not.toBe(null);
108 } else {
109 // However, we have tests where we convert functions to classes,
110 // and in those cases it's expected nothing would get updated.
111 // (Instead, the export change branch above would take care of it.)
112 }
113 });
114 expect(ReactFreshRuntime._getMountedRootCount()).toBe(1);
115 }
116
117 function $RefreshReg$(type, id) {
118 ReactFreshRuntime.register(type, id);
119 }
120
121 function $RefreshSig$() {
122 return ReactFreshRuntime.createSignatureFunctionForTransform();
123 }
124
125 // @reactVersion >= 16.9
126 it('should not break the DevTools store', () => {
127 render(`
128 function Parent() {
129 return <Child key="A" />;
130 };
131
132 function Child() {
133 return <div />;
134 };
135
136 export default Parent;
137 `);
138 expect(store).toMatchInlineSnapshot(`
139 [root]
140 ▾ <Parent>
141 <Child key="A">
142 `);
143
144 let element = getContainer().firstChild;
145 expect(getContainer().firstChild).not.toBe(null);
146
147 patch(`
148 function Parent() {
149 return <Child key="A" />;
150 };
151
152 function Child() {
153 return <div />;
154 };
155
156 export default Parent;
157 `);
158 expect(store).toMatchInlineSnapshot(`
159 [root]
160 ▾ <Parent>
161 <Child key="A">
162 `);
163
164 // State is preserved; this verifies that Fast Refresh is wired up.
165 expect(getContainer().firstChild).toBe(element);
166 element = getContainer().firstChild;
167
168 patch(`
169 function Parent() {
170 return <Child key="B" />;
171 };
172
173 function Child() {
174 return <div />;
175 };
176
177 export default Parent;
178 `);
179 expect(store).toMatchInlineSnapshot(`
180 [root]
181 ▾ <Parent>
182 <Child key="B">
183 `);
184
185 // State is reset because hooks changed.
186 expect(getContainer().firstChild).not.toBe(element);
187 });
188
189 // @reactVersion < 18.0
190 // @reactVersion >= 16.9
191 it('should not break when there are warnings in between patching (before post commit hook)', () => {
192 withErrorsOrWarningsIgnored(['Expected:'], () => {
193 render(`
194 const {useState} = React;
195
196 export default function Component() {
197 const [state, setState] = useState(1);
198 console.warn("Expected: warning during render");
199 return null;
200 }
201 `);
202 });
203 expect(store).toMatchInlineSnapshot(`
204 ✕ 0, ⚠ 1
205 [root]
206 <Component> ⚠
207 `);
208
209 withErrorsOrWarningsIgnored(['Expected:'], () => {
210 patch(`
211 const {useEffect, useState} = React;
212
213 export default function Component() {
214 const [state, setState] = useState(1);
215 console.warn("Expected: warning during render");
216 return null;
217 }
218 `);
219 });
220 expect(store).toMatchInlineSnapshot(`
221 ✕ 0, ⚠ 2
222 [root]
223 <Component> ⚠
224 `);
225
226 withErrorsOrWarningsIgnored(['Expected:'], () => {
227 patch(`
228 const {useEffect, useState} = React;
229
230 export default function Component() {
231 const [state, setState] = useState(1);
232 useEffect(() => {
233 console.error("Expected: error during effect");
234 });
235 console.warn("Expected: warning during render");
236 return null;
237 }
238 `);
239 });
240 expect(store).toMatchInlineSnapshot(`
241 ✕ 0, ⚠ 1
242 [root]
243 <Component> ⚠
244 `);
245
246 withErrorsOrWarningsIgnored(['Expected:'], () => {
247 patch(`
248 const {useEffect, useState} = React;
249
250 export default function Component() {
251 const [state, setState] = useState(1);
252 console.warn("Expected: warning during render");
253 return null;
254 }
255 `);
256 });
257 expect(store).toMatchInlineSnapshot(`
258 ✕ 0, ⚠ 1
259 [root]
260 <Component> ⚠
261 `);
262 });
263
264 // @reactVersion >= 18.0
265 it('should not break when there are warnings in between patching (with post commit hook)', () => {
266 withErrorsOrWarningsIgnored(['Expected:'], () => {
267 render(`
268 const {useState} = React;
269
270 export default function Component() {
271 const [state, setState] = useState(1);
272 console.warn("Expected: warning during render");
273 return null;
274 }
275 `);
276 });
277 expect(store).toMatchInlineSnapshot(`
278 ✕ 0, ⚠ 1
279 [root]
280 <Component> ⚠
281 `);
282
283 withErrorsOrWarningsIgnored(['Expected:'], () => {
284 patch(`
285 const {useEffect, useState} = React;
286
287 export default function Component() {
288 const [state, setState] = useState(1);
289 console.warn("Expected: warning during render");
290 return null;
291 }
292 `);
293 });
294 expect(store).toMatchInlineSnapshot(`
295 ✕ 0, ⚠ 2
296 [root]
297 <Component> ⚠
298 `);
299
300 withErrorsOrWarningsIgnored(['Expected:'], () => {
301 patch(`
302 const {useEffect, useState} = React;
303
304 export default function Component() {
305 const [state, setState] = useState(1);
306 useEffect(() => {
307 console.error("Expected: error during effect");
308 });
309 console.warn("Expected: warning during render");
310 return null;
311 }
312 `);
313 });
314 expect(store).toMatchInlineSnapshot(`
315 ✕ 1, ⚠ 1
316 [root]
317 <Component> ✕⚠
318 `);
319
320 withErrorsOrWarningsIgnored(['Expected:'], () => {
321 patch(`
322 const {useEffect, useState} = React;
323
324 export default function Component() {
325 const [state, setState] = useState(1);
326 console.warn("Expected: warning during render");
327 return null;
328 }
329 `);
330 });
331 expect(store).toMatchInlineSnapshot(`
332 ✕ 0, ⚠ 1
333 [root]
334 <Component> ⚠
335 `);
336 });
337
338 // TODO (bvaughn) Write a test that checks in between the steps of patch
339 });