@samitouri / QOS-React-2 / commits / 702fc984e6

Codemod act -> await act (4/?) (#26338)

Similar to the rationale for `waitFor` (see #26285), we should always await the result of an `act` call so that microtasks have a chance to fire. This only affects the internal `act` that we use in our repo, for now. In the public `act` API, we don't yet require this; however, we effectively will for any update that triggers suspense once `use` lands. So we likely will start warning in an upcoming minor.

Andrew Clark committed Mar 8, 2023 at 11:36 UTC 702fc984e68e852c76e0df608cd631a7c2e7763f
18 files changed +907 -1075
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+1 -1
@@ -984,7 +984,7 @@ describe('ReactHooksInspectionIntegration', () => {
984 children: ['count: ', '1'],
985 });
986
987 - act(incrementCount);
987 + await act(async () => incrementCount());
988 expect(renderer.toJSON()).toEqual({
989 type: 'div',
990 props: {},
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+20 -22
@@ -1069,8 +1069,8 @@ describe('InspectedElement', () => {
1069 });
1070
1071 async function loadPath(path) {
1072 - TestUtilsAct(() => {
1073 - TestRendererAct(() => {
1072 + await TestUtilsAct(async () => {
1073 + await TestRendererAct(async () => {
1074 inspectElementPath(path);
1075 jest.runOnlyPendingTimers();
1076 });
@@ -1224,8 +1224,8 @@ describe('InspectedElement', () => {
1224 });
1225
1226 async function loadPath(path) {
1227 - TestUtilsAct(() => {
1228 - TestRendererAct(() => {
1227 + await TestUtilsAct(async () => {
1228 + await TestRendererAct(async () => {
1229 inspectElementPath(path);
1230 jest.runOnlyPendingTimers();
1231 });
@@ -1306,8 +1306,8 @@ describe('InspectedElement', () => {
1306 });
1307
1308 async function loadPath(path) {
1309 - TestUtilsAct(() => {
1310 - TestRendererAct(() => {
1309 + await TestUtilsAct(async () => {
1310 + await TestRendererAct(async () => {
1311 inspectElementPath(path);
1312 jest.runOnlyPendingTimers();
1313 });
@@ -1375,8 +1375,8 @@ describe('InspectedElement', () => {
1375 }
1376 `);
1377
1378 - TestRendererAct(() => {
1379 - TestUtilsAct(() => {
1378 + await TestRendererAct(async () => {
1379 + await TestUtilsAct(async () => {
1380 legacyRender(
1381 <Example
1382 nestedObject={{
@@ -1469,8 +1469,8 @@ describe('InspectedElement', () => {
1469 });
1470
1471 async function loadPath(path) {
1472 - TestUtilsAct(() => {
1473 - TestRendererAct(() => {
1472 + await TestUtilsAct(async () => {
1473 + await TestRendererAct(async () => {
1474 inspectElementPath(path);
1475 jest.runOnlyPendingTimers();
1476 });
@@ -1513,8 +1513,8 @@ describe('InspectedElement', () => {
1513 }
1514 `);
1515
1516 - TestRendererAct(() => {
1517 - TestUtilsAct(() => {
1516 + await TestRendererAct(async () => {
1517 + await TestUtilsAct(async () => {
1518 legacyRender(
1519 <Example
1520 nestedObject={{
@@ -1596,8 +1596,8 @@ describe('InspectedElement', () => {
1596 });
1597
1598 async function loadPath(path) {
1599 - TestUtilsAct(() => {
1600 - TestRendererAct(() => {
1599 + await TestUtilsAct(async () => {
1600 + await TestRendererAct(async () => {
1601 inspectElementPath(path);
1602 jest.runOnlyPendingTimers();
1603 });
@@ -1618,7 +1618,7 @@ describe('InspectedElement', () => {
1618 }
1619 `);
1620
1621 - TestUtilsAct(() => {
1621 + await TestUtilsAct(async () => {
1622 legacyRender(
1623 <Example
1624 nestedObject={{
@@ -1640,11 +1640,9 @@ describe('InspectedElement', () => {
1640 expect(inspectedElement.props).toMatchInlineSnapshot(`
1641 {
1642 "nestedObject": {
1643 - "a": {
1644 - "b": {
1645 - "value": 2,
1646 - },
1647 - "value": 2,
1643 + "a": Dehydrated {
1644 + "preview_short": {…},
1645 + "preview_long": {b: {…}, value: 2},
1646 },
1647 "value": 2,
1648 },
@@ -2833,7 +2831,7 @@ describe('InspectedElement', () => {
2831 };
2832 const toggleError = async forceError => {
2833 await withErrorsOrWarningsIgnored(['ErrorBoundary'], async () => {
2836 - await TestUtilsAct(() => {
2834 + await TestUtilsAct(async () => {
2835 bridge.send('overrideError', {
2836 id: targetErrorBoundaryID,
2837 rendererID: store.getRendererIDForElement(targetErrorBoundaryID),
@@ -2842,7 +2840,7 @@ describe('InspectedElement', () => {
2840 });
2841 });
2842
2845 - TestUtilsAct(() => {
2843 + await TestUtilsAct(async () => {
2844 jest.runOnlyPendingTimers();
2845 });
2846 };
packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
+95 -76
@@ -19,10 +19,8 @@ describe('Store component filters', () => {
19 let utils;
20 let internalAct;
21
22 - const act = (callback: Function) => {
23 - internalAct(() => {
24 - callback();
25 - });
22 + const act = async (callback: Function) => {
23 + internalAct(callback);
24 jest.runAllTimers(); // Flush Bridge operations
25 };
26
@@ -42,15 +40,15 @@ describe('Store component filters', () => {
40 });
41
42 // @reactVersion >= 16.0
45 - it('should throw if filters are updated while profiling', () => {
46 - act(() => store.profilerStore.startProfiling());
43 + it('should throw if filters are updated while profiling', async () => {
44 + await act(async () => store.profilerStore.startProfiling());
45 expect(() => (store.componentFilters = [])).toThrow(
46 'Cannot modify filter preferences while profiling',
47 );
48 });
49
50 // @reactVersion >= 16.0
53 - it('should support filtering by element type', () => {
51 + it('should support filtering by element type', async () => {
52 class ClassComponent extends React.Component<{children: React$Node}> {
53 render() {
54 return <div>{this.props.children}</div>;
@@ -58,7 +56,7 @@ describe('Store component filters', () => {
56 }
57 const FunctionComponent = () => <div>Hi</div>;
58
61 - act(() =>
59 + await act(async () =>
60 legacyRender(
61 <ClassComponent>
62 <FunctionComponent />
@@ -74,8 +72,8 @@ describe('Store component filters', () => {
72 <div>
73 `);
74
77 - act(
78 - () =>
75 + await act(
76 + async () =>
77 (store.componentFilters = [
78 utils.createElementTypeFilter(Types.ElementTypeHostComponent),
79 ]),
@@ -86,8 +84,8 @@ describe('Store component filters', () => {
84 <FunctionComponent>
85 `);
86
89 - act(
90 - () =>
87 + await act(
88 + async () =>
89 (store.componentFilters = [
90 utils.createElementTypeFilter(Types.ElementTypeClass),
91 ]),
@@ -99,8 +97,8 @@ describe('Store component filters', () => {
97 <div>
98 `);
99
102 - act(
103 - () =>
100 + await act(
101 + async () =>
102 (store.componentFilters = [
103 utils.createElementTypeFilter(Types.ElementTypeClass),
104 utils.createElementTypeFilter(Types.ElementTypeFunction),
@@ -112,8 +110,8 @@ describe('Store component filters', () => {
110 <div>
111 `);
112
115 - act(
116 - () =>
113 + await act(
114 + async () =>
115 (store.componentFilters = [
116 utils.createElementTypeFilter(Types.ElementTypeClass, false),
117 utils.createElementTypeFilter(Types.ElementTypeFunction, false),
@@ -127,7 +125,7 @@ describe('Store component filters', () => {
125 <div>
126 `);
127
130 - act(() => (store.componentFilters = []));
128 + await act(async () => (store.componentFilters = []));
129 expect(store).toMatchInlineSnapshot(`
130 [root]
131 ▾ <ClassComponent>
@@ -138,18 +136,20 @@ describe('Store component filters', () => {
136 });
137
138 // @reactVersion >= 16.0
141 - it('should ignore invalid ElementTypeRoot filter', () => {
139 + it('should ignore invalid ElementTypeRoot filter', async () => {
140 const Component = () => <div>Hi</div>;
141
144 - act(() => legacyRender(<Component />, document.createElement('div')));
142 + await act(async () =>
143 + legacyRender(<Component />, document.createElement('div')),
144 + );
145 expect(store).toMatchInlineSnapshot(`
146 [root]
147 ▾ <Component>
148 <div>
149 `);
150
151 - act(
152 - () =>
151 + await act(
152 + async () =>
153 (store.componentFilters = [
154 utils.createElementTypeFilter(Types.ElementTypeRoot),
155 ]),
@@ -163,13 +163,13 @@ describe('Store component filters', () => {
163 });
164
165 // @reactVersion >= 16.2
166 - it('should filter by display name', () => {
166 + it('should filter by display name', async () => {
167 const Text = ({label}) => label;
168 const Foo = () => <Text label="foo" />;
169 const Bar = () => <Text label="bar" />;
170 const Baz = () => <Text label="baz" />;
171
172 - act(() =>
172 + await act(async () =>
173 legacyRender(
174 <React.Fragment>
175 <Foo />
@@ -189,8 +189,9 @@ describe('Store component filters', () => {
189 <Text>
190 `);
191
192 - act(
193 - () => (store.componentFilters = [utils.createDisplayNameFilter('Foo')]),
192 + await act(
193 + async () =>
194 + (store.componentFilters = [utils.createDisplayNameFilter('Foo')]),
195 );
196 expect(store).toMatchInlineSnapshot(`
197 [root]
@@ -201,7 +202,10 @@ describe('Store component filters', () => {
202 <Text>
203 `);
204
204 - act(() => (store.componentFilters = [utils.createDisplayNameFilter('Ba')]));
205 + await act(
206 + async () =>
207 + (store.componentFilters = [utils.createDisplayNameFilter('Ba')]),
208 + );
209 expect(store).toMatchInlineSnapshot(`
210 [root]
211 ▾ <Foo>
@@ -210,8 +214,9 @@ describe('Store component filters', () => {
214 <Text>
215 `);
216
213 - act(
214 - () => (store.componentFilters = [utils.createDisplayNameFilter('B.z')]),
217 + await act(
218 + async () =>
219 + (store.componentFilters = [utils.createDisplayNameFilter('B.z')]),
220 );
221 expect(store).toMatchInlineSnapshot(`
222 [root]
@@ -224,18 +229,20 @@ describe('Store component filters', () => {
229 });
230
231 // @reactVersion >= 16.0
227 - it('should filter by path', () => {
232 + it('should filter by path', async () => {
233 const Component = () => <div>Hi</div>;
234
230 - act(() => legacyRender(<Component />, document.createElement('div')));
235 + await act(async () =>
236 + legacyRender(<Component />, document.createElement('div')),
237 + );
238 expect(store).toMatchInlineSnapshot(`
239 [root]
240 ▾ <Component>
241 <div>
242 `);
243
237 - act(
238 - () =>
244 + await act(
245 + async () =>
246 (store.componentFilters = [
247 utils.createLocationFilter(__filename.replace(__dirname, '')),
248 ]),
@@ -243,8 +250,8 @@ describe('Store component filters', () => {
250
251 expect(store).toMatchInlineSnapshot(`[root]`);
252
246 - act(
247 - () =>
253 + await act(
254 + async () =>
255 (store.componentFilters = [
256 utils.createLocationFilter('this:is:a:made:up:path'),
257 ]),
@@ -258,14 +265,14 @@ describe('Store component filters', () => {
265 });
266
267 // @reactVersion >= 16.0
261 - it('should filter HOCs', () => {
268 + it('should filter HOCs', async () => {
269 const Component = () => <div>Hi</div>;
270 const Foo = () => <Component />;
271 Foo.displayName = 'Foo(Component)';
272 const Bar = () => <Foo />;
273 Bar.displayName = 'Bar(Foo(Component))';
274
268 - act(() => legacyRender(<Bar />, document.createElement('div')));
275 + await act(async () => legacyRender(<Bar />, document.createElement('div')));
276 expect(store).toMatchInlineSnapshot(`
277 [root]
278 ▾ <Component> [Bar][Foo]
@@ -274,14 +281,18 @@ describe('Store component filters', () => {
281 <div>
282 `);
283
277 - act(() => (store.componentFilters = [utils.createHOCFilter(true)]));
284 + await act(
285 + async () => (store.componentFilters = [utils.createHOCFilter(true)]),
286 + );
287 expect(store).toMatchInlineSnapshot(`
288 [root]
289 ▾ <Component>
290 <div>
291 `);
292
284 - act(() => (store.componentFilters = [utils.createHOCFilter(false)]));
293 + await act(
294 + async () => (store.componentFilters = [utils.createHOCFilter(false)]),
295 + );
296 expect(store).toMatchInlineSnapshot(`
297 [root]
298 ▾ <Component> [Bar][Foo]
@@ -292,29 +303,31 @@ describe('Store component filters', () => {
303 });
304
305 // @reactVersion >= 16.0
295 - it('should not send a bridge update if the set of enabled filters has not changed', () => {
296 - act(() => (store.componentFilters = [utils.createHOCFilter(true)]));
306 + it('should not send a bridge update if the set of enabled filters has not changed', async () => {
307 + await act(
308 + async () => (store.componentFilters = [utils.createHOCFilter(true)]),
309 + );
310
311 bridge.addListener('updateComponentFilters', componentFilters => {
312 throw Error('Unexpected component update');
313 });
314
302 - act(
303 - () =>
315 + await act(
316 + async () =>
317 (store.componentFilters = [
318 utils.createHOCFilter(false),
319 utils.createHOCFilter(true),
320 ]),
321 );
309 - act(
310 - () =>
322 + await act(
323 + async () =>
324 (store.componentFilters = [
325 utils.createHOCFilter(true),
326 utils.createLocationFilter('abc', false),
327 ]),
328 );
316 - act(
317 - () =>
329 + await act(
330 + async () =>
331 (store.componentFilters = [
332 utils.createHOCFilter(true),
333 utils.createElementTypeFilter(Types.ElementTypeHostComponent, false),
@@ -323,7 +336,7 @@ describe('Store component filters', () => {
336 });
337
338 // @reactVersion >= 18.0
326 - it('should not break when Suspense nodes are filtered from the tree', () => {
339 + it('should not break when Suspense nodes are filtered from the tree', async () => {
340 const promise = new Promise(() => {});
341
342 const Loading = () => <div>Loading...</div>;
@@ -346,7 +359,9 @@ describe('Store component filters', () => {
359 ];
360
361 const container = document.createElement('div');
349 - act(() => legacyRender(<Wrapper shouldSuspend={true} />, container));
362 + await act(async () =>
363 + legacyRender(<Wrapper shouldSuspend={true} />, container),
364 + );
365 expect(store).toMatchInlineSnapshot(`
366 [root]
367 ▾ <Wrapper>
@@ -354,15 +369,21 @@ describe('Store component filters', () => {
369 <div>
370 `);
371
357 - act(() => legacyRender(<Wrapper shouldSuspend={false} />, container));
372 + await act(async () =>
373 + legacyRender(<Wrapper shouldSuspend={false} />, container),
374 + );
375 expect(store).toMatchInlineSnapshot(`
376 + ✕ 1, ⚠ 0
377 [root]
378 ▾ <Wrapper>
379 <Component>
380 `);
381
364 - act(() => legacyRender(<Wrapper shouldSuspend={true} />, container));
382 + await act(async () =>
383 + legacyRender(<Wrapper shouldSuspend={true} />, container),
384 + );
385 expect(store).toMatchInlineSnapshot(`
386 + ✕ 2, ⚠ 0
387 [root]
388 ▾ <Wrapper>
389 ▾ <Loading>
@@ -372,7 +393,7 @@ describe('Store component filters', () => {
393
394 describe('inline errors and warnings', () => {
395 // @reactVersion >= 17.0
375 - it('only counts for unfiltered components', () => {
396 + it('only counts for unfiltered components', async () => {
397 function ComponentWithWarning() {
398 console.warn('test-only: render warning');
399 return null;
@@ -395,31 +416,29 @@ describe('Store component filters', () => {
416 require('react-dom');
417
418 const container = document.createElement('div');
419 + await act(
420 + async () =>
421 + (store.componentFilters = [
422 + utils.createDisplayNameFilter('Warning'),
423 + utils.createDisplayNameFilter('Error'),
424 + ]),
425 + );
426 utils.withErrorsOrWarningsIgnored(['test-only:'], () => {
399 - act(
400 - () =>
401 - (store.componentFilters = [
402 - utils.createDisplayNameFilter('Warning'),
403 - utils.createDisplayNameFilter('Error'),
404 - ]),
405 - );
406 - act(() =>
407 - legacyRender(
408 - <React.Fragment>
409 - <ComponentWithError />
410 - <ComponentWithWarning />
411 - <ComponentWithWarningAndError />
412 - </React.Fragment>,
413 - container,
414 - ),
427 + legacyRender(
428 + <React.Fragment>
429 + <ComponentWithError />
430 + <ComponentWithWarning />
431 + <ComponentWithWarningAndError />
432 + </React.Fragment>,
433 + container,
434 );
435 });
436
418 - expect(store).toMatchInlineSnapshot(`[root]`);
437 + expect(store).toMatchInlineSnapshot(``);
438 expect(store.errorCount).toBe(0);
439 expect(store.warningCount).toBe(0);
440
422 - act(() => (store.componentFilters = []));
441 + await act(async () => (store.componentFilters = []));
442 expect(store).toMatchInlineSnapshot(`
443 ✕ 2, ⚠ 2
444 [root]
@@ -428,8 +447,8 @@ describe('Store component filters', () => {
447 <ComponentWithWarningAndError> ✕⚠
448 `);
449
431 - act(
432 - () =>
450 + await act(
451 + async () =>
452 (store.componentFilters = [utils.createDisplayNameFilter('Warning')]),
453 );
454 expect(store).toMatchInlineSnapshot(`
@@ -438,8 +457,8 @@ describe('Store component filters', () => {
457 <ComponentWithError> ✕
458 `);
459
441 - act(
442 - () =>
460 + await act(
461 + async () =>
462 (store.componentFilters = [utils.createDisplayNameFilter('Error')]),
463 );
464 expect(store).toMatchInlineSnapshot(`
@@ -448,8 +467,8 @@ describe('Store component filters', () => {
467 <ComponentWithWarning> ⚠
468 `);
469
451 - act(
452 - () =>
470 + await act(
471 + async () =>
472 (store.componentFilters = [
473 utils.createDisplayNameFilter('Warning'),
474 utils.createDisplayNameFilter('Error'),
@@ -459,7 +478,7 @@ describe('Store component filters', () => {
478 expect(store.errorCount).toBe(0);
479 expect(store.warningCount).toBe(0);
480
462 - act(() => (store.componentFilters = []));
481 + await act(async () => (store.componentFilters = []));
482 expect(store).toMatchInlineSnapshot(`
483 ✕ 2, ⚠ 2
484 [root]
packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js
+10 -15
@@ -48,21 +48,16 @@ module.exports = function (initModules) {
48 // ====================================
49
50 // promisified version of ReactDOM.render()
51 - function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
52 - return new Promise(resolve => {
53 - if (forceHydrate) {
54 - act(() => {
55 - ReactDOM.hydrate(reactElement, domElement);
56 - });
57 - } else {
58 - act(() => {
59 - ReactDOM.render(reactElement, domElement);
60 - });
61 - }
62 - // We can't use the callback for resolution because that will not catch
63 - // errors. They're thrown.
64 - resolve();
65 - });
51 + async function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
52 + if (forceHydrate) {
53 + await act(async () => {
54 + ReactDOM.hydrate(reactElement, domElement);
55 + });
56 + } else {
57 + await act(async () => {
58 + ReactDOM.render(reactElement, domElement);
59 + });
60 + }
61 }
62 // performs fn asynchronously and expects count errors logged to console.error.
63 // will fail the test if the count of errors logged is not equal to count.
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+11 -11
@@ -2520,7 +2520,7 @@ describe('DOMPluginEventSystem', () => {
2520 });
2521
2522 // @gate www
2523 - it('beforeblur and afterblur are called after a focused element is suspended', () => {
2523 + it('beforeblur and afterblur are called after a focused element is suspended', async () => {
2524 const log = [];
2525 // We have to persist here because we want to read relatedTarget later.
2526 const onAfterBlur = jest.fn(e => {
@@ -2575,7 +2575,7 @@ describe('DOMPluginEventSystem', () => {
2575
2576 const root = ReactDOMClient.createRoot(container2);
2577
2578 - act(() => {
2578 + await act(async () => {
2579 root.render(<Component />);
2580 });
2581 jest.runAllTimers();
@@ -2587,7 +2587,7 @@ describe('DOMPluginEventSystem', () => {
2587 expect(onAfterBlur).toHaveBeenCalledTimes(0);
2588
2589 suspend = true;
2590 - act(() => {
2590 + await act(async () => {
2591 root.render(<Component />);
2592 });
2593 jest.runAllTimers();
@@ -2604,7 +2604,7 @@ describe('DOMPluginEventSystem', () => {
2604 });
2605
2606 // @gate www
2607 - it('beforeblur should skip handlers from a deleted subtree after the focused element is suspended', () => {
2607 + it('beforeblur should skip handlers from a deleted subtree after the focused element is suspended', async () => {
2608 const onBeforeBlur = jest.fn();
2609 const innerRef = React.createRef();
2610 const innerRef2 = React.createRef();
@@ -2661,7 +2661,7 @@ describe('DOMPluginEventSystem', () => {
2661
2662 const root = ReactDOMClient.createRoot(container2);
2663
2664 - act(() => {
2664 + await act(async () => {
2665 root.render(<Component />);
2666 });
2667 jest.runAllTimers();
@@ -2672,7 +2672,7 @@ describe('DOMPluginEventSystem', () => {
2672 expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2673
2674 suspend = true;
2675 - act(() => {
2675 + await act(async () => {
2676 root.render(<Component />);
2677 });
2678 jest.runAllTimers();
@@ -2684,17 +2684,17 @@ describe('DOMPluginEventSystem', () => {
2684 });
2685
2686 // @gate www
2687 - it('regression: does not fire beforeblur/afterblur if target is already hidden', () => {
2687 + it('regression: does not fire beforeblur/afterblur if target is already hidden', async () => {
2688 const Suspense = React.Suspense;
2689 let suspend = false;
2690 - const promise = Promise.resolve();
2690 + const fakePromise = {then() {}};
2691 const setBeforeBlurHandle =
2692 ReactDOM.unstable_createEventHandle('beforeblur');
2693 const innerRef = React.createRef();
2694
2695 function Child() {
2696 if (suspend) {
2697 - throw promise;
2697 + throw fakePromise;
2698 }
2699 return <input ref={innerRef} />;
2700 }
@@ -2726,7 +2726,7 @@ describe('DOMPluginEventSystem', () => {
2726 document.body.appendChild(container2);
2727
2728 const root = ReactDOMClient.createRoot(container2);
2729 - act(() => {
2729 + await act(async () => {
2730 root.render(<Component />);
2731 });
2732
@@ -2737,7 +2737,7 @@ describe('DOMPluginEventSystem', () => {
2737
2738 // Suspend. This hides the input node, causing it to lose focus.
2739 suspend = true;
2740 - act(() => {
2740 + await act(async () => {
2741 root.render(<Component />);
2742 });
2743
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+70 -71
@@ -20,6 +20,7 @@ let ReactDOMServer;
20 let act;
21 let assertLog;
22 let waitForAll;
23 +let waitForThrow;
24
25 describe('ReactHooks', () => {
26 beforeEach(() => {
@@ -36,6 +37,7 @@ describe('ReactHooks', () => {
37 const InternalTestUtils = require('internal-test-utils');
38 assertLog = InternalTestUtils.assertLog;
39 waitForAll = InternalTestUtils.waitForAll;
40 + waitForThrow = InternalTestUtils.waitForThrow;
41 });
42
43 if (__DEV__) {
@@ -90,7 +92,7 @@ describe('ReactHooks', () => {
92 expect(root).toMatchRenderedOutput('0, 0');
93
94 // Normal update
93 - act(() => {
95 + await act(async () => {
96 setCounter1(1);
97 setCounter2(1);
98 });
@@ -98,12 +100,12 @@ describe('ReactHooks', () => {
100 assertLog(['Parent: 1, 1', 'Child: 1, 1', 'Effect: 1, 1']);
101
102 // Update that bails out.
101 - act(() => setCounter1(1));
103 + await act(async () => setCounter1(1));
104 assertLog(['Parent: 1, 1']);
105
106 // This time, one of the state updates but the other one doesn't. So we
107 // can't bail out.
106 - act(() => {
108 + await act(async () => {
109 setCounter1(1);
110 setCounter2(2);
111 });
@@ -111,7 +113,7 @@ describe('ReactHooks', () => {
113 assertLog(['Parent: 1, 2', 'Child: 1, 2', 'Effect: 1, 2']);
114
115 // Lots of updates that eventually resolve to the current values.
114 - act(() => {
116 + await act(async () => {
117 setCounter1(9);
118 setCounter2(3);
119 setCounter1(4);
@@ -125,7 +127,7 @@ describe('ReactHooks', () => {
127 assertLog(['Parent: 1, 2']);
128
129 // prepare to check SameValue
128 - act(() => {
130 + await act(async () => {
131 setCounter1(0 / -1);
132 setCounter2(NaN);
133 });
@@ -133,7 +135,7 @@ describe('ReactHooks', () => {
135 assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
136
137 // check if re-setting to negative 0 / NaN still bails out
136 - act(() => {
138 + await act(async () => {
139 setCounter1(0 / -1);
140 setCounter2(NaN);
141 setCounter2(Infinity);
@@ -143,7 +145,7 @@ describe('ReactHooks', () => {
145 assertLog(['Parent: 0, NaN']);
146
147 // check if changing negative 0 to positive 0 does not bail out
146 - act(() => {
148 + await act(async () => {
149 setCounter1(0);
150 });
151 assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
@@ -178,7 +180,7 @@ describe('ReactHooks', () => {
180 expect(root).toMatchRenderedOutput('0, 0 (light)');
181
182 // Normal update
181 - act(() => {
183 + await act(async () => {
184 setCounter1(1);
185 setCounter2(1);
186 });
@@ -186,12 +188,12 @@ describe('ReactHooks', () => {
188 assertLog(['Parent: 1, 1 (light)', 'Child: 1, 1 (light)']);
189
190 // Update that bails out.
189 - act(() => setCounter1(1));
191 + await act(async () => setCounter1(1));
192 assertLog(['Parent: 1, 1 (light)']);
193
194 // This time, one of the state updates but the other one doesn't. So we
195 // can't bail out.
194 - act(() => {
196 + await act(async () => {
197 setCounter1(1);
198 setCounter2(2);
199 });
@@ -200,7 +202,7 @@ describe('ReactHooks', () => {
202
203 // Updates bail out, but component still renders because props
204 // have changed
203 - act(() => {
205 + await act(async () => {
206 setCounter1(1);
207 setCounter2(2);
208 root.update(<Parent theme="dark" />);
@@ -209,7 +211,7 @@ describe('ReactHooks', () => {
211 assertLog(['Parent: 1, 2 (dark)', 'Child: 1, 2 (dark)']);
212
213 // Both props and state bail out
212 - act(() => {
214 + await act(async () => {
215 setCounter1(1);
216 setCounter2(2);
217 root.update(<Parent theme="dark" />);
@@ -235,8 +237,8 @@ describe('ReactHooks', () => {
237 await waitForAll(['Count: 0']);
238 expect(root).toMatchRenderedOutput('0');
239
238 - expect(() => {
239 - act(() =>
240 + await expect(async () => {
241 + await act(async () =>
242 setCounter(1, () => {
243 throw new Error('Expected to ignore the callback.');
244 }),
@@ -269,8 +271,8 @@ describe('ReactHooks', () => {
271 await waitForAll(['Count: 0']);
272 expect(root).toMatchRenderedOutput('0');
273
272 - expect(() => {
273 - act(() =>
274 + await expect(async () => {
275 + await act(async () =>
276 dispatch(1, () => {
277 throw new Error('Expected to ignore the callback.');
278 }),
@@ -321,7 +323,7 @@ describe('ReactHooks', () => {
323 return <Child text={text} />;
324 }
325 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
324 - act(() => {
326 + await act(async () => {
327 root.update(
328 <ThemeProvider>
329 <Parent />
@@ -344,18 +346,18 @@ describe('ReactHooks', () => {
346 expect(root).toMatchRenderedOutput('0 (light)');
347
348 // Normal update
347 - act(() => setCounter(1));
349 + await act(async () => setCounter(1));
350 assertLog(['Parent: 1 (light)', 'Child: 1 (light)', 'Effect: 1 (light)']);
351 expect(root).toMatchRenderedOutput('1 (light)');
352
353 // Update that doesn't change state, so it bails out
352 - act(() => setCounter(1));
354 + await act(async () => setCounter(1));
355 assertLog(['Parent: 1 (light)']);
356 expect(root).toMatchRenderedOutput('1 (light)');
357
358 // Update that doesn't change state, but the context changes, too, so it
359 // can't bail out
358 - act(() => {
360 + await act(async () => {
361 setCounter(1);
362 setTheme('dark');
363 });
@@ -394,7 +396,7 @@ describe('ReactHooks', () => {
396 expect(root).toMatchRenderedOutput('0');
397
398 // Normal update
397 - act(() => setCounter(1));
399 + await act(async () => setCounter(1));
400 assertLog(['Parent: 1', 'Child: 1', 'Effect: 1']);
401 expect(root).toMatchRenderedOutput('1');
402
@@ -402,30 +404,30 @@ describe('ReactHooks', () => {
404 // because the alternate fiber has pending update priority, so we have to
405 // enter the render phase before we can bail out. But we bail out before
406 // rendering the child, and we don't fire any effects.
405 - act(() => setCounter(1));
407 + await act(async () => setCounter(1));
408 assertLog(['Parent: 1']);
409 expect(root).toMatchRenderedOutput('1');
410
411 // Update to the same state again. This times, neither fiber has pending
412 // update priority, so we can bail out before even entering the render phase.
411 - act(() => setCounter(1));
413 + await act(async () => setCounter(1));
414 await waitForAll([]);
415 expect(root).toMatchRenderedOutput('1');
416
417 // This changes the state to something different so it renders normally.
416 - act(() => setCounter(2));
418 + await act(async () => setCounter(2));
419 assertLog(['Parent: 2', 'Child: 2', 'Effect: 2']);
420 expect(root).toMatchRenderedOutput('2');
421
422 // prepare to check SameValue
421 - act(() => {
423 + await act(async () => {
424 setCounter(0);
425 });
426 assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
427 expect(root).toMatchRenderedOutput('0');
428
429 // Update to the same state for the first time to flush the queue
428 - act(() => {
430 + await act(async () => {
431 setCounter(0);
432 });
433
@@ -433,14 +435,14 @@ describe('ReactHooks', () => {
435 expect(root).toMatchRenderedOutput('0');
436
437 // Update again to the same state. Should bail out.
436 - act(() => {
438 + await act(async () => {
439 setCounter(0);
440 });
441 await waitForAll([]);
442 expect(root).toMatchRenderedOutput('0');
443
444 // Update to a different state (positive 0 to negative 0)
443 - act(() => {
445 + await act(async () => {
446 setCounter(0 / -1);
447 });
448 assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
@@ -615,7 +617,7 @@ describe('ReactHooks', () => {
617 ]);
618 });
619
618 - it('warns if deps is not an array', () => {
620 + it('warns if deps is not an array', async () => {
621 const {useEffect, useLayoutEffect, useMemo, useCallback} = React;
622
623 function App(props) {
@@ -626,8 +628,8 @@ describe('ReactHooks', () => {
628 return null;
629 }
630
629 - expect(() => {
630 - act(() => {
631 + await expect(async () => {
632 + await act(async () => {
633 ReactTestRenderer.create(<App deps={'hello'} />);
634 });
635 }).toErrorDev([
@@ -640,8 +642,8 @@ describe('ReactHooks', () => {
642 'Warning: useCallback received a final argument that is not an array (instead, received `string`). ' +
643 'When specified, the final argument must be an array.',
644 ]);
643 - expect(() => {
644 - act(() => {
645 + await expect(async () => {
646 + await act(async () => {
647 ReactTestRenderer.create(<App deps={100500} />);
648 });
649 }).toErrorDev([
@@ -654,8 +656,8 @@ describe('ReactHooks', () => {
656 'Warning: useCallback received a final argument that is not an array (instead, received `number`). ' +
657 'When specified, the final argument must be an array.',
658 ]);
657 - expect(() => {
658 - act(() => {
659 + await expect(async () => {
660 + await act(async () => {
661 ReactTestRenderer.create(<App deps={{}} />);
662 });
663 }).toErrorDev([
@@ -669,7 +671,7 @@ describe('ReactHooks', () => {
671 'When specified, the final argument must be an array.',
672 ]);
673
672 - act(() => {
674 + await act(async () => {
675 ReactTestRenderer.create(<App deps={[]} />);
676 ReactTestRenderer.create(<App deps={null} />);
677 ReactTestRenderer.create(<App deps={undefined} />);
@@ -695,7 +697,7 @@ describe('ReactHooks', () => {
697 ReactTestRenderer.create(<App deps={undefined} />);
698 });
699
698 - it('does not forget render phase useState updates inside an effect', () => {
700 + it('does not forget render phase useState updates inside an effect', async () => {
701 const {useState, useEffect} = React;
702
703 function Counter() {
@@ -712,13 +714,13 @@ describe('ReactHooks', () => {
714 }
715
716 const root = ReactTestRenderer.create(null);
715 - act(() => {
717 + await act(async () => {
718 root.update(<Counter />);
719 });
720 expect(root).toMatchRenderedOutput('4');
721 });
722
721 - it('does not forget render phase useReducer updates inside an effect with hoisted reducer', () => {
723 + it('does not forget render phase useReducer updates inside an effect with hoisted reducer', async () => {
724 const {useReducer, useEffect} = React;
725
726 const reducer = x => x + 1;
@@ -736,13 +738,13 @@ describe('ReactHooks', () => {
738 }
739
740 const root = ReactTestRenderer.create(null);
739 - act(() => {
741 + await act(async () => {
742 root.update(<Counter />);
743 });
744 expect(root).toMatchRenderedOutput('4');
745 });
746
745 - it('does not forget render phase useReducer updates inside an effect with inline reducer', () => {
747 + it('does not forget render phase useReducer updates inside an effect with inline reducer', async () => {
748 const {useReducer, useEffect} = React;
749
750 function Counter() {
@@ -759,7 +761,7 @@ describe('ReactHooks', () => {
761 }
762
763 const root = ReactTestRenderer.create(null);
762 - act(() => {
764 + await act(async () => {
765 root.update(<Counter />);
766 });
767 expect(root).toMatchRenderedOutput('4');
@@ -914,7 +916,7 @@ describe('ReactHooks', () => {
916 });
917
918 // Throws because there's no runtime cost for being strict here.
917 - it('throws when reading context inside useEffect', () => {
919 + it('throws when reading context inside useEffect', async () => {
920 const {useEffect, createContext} = React;
921 const ReactCurrentDispatcher =
922 React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
@@ -928,14 +930,11 @@ describe('ReactHooks', () => {
930 return null;
931 }
932
931 - expect(() => {
932 - act(() => {
933 - ReactTestRenderer.create(<App />);
934 - });
935 - }).toThrow(
933 + await act(async () => {
934 + ReactTestRenderer.create(<App />);
935 // The exact message doesn't matter, just make sure we don't allow this
937 - 'Context can only be read while React is rendering',
938 - );
936 + await waitForThrow('Context can only be read while React is rendering');
937 + });
938 });
939
940 // Throws because there's no runtime cost for being strict here.
@@ -1070,7 +1069,7 @@ describe('ReactHooks', () => {
1069 );
1070 });
1071
1073 - it('resets warning internal state when interrupted by an error', () => {
1072 + it('resets warning internal state when interrupted by an error', async () => {
1073 const ReactCurrentDispatcher =
1074 React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
1075 .ReactCurrentDispatcher;
@@ -1132,7 +1131,7 @@ describe('ReactHooks', () => {
1131 }
1132 // Verify it doesn't think we're still inside a Hook.
1133 // Should have no warnings.
1135 - act(() => {
1134 + await act(async () => {
1135 ReactTestRenderer.create(<Valid />);
1136 });
1137
@@ -1473,7 +1472,7 @@ describe('ReactHooks', () => {
1472 .replace('use', '')
1473 .replace('Helper', '');
1474
1476 - it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, () => {
1475 + it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, async () => {
1476 function App(props) {
1477 /* eslint-disable no-unused-vars */
1478 if (props.update) {
@@ -1489,12 +1488,12 @@ describe('ReactHooks', () => {
1488 /* eslint-enable no-unused-vars */
1489 }
1490 let root;
1492 - act(() => {
1491 + await act(async () => {
1492 root = ReactTestRenderer.create(<App update={false} />);
1493 });
1495 - expect(() => {
1494 + await expect(async () => {
1495 try {
1497 - act(() => {
1496 + await act(async () => {
1497 root.update(<App update={true} />);
1498 });
1499 } catch (error) {
@@ -1515,7 +1514,7 @@ describe('ReactHooks', () => {
1514
1515 // further warnings for this component are silenced
1516 try {
1518 - act(() => {
1517 + await act(async () => {
1518 root.update(<App update={false} />);
1519 });
1520 } catch (error) {
@@ -1525,7 +1524,7 @@ describe('ReactHooks', () => {
1524 }
1525 });
1526
1528 - it(`warns when more hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, () => {
1527 + it(`warns when more hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, async () => {
1528 function App(props) {
1529 /* eslint-disable no-unused-vars */
1530 if (props.update) {
@@ -1538,13 +1537,13 @@ describe('ReactHooks', () => {
1537 /* eslint-enable no-unused-vars */
1538 }
1539 let root;
1541 - act(() => {
1540 + await act(async () => {
1541 root = ReactTestRenderer.create(<App update={false} />);
1542 });
1543
1545 - expect(() => {
1544 + await expect(async () => {
1545 try {
1547 - act(() => {
1546 + await act(async () => {
1547 root.update(<App update={true} />);
1548 });
1549 } catch (error) {
@@ -1579,7 +1578,7 @@ describe('ReactHooks', () => {
1578 .replace('use', '')
1579 .replace('Helper', '');
1580
1582 - it(`warns when fewer hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, () => {
1581 + it(`warns when fewer hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, async () => {
1582 function App(props) {
1583 /* eslint-disable no-unused-vars */
1584 if (props.update) {
@@ -1592,15 +1591,15 @@ describe('ReactHooks', () => {
1591 /* eslint-enable no-unused-vars */
1592 }
1593 let root;
1595 - act(() => {
1594 + await act(async () => {
1595 root = ReactTestRenderer.create(<App update={false} />);
1596 });
1597
1599 - expect(() => {
1600 - act(() => {
1598 + await act(async () => {
1599 + expect(() => {
1600 root.update(<App update={true} />);
1602 - });
1603 - }).toThrow('Rendered fewer hooks than expected.');
1601 + }).toThrow('Rendered fewer hooks than expected. ');
1602 + });
1603 });
1604 });
1605
@@ -1726,7 +1725,7 @@ describe('ReactHooks', () => {
1725 });
1726
1727 // Regression test for https://github.com/facebook/react/issues/15057
1729 - it('does not fire a false positive warning when previous effect unmounts the component', () => {
1728 + it('does not fire a false positive warning when previous effect unmounts the component', async () => {
1729 const {useState, useEffect} = React;
1730 let globalListener;
1731
@@ -1762,7 +1761,7 @@ describe('ReactHooks', () => {
1761 return null;
1762 }
1763
1765 - act(() => {
1764 + await act(async () => {
1765 ReactTestRenderer.create(<A />);
1766 });
1767
@@ -1912,7 +1911,7 @@ describe('ReactHooks', () => {
1911 }
1912
1913 let root;
1915 - act(() => {
1914 + await act(async () => {
1915 root = ReactTestRenderer.create(
1916 <ErrorBoundary>
1917 <Thrower />
@@ -1921,7 +1920,7 @@ describe('ReactHooks', () => {
1920 });
1921
1922 expect(root).toMatchRenderedOutput('Throw!');
1924 - act(() => setShouldThrow(true));
1923 + await act(async () => setShouldThrow(true));
1924 expect(root).toMatchRenderedOutput('Error!');
1925 });
1926 });
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+106 -108
@@ -294,11 +294,11 @@ describe('ReactHooksWithNoopRenderer', () => {
294 await waitForAll(['Count: 0']);
295 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
296
297 - act(() => counter.current.updateCount(1));
297 + await act(async () => counter.current.updateCount(1));
298 assertLog(['Count: 1']);
299 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
300
301 - act(() => counter.current.updateCount(count => count + 10));
301 + await act(async () => counter.current.updateCount(count => count + 10));
302 assertLog(['Count: 11']);
303 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
304 });
@@ -318,7 +318,7 @@ describe('ReactHooksWithNoopRenderer', () => {
318 await waitForAll(['getInitialState', 'Count: 42']);
319 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 42" />);
320
321 - act(() => counter.current.updateCount(7));
321 + await act(async () => counter.current.updateCount(7));
322 assertLog(['Count: 7']);
323 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 7" />);
324 });
@@ -336,10 +336,10 @@ describe('ReactHooksWithNoopRenderer', () => {
336 await waitForAll(['Count: 0']);
337 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
338
339 - act(() => counter.current.updateCount(7));
339 + await act(async () => counter.current.updateCount(7));
340 assertLog(['Count: 7']);
341
342 - act(() => counter.current.updateLabel('Total'));
342 + await act(async () => counter.current.updateLabel('Total'));
343 assertLog(['Total: 7']);
344 });
345
@@ -356,13 +356,13 @@ describe('ReactHooksWithNoopRenderer', () => {
356
357 const firstUpdater = updater;
358
359 - act(() => firstUpdater(1));
359 + await act(async () => firstUpdater(1));
360 assertLog(['Count: 1']);
361 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
362
363 const secondUpdater = updater;
364
365 - act(() => firstUpdater(count => count + 10));
365 + await act(async () => firstUpdater(count => count + 10));
366 assertLog(['Count: 11']);
367 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
368
@@ -381,7 +381,7 @@ describe('ReactHooksWithNoopRenderer', () => {
381 await waitForAll([]);
382 ReactNoop.render(null);
383 await waitForAll([]);
384 - act(() => _updateCount(1));
384 + await act(async () => _updateCount(1));
385 });
386
387 it('works with memo', async () => {
@@ -401,7 +401,7 @@ describe('ReactHooksWithNoopRenderer', () => {
401 await waitForAll([]);
402 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
403
404 - act(() => _updateCount(1));
404 + await act(async () => _updateCount(1));
405 assertLog(['Count: 1']);
406 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
407 });
@@ -637,7 +637,7 @@ describe('ReactHooksWithNoopRenderer', () => {
637
638 // Test that it works on update, too. This time the log is a bit different
639 // because we started with reducerB instead of reducerA.
640 - act(() => {
640 + await act(async () => {
641 counter.current.dispatch('reset');
642 });
643 ReactNoop.render(<Counter ref={counter} />);
@@ -851,10 +851,10 @@ describe('ReactHooksWithNoopRenderer', () => {
851 await waitForAll(['Count: 0']);
852 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
853
854 - act(() => counter.current.dispatch(INCREMENT));
854 + await act(async () => counter.current.dispatch(INCREMENT));
855 assertLog(['Count: 1']);
856 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
857 - act(() => {
857 + await act(async () => {
858 counter.current.dispatch(DECREMENT);
859 counter.current.dispatch(DECREMENT);
860 counter.current.dispatch(DECREMENT);
@@ -893,11 +893,11 @@ describe('ReactHooksWithNoopRenderer', () => {
893 await waitForAll(['Init', 'Count: 10']);
894 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 10" />);
895
896 - act(() => counter.current.dispatch(INCREMENT));
896 + await act(async () => counter.current.dispatch(INCREMENT));
897 assertLog(['Count: 11']);
898 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
899
900 - act(() => {
900 + await act(async () => {
901 counter.current.dispatch(DECREMENT);
902 counter.current.dispatch(DECREMENT);
903 counter.current.dispatch(DECREMENT);
@@ -1427,7 +1427,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1427 return state;
1428 }
1429
1430 - act(() => {
1430 + await act(async () => {
1431 ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1432 Scheduler.log('Sync effect'),
1433 );
@@ -1437,7 +1437,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1437 ReactNoop.unmountRootWithID('root');
1438 await waitForAll(['passive destroy']);
1439
1440 - act(() => {
1440 + await act(async () => {
1441 updaterFunction(true);
1442 });
1443 });
@@ -1624,7 +1624,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1624 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
1625 // A flush sync doesn't cause the passive effects to fire.
1626 // So we haven't added the other update yet.
1627 - act(() => {
1627 + await act(async () => {
1628 ReactNoop.flushSync(() => {
1629 _updateCount(2);
1630 });
@@ -2256,7 +2256,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2256
2257 // @gate skipUnmountedBoundaries
2258 it('should use the nearest still-mounted boundary if there are no unmounted boundaries', async () => {
2259 - act(() => {
2259 + await act(async () => {
2260 ReactNoop.render(
2261 <LogOnlyErrorBoundary>
2262 <BrokenUseEffectCleanup />
@@ -2269,7 +2269,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2269 'BrokenUseEffectCleanup useEffect',
2270 ]);
2271
2272 - act(() => {
2272 + await act(async () => {
2273 ReactNoop.render(<LogOnlyErrorBoundary />);
2274 });
2275
@@ -2294,7 +2294,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2294 }
2295 }
2296
2297 - act(() => {
2297 + await act(async () => {
2298 ReactNoop.render(
2299 <LogOnlyErrorBoundary>
2300 <Conditional showChildren={true} />
@@ -2308,7 +2308,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2308 'BrokenUseEffectCleanup useEffect',
2309 ]);
2310
2311 - act(() => {
2311 + await act(async () => {
2312 ReactNoop.render(
2313 <LogOnlyErrorBoundary>
2314 <Conditional showChildren={false} />
@@ -2333,7 +2333,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2333 }
2334 }
2335
2336 - act(() => {
2336 + await act(async () => {
2337 ReactNoop.render(
2338 <ErrorBoundary>
2339 <Conditional showChildren={true} />
@@ -2346,7 +2346,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2346 'BrokenUseEffectCleanup useEffect',
2347 ]);
2348
2349 - act(() => {
2349 + await act(async () => {
2350 ReactNoop.render(
2351 <ErrorBoundary>
2352 <Conditional showChildren={false} />
@@ -2381,7 +2381,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2381 }
2382 }
2383
2384 - act(() => {
2384 + await act(async () => {
2385 ReactNoop.render(<Conditional showChildren={true} />);
2386 });
2387
@@ -2390,11 +2390,10 @@ describe('ReactHooksWithNoopRenderer', () => {
2390 'BrokenUseEffectCleanup useEffect',
2391 ]);
2392
2393 - expect(() => {
2394 - act(() => {
2395 - ReactNoop.render(<Conditional showChildren={false} />);
2396 - });
2397 - }).toThrow('Expected error');
2393 + await act(async () => {
2394 + ReactNoop.render(<Conditional showChildren={false} />);
2395 + await waitForThrow('Expected error');
2396 + });
2397
2398 assertLog(['BrokenUseEffectCleanup useEffect destroy']);
2399
@@ -2425,7 +2424,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2424 prevProps.prop === nextProps.prop;
2425 const MemoizedChild = React.memo(Child, isEqual);
2426
2428 - act(() => {
2427 + await act(async () => {
2428 ReactNoop.render(
2429 <Wrapper>
2430 <MemoizedChild key={1} />
@@ -2435,7 +2434,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2434 assertLog(['render', 'layout create', 'passive create']);
2435
2436 // Include at least one no-op (memoized) update to trigger original bug.
2438 - act(() => {
2437 + await act(async () => {
2438 ReactNoop.render(
2439 <Wrapper>
2440 <MemoizedChild key={1} />
@@ -2444,7 +2443,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2443 });
2444 assertLog([]);
2445
2447 - act(() => {
2446 + await act(async () => {
2447 ReactNoop.render(
2448 <Wrapper>
2449 <MemoizedChild key={2} />
@@ -2459,7 +2458,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2458 'passive create',
2459 ]);
2460
2462 - act(() => {
2461 + await act(async () => {
2462 ReactNoop.render(null);
2463 });
2464 assertLog(['layout destroy', 'passive destroy']);
@@ -2492,7 +2491,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2491 prevProps.prop === nextProps.prop;
2492 const MemoizedChild = React.memo(Child, isEqual);
2493
2495 - act(() => {
2494 + await act(async () => {
2495 ReactNoop.render(
2496 <Wrapper>
2497 <MemoizedChild key={1} />
@@ -2502,7 +2501,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2501 assertLog(['render', 'layout create', 'passive create']);
2502
2503 // Include at least one no-op (memoized) update to trigger original bug.
2505 - act(() => {
2504 + await act(async () => {
2505 ReactNoop.render(
2506 <Wrapper>
2507 <MemoizedChild key={1} />
@@ -2511,7 +2510,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2510 });
2511 assertLog([]);
2512
2514 - act(() => {
2513 + await act(async () => {
2514 ReactNoop.render(
2515 <Wrapper>
2516 <MemoizedChild key={2} />
@@ -2526,12 +2525,16 @@ describe('ReactHooksWithNoopRenderer', () => {
2525 'passive create',
2526 ]);
2527
2529 - act(() => {
2528 + await act(async () => {
2529 ReactNoop.render(null);
2530 });
2531 assertLog(['layout destroy', 'passive destroy']);
2532 });
2533
2534 + // TODO: This test fails when skipUnmountedBoundaries is disabled. However,
2535 + // it's also rolled out to open source already and partially to www. So
2536 + // we should probably just land it.
2537 + // @gate skipUnmountedBoundaries
2538 it('assumes passive effect destroy function is either a function or undefined', async () => {
2539 function App(props) {
2540 useEffect(() => {
@@ -2541,8 +2544,8 @@ describe('ReactHooksWithNoopRenderer', () => {
2544 }
2545
2546 const root1 = ReactNoop.createRoot();
2544 - expect(() => {
2545 - act(() => {
2547 + await expect(async () => {
2548 + await act(async () => {
2549 root1.render(<App return={17} />);
2550 });
2551 }).toErrorDev([
@@ -2551,8 +2554,8 @@ describe('ReactHooksWithNoopRenderer', () => {
2554 ]);
2555
2556 const root2 = ReactNoop.createRoot();
2554 - expect(() => {
2555 - act(() => {
2557 + await expect(async () => {
2558 + await act(async () => {
2559 root2.render(<App return={null} />);
2560 });
2561 }).toErrorDev([
@@ -2562,8 +2565,8 @@ describe('ReactHooksWithNoopRenderer', () => {
2565 ]);
2566
2567 const root3 = ReactNoop.createRoot();
2565 - expect(() => {
2566 - act(() => {
2568 + await expect(async () => {
2569 + await act(async () => {
2570 root3.render(<App return={Promise.resolve()} />);
2571 });
2572 }).toErrorDev([
@@ -2573,11 +2576,10 @@ describe('ReactHooksWithNoopRenderer', () => {
2576 ]);
2577
2578 // Error on unmount because React assumes the value is a function
2576 - expect(() =>
2577 - act(() => {
2578 - root3.unmount();
2579 - }),
2580 - ).toThrow('is not a function');
2579 + await act(async () => {
2580 + root3.render(null);
2581 + await waitForThrow('is not a function');
2582 + });
2583 });
2584 });
2585
@@ -2921,8 +2923,8 @@ describe('ReactHooksWithNoopRenderer', () => {
2923 }
2924
2925 const root1 = ReactNoop.createRoot();
2924 - expect(() => {
2925 - act(() => {
2926 + await expect(async () => {
2927 + await act(async () => {
2928 root1.render(<App return={17} />);
2929 });
2930 }).toErrorDev([
@@ -2931,8 +2933,8 @@ describe('ReactHooksWithNoopRenderer', () => {
2933 ]);
2934
2935 const root2 = ReactNoop.createRoot();
2934 - expect(() => {
2935 - act(() => {
2936 + await expect(async () => {
2937 + await act(async () => {
2938 root2.render(<App return={null} />);
2939 });
2940 }).toErrorDev([
@@ -2942,8 +2944,8 @@ describe('ReactHooksWithNoopRenderer', () => {
2944 ]);
2945
2946 const root3 = ReactNoop.createRoot();
2945 - expect(() => {
2946 - act(() => {
2947 + await expect(async () => {
2948 + await act(async () => {
2949 root3.render(<App return={Promise.resolve()} />);
2950 });
2951 }).toErrorDev([
@@ -2953,11 +2955,10 @@ describe('ReactHooksWithNoopRenderer', () => {
2955 ]);
2956
2957 // Error on unmount because React assumes the value is a function
2956 - expect(() =>
2957 - act(() => {
2958 - root3.unmount();
2959 - }),
2960 - ).toThrow('is not a function');
2958 + await act(async () => {
2959 + root3.render(null);
2960 + await waitForThrow('is not a function');
2961 + });
2962 });
2963
2964 it('warns when setState is called from insertion effect setup', async () => {
@@ -2973,17 +2974,16 @@ describe('ReactHooksWithNoopRenderer', () => {
2974 }
2975
2976 const root = ReactNoop.createRoot();
2976 - expect(() => {
2977 - act(() => {
2977 + await expect(async () => {
2978 + await act(async () => {
2979 root.render(<App />);
2980 });
2981 }).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
2982
2982 - expect(() => {
2983 - act(() => {
2984 - root.render(<App throw={true} />);
2985 - });
2986 - }).toThrow('No');
2983 + await act(async () => {
2984 + root.render(<App throw={true} />);
2985 + await waitForThrow('No');
2986 + });
2987
2988 // Should not warn for regular effects after throw.
2989 function NotInsertion() {
@@ -2993,7 +2993,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2993 }, []);
2994 return null;
2995 }
2996 - act(() => {
2996 + await act(async () => {
2997 root.render(<NotInsertion />);
2998 });
2999 });
@@ -3013,20 +3013,19 @@ describe('ReactHooksWithNoopRenderer', () => {
3013 }
3014
3015 const root = ReactNoop.createRoot();
3016 - act(() => {
3016 + await act(async () => {
3017 root.render(<App foo="hello" />);
3018 });
3019 - expect(() => {
3020 - act(() => {
3019 + await expect(async () => {
3020 + await act(async () => {
3021 root.render(<App foo="goodbye" />);
3022 });
3023 }).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
3024
3025 - expect(() => {
3026 - act(() => {
3027 - root.render(<App throw={true} />);
3028 - });
3029 - }).toThrow('No');
3025 + await act(async () => {
3026 + root.render(<App throw={true} />);
3027 + await waitForThrow('No');
3028 + });
3029
3030 // Should not warn for regular effects after throw.
3031 function NotInsertion() {
@@ -3036,7 +3035,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3035 }, []);
3036 return null;
3037 }
3039 - act(() => {
3038 + await act(async () => {
3039 root.render(<NotInsertion />);
3040 });
3041 });
@@ -3204,8 +3203,8 @@ describe('ReactHooksWithNoopRenderer', () => {
3203 }
3204
3205 const root1 = ReactNoop.createRoot();
3207 - expect(() => {
3208 - act(() => {
3206 + await expect(async () => {
3207 + await act(async () => {
3208 root1.render(<App return={17} />);
3209 });
3210 }).toErrorDev([
@@ -3214,8 +3213,8 @@ describe('ReactHooksWithNoopRenderer', () => {
3213 ]);
3214
3215 const root2 = ReactNoop.createRoot();
3217 - expect(() => {
3218 - act(() => {
3216 + await expect(async () => {
3217 + await act(async () => {
3218 root2.render(<App return={null} />);
3219 });
3220 }).toErrorDev([
@@ -3225,8 +3224,8 @@ describe('ReactHooksWithNoopRenderer', () => {
3224 ]);
3225
3226 const root3 = ReactNoop.createRoot();
3228 - expect(() => {
3229 - act(() => {
3227 + await expect(async () => {
3228 + await act(async () => {
3229 root3.render(<App return={Promise.resolve()} />);
3230 });
3231 }).toErrorDev([
@@ -3236,11 +3235,10 @@ describe('ReactHooksWithNoopRenderer', () => {
3235 ]);
3236
3237 // Error on unmount because React assumes the value is a function
3239 - expect(() =>
3240 - act(() => {
3241 - root3.unmount();
3242 - }),
3243 - ).toThrow('is not a function');
3238 + await act(async () => {
3239 + root3.render(null);
3240 + await waitForThrow('is not a function');
3241 + });
3242 });
3243 });
3244
@@ -3279,7 +3277,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3277 </>,
3278 );
3279
3282 - act(button.current.increment);
3280 + await act(async () => button.current.increment());
3281 assertLog([
3282 // Button should not re-render, because its props haven't changed
3283 // 'Increment',
@@ -3307,7 +3305,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3305 );
3306
3307 // Callback should have updated
3310 - act(button.current.increment);
3308 + await act(async () => button.current.increment());
3309 assertLog(['Count: 11']);
3310 expect(ReactNoop).toMatchRenderedOutput(
3311 <>
@@ -3425,7 +3423,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3423 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
3424 expect(counter.current.count).toBe(0);
3425
3428 - act(() => {
3426 + await act(async () => {
3427 counter.current.dispatch(INCREMENT);
3428 });
3429 assertLog(['Count: 1']);
@@ -3455,7 +3453,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3453 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
3454 expect(counter.current.count).toBe(0);
3455
3458 - act(() => {
3456 + await act(async () => {
3457 counter.current.dispatch(INCREMENT);
3458 });
3459 assertLog(['Count: 1']);
@@ -3492,7 +3490,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3490 expect(counter.current.count).toBe(0);
3491 expect(totalRefUpdates).toBe(1);
3492
3495 - act(() => {
3493 + await act(async () => {
3494 counter.current.dispatch(INCREMENT);
3495 });
3496 assertLog(['Count: 1']);
@@ -3591,7 +3589,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3589 );
3590 }
3591
3594 - act(() => {
3592 + await act(async () => {
3593 ReactNoop.render(<App />);
3594 });
3595
@@ -3689,7 +3687,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3687 <span prop="A: 0, B: 0, C: [not loaded]" />,
3688 );
3689
3692 - act(() => {
3690 + await act(async () => {
3691 updateA(2);
3692 updateB(3);
3693 });
@@ -3751,7 +3749,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3749 ReactNoop.render(<App loadC={true} />);
3750 await waitForAll(['A: 0, B: 0, C: 0']);
3751 expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 0, B: 0, C: 0" />);
3754 - act(() => {
3752 + await act(async () => {
3753 updateA(2);
3754 updateB(3);
3755 updateC(4);
@@ -3857,7 +3855,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3855 expect(ReactNoop).toMatchRenderedOutput('1');
3856 });
3857
3860 - act(() => {
3858 + await act(async () => {
3859 setCounter(2);
3860 });
3861 assertLog(['Render: 1', 'Effect: 2', 'Reducer: 2', 'Render: 2']);
@@ -3892,7 +3890,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3890 await waitForAll(['Render disabled: true', 'Render count: 0']);
3891 expect(ReactNoop).toMatchRenderedOutput('0');
3892
3895 - act(() => {
3893 + await act(async () => {
3894 // These increments should have no effect, since disabled=true
3895 increment();
3896 increment();
@@ -3901,7 +3899,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3899 assertLog(['Render disabled: true', 'Render count: 0']);
3900 expect(ReactNoop).toMatchRenderedOutput('0');
3901
3904 - act(() => {
3902 + await act(async () => {
3903 // Enabling the updater should *not* replay the previous increment() actions
3904 setDisabled(false);
3905 });
@@ -3941,7 +3939,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3939 await waitForAll(['Render disabled: true', 'Render count: 0']);
3940 expect(ReactNoop).toMatchRenderedOutput('0');
3941
3944 - act(() => {
3942 + await act(async () => {
3943 // These increments should have no effect, since disabled=true
3944 increment();
3945 increment();
@@ -3950,7 +3948,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3948 assertLog(['Render count: 0']);
3949 expect(ReactNoop).toMatchRenderedOutput('0');
3950
3953 - act(() => {
3951 + await act(async () => {
3952 // Enabling the updater should *not* replay the previous increment() actions
3953 setDisabled(false);
3954 });
@@ -3990,7 +3988,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3988 await waitForAll(['Render disabled: true', 'Render count: 0']);
3989 expect(ReactNoop).toMatchRenderedOutput('0');
3990
3993 - act(() => {
3991 + await act(async () => {
3992 // Although the increment happens first (and would seem to do nothing since disabled=true),
3993 // because these calls are in a batch the parent updates first. This should cause the child
3994 // to re-render with disabled=false and *then* process the increment action, which now
@@ -4085,7 +4083,7 @@ describe('ReactHooksWithNoopRenderer', () => {
4083 ]);
4084 expect(ReactNoop).toMatchRenderedOutput('0');
4085
4088 - act(() => dispatch());
4086 + await act(async () => dispatch());
4087 assertLog(['Step: 5, Shadow: 5']);
4088 expect(ReactNoop).toMatchRenderedOutput('5');
4089 });
@@ -4110,10 +4108,10 @@ describe('ReactHooksWithNoopRenderer', () => {
4108 return `${a ? 'A' : 'a'}${b ? 'B' : 'b'}${c ? 'C' : 'c'}`;
4109 }
4110
4113 - act(() => ReactNoop.render(<App />));
4111 + await act(async () => ReactNoop.render(<App />));
4112 expect(ReactNoop).toMatchRenderedOutput('abc');
4113
4116 - act(() => {
4114 + await act(async () => {
4115 updateA(true);
4116 // This update should not get dropped.
4117 updateC(true);
@@ -4282,25 +4280,25 @@ describe('ReactHooksWithNoopRenderer', () => {
4280 return <Text text={`Render: ${count}`} />;
4281 }
4282
4285 - act(() => {
4283 + await act(async () => {
4284 ReactNoop.render(<Test />);
4285 });
4286
4287 assertLog(['Render: 0', 'Effect: 0']);
4288
4291 - act(() => {
4289 + await act(async () => {
4290 handleClick();
4291 });
4292
4293 assertLog(['Render: 0']);
4294
4297 - act(() => {
4295 + await act(async () => {
4296 handleClick();
4297 });
4298
4299 assertLog(['Render: 0']);
4300
4303 - act(() => {
4301 + await act(async () => {
4302 handleClick();
4303 });
4304
packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js
+10 -10
@@ -32,8 +32,8 @@ describe('ReactOffscreenStrictMode', () => {
32 }
33
34 // @gate __DEV__ && enableOffscreen
35 - it('should trigger strict effects when offscreen is visible', () => {
36 - act(() => {
35 + it('should trigger strict effects when offscreen is visible', async () => {
36 + await act(async () => {
37 ReactNoop.render(
38 <React.StrictMode>
39 <Offscreen mode="visible">
@@ -56,8 +56,8 @@ describe('ReactOffscreenStrictMode', () => {
56 });
57
58 // @gate __DEV__ && enableOffscreen && useModernStrictMode
59 - it('should not trigger strict effects when offscreen is hidden', () => {
60 - act(() => {
59 + it('should not trigger strict effects when offscreen is hidden', async () => {
60 + await act(async () => {
61 ReactNoop.render(
62 <React.StrictMode>
63 <Offscreen mode="hidden">
@@ -71,7 +71,7 @@ describe('ReactOffscreenStrictMode', () => {
71
72 log = [];
73
74 - act(() => {
74 + await act(async () => {
75 ReactNoop.render(
76 <React.StrictMode>
77 <Offscreen mode="hidden">
@@ -86,7 +86,7 @@ describe('ReactOffscreenStrictMode', () => {
86
87 log = [];
88
89 - act(() => {
89 + await act(async () => {
90 ReactNoop.render(
91 <React.StrictMode>
92 <Offscreen mode="visible">
@@ -109,7 +109,7 @@ describe('ReactOffscreenStrictMode', () => {
109
110 log = [];
111
112 - act(() => {
112 + await act(async () => {
113 ReactNoop.render(
114 <React.StrictMode>
115 <Offscreen mode="hidden">
@@ -127,7 +127,7 @@ describe('ReactOffscreenStrictMode', () => {
127 ]);
128 });
129
130 - it('should not cause infinite render loop when StrictMode is used with Suspense and synchronous set states', () => {
130 + it('should not cause infinite render loop when StrictMode is used with Suspense and synchronous set states', async () => {
131 // This is a regression test, see https://github.com/facebook/react/pull/25179 for more details.
132 function App() {
133 const [state, setState] = React.useState(false);
@@ -143,7 +143,7 @@ describe('ReactOffscreenStrictMode', () => {
143 return state;
144 }
145
146 - act(() => {
146 + await act(async () => {
147 ReactNoop.render(
148 <React.StrictMode>
149 <React.Suspense>
@@ -193,7 +193,7 @@ describe('ReactOffscreenStrictMode', () => {
193 return null;
194 }
195
196 - act(() => {
196 + await act(async () => {
197 ReactNoop.render(
198 <React.StrictMode>
199 <Offscreen mode="visible">
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+162 -377
@@ -2,12 +2,9 @@ let React;
2 let ReactTestRenderer;
3 let ReactFeatureFlags;
4 let Scheduler;
5 -let ReactCache;
5 let Suspense;
6 let act;
8 -
9 -let TextResource;
10 -let textResourceShouldFail;
7 +let textCache;
8
9 let assertLog;
10 let waitForPaint;
@@ -24,7 +21,6 @@ describe('ReactSuspense', () => {
21 ReactTestRenderer = require('react-test-renderer');
22 act = require('jest-react').act;
23 Scheduler = require('scheduler');
27 - ReactCache = require('react-cache');
24
25 Suspense = React.Suspense;
26
@@ -34,73 +30,71 @@ describe('ReactSuspense', () => {
30 assertLog = InternalTestUtils.assertLog;
31 waitFor = InternalTestUtils.waitFor;
32
37 - TextResource = ReactCache.unstable_createResource(
38 - ([text, ms = 0]) => {
39 - let listeners = null;
40 - let status = 'pending';
41 - let value = null;
42 - return {
43 - then(resolve, reject) {
44 - switch (status) {
45 - case 'pending': {
46 - if (listeners === null) {
47 - listeners = [{resolve, reject}];
48 - setTimeout(() => {
49 - if (textResourceShouldFail) {
50 - Scheduler.log(`Promise rejected [${text}]`);
51 - status = 'rejected';
52 - value = new Error('Failed to load: ' + text);
53 - listeners.forEach(listener => listener.reject(value));
54 - } else {
55 - Scheduler.log(`Promise resolved [${text}]`);
56 - status = 'resolved';
57 - value = text;
58 - listeners.forEach(listener => listener.resolve(value));
59 - }
60 - }, ms);
61 - } else {
62 - listeners.push({resolve, reject});
63 - }
64 - break;
65 - }
66 - case 'resolved': {
67 - resolve(value);
68 - break;
69 - }
70 - case 'rejected': {
71 - reject(value);
72 - break;
73 - }
74 - }
75 - },
76 - };
77 - },
78 - ([text, ms]) => text,
79 - );
80 - textResourceShouldFail = false;
33 + textCache = new Map();
34 });
35
83 - function Text(props) {
84 - Scheduler.log(props.text);
85 - return props.text;
36 + function resolveText(text) {
37 + const record = textCache.get(text);
38 + if (record === undefined) {
39 + const newRecord = {
40 + status: 'resolved',
41 + value: text,
42 + };
43 + textCache.set(text, newRecord);
44 + } else if (record.status === 'pending') {
45 + const thenable = record.value;
46 + record.status = 'resolved';
47 + record.value = text;
48 + thenable.pings.forEach(t => t());
49 + }
50 }
51
88 - function AsyncText(props) {
89 - const text = props.text;
90 - try {
91 - TextResource.read([props.text, props.ms]);
92 - Scheduler.log(text);
93 - return text;
94 - } catch (promise) {
95 - if (typeof promise.then === 'function') {
96 - Scheduler.log(`Suspend! [${text}]`);
97 - } else {
98 - Scheduler.log(`Error! [${text}]`);
52 + function readText(text) {
53 + const record = textCache.get(text);
54 + if (record !== undefined) {
55 + switch (record.status) {
56 + case 'pending':
57 + Scheduler.log(`Suspend! [${text}]`);
58 + throw record.value;
59 + case 'rejected':
60 + throw record.value;
61 + case 'resolved':
62 + return record.value;
63 }
100 - throw promise;
64 + } else {
65 + Scheduler.log(`Suspend! [${text}]`);
66 + const thenable = {
67 + pings: [],
68 + then(resolve) {
69 + if (newRecord.status === 'pending') {
70 + thenable.pings.push(resolve);
71 + } else {
72 + Promise.resolve().then(() => resolve(newRecord.value));
73 + }
74 + },
75 + };
76 +
77 + const newRecord = {
78 + status: 'pending',
79 + value: thenable,
80 + };
81 + textCache.set(text, newRecord);
82 +
83 + throw thenable;
84 }
85 }
86
87 + function Text({text}) {
88 + Scheduler.log(text);
89 + return text;
90 + }
91 +
92 + function AsyncText({text}) {
93 + readText(text);
94 + Scheduler.log(text);
95 + return text;
96 + }
97 +
98 it('suspends rendering and continues later', async () => {
99 function Bar(props) {
100 Scheduler.log('Bar');
@@ -146,16 +140,10 @@ describe('ReactSuspense', () => {
140 ]);
141 expect(root).toMatchRenderedOutput(null);
142
149 - // Flush some of the time
150 - jest.advanceTimersByTime(50);
151 - // Still nothing...
143 await waitForAll([]);
144 expect(root).toMatchRenderedOutput(null);
145
155 - // Flush the promise completely
156 - jest.advanceTimersByTime(50);
157 - // Renders successfully
158 - assertLog(['Promise resolved [A]']);
146 + await resolveText('A');
147 await waitForAll(['Foo', 'Bar', 'A', 'B']);
148 expect(root).toMatchRenderedOutput('AB');
149 });
@@ -184,19 +172,15 @@ describe('ReactSuspense', () => {
172 ]);
173 expect(root).toMatchRenderedOutput('Loading A...Loading B...');
174
187 - // Advance time by enough that the first Suspense's promise resolves and
188 - // switches back to the normal view. The second Suspense should still
189 - // show the placeholder
190 - jest.advanceTimersByTime(5000);
191 - // TODO: Should we throw if you forget to call toHaveYielded?
192 - assertLog(['Promise resolved [A]']);
175 + // Resolve first Suspense's promise and switch back to the normal view. The
176 + // second Suspense should still show the placeholder
177 + await resolveText('A');
178 await waitForAll(['A']);
179 expect(root).toMatchRenderedOutput('ALoading B...');
180
196 - // Advance time by enough that the second Suspense's promise resolves
197 - // and switches back to the normal view
198 - jest.advanceTimersByTime(1000);
199 - assertLog(['Promise resolved [B]']);
181 + // Resolve the second Suspense's promise resolves and switche back to the
182 + // normal view
183 + await resolveText('B');
184 await waitForAll(['B']);
185 expect(root).toMatchRenderedOutput('AB');
186 });
@@ -284,11 +268,6 @@ describe('ReactSuspense', () => {
268 );
269 }
270
287 - // Committing fallbacks should be throttled.
288 - // First, advance some time to skip the first threshold.
289 - jest.advanceTimersByTime(600);
290 - Scheduler.unstable_advanceTime(600);
291 -
271 const root = ReactTestRenderer.create(<Foo />, {
272 unstable_isConcurrent: true,
273 });
@@ -302,10 +281,7 @@ describe('ReactSuspense', () => {
281 ]);
282 expect(root).toMatchRenderedOutput('Loading...');
283
305 - // Resolve A.
306 - jest.advanceTimersByTime(200);
307 - Scheduler.unstable_advanceTime(200);
308 - assertLog(['Promise resolved [A]']);
284 + await resolveText('A');
285 await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
286
287 // By this point, we have enough info to show "A" and "Loading more..."
@@ -313,15 +289,12 @@ describe('ReactSuspense', () => {
289 // showing the inner fallback hoping that B will resolve soon enough.
290 expect(root).toMatchRenderedOutput('Loading...');
291
316 - // Resolve B.
317 - jest.advanceTimersByTime(100);
318 - Scheduler.unstable_advanceTime(100);
319 - assertLog(['Promise resolved [B]']);
320 -
292 // By this point, B has resolved.
293 // We're still showing the outer fallback.
294 + await resolveText('B');
295 expect(root).toMatchRenderedOutput('Loading...');
296 await waitForAll(['A', 'B']);
297 +
298 // Then contents of both should pop in together.
299 expect(root).toMatchRenderedOutput('AB');
300 });
@@ -339,11 +312,6 @@ describe('ReactSuspense', () => {
312 );
313 }
314
342 - // Committing fallbacks should be throttled.
343 - // First, advance some time to skip the first threshold.
344 - jest.advanceTimersByTime(600);
345 - Scheduler.unstable_advanceTime(600);
346 -
315 const root = ReactTestRenderer.create(<Foo />, {
316 unstable_isConcurrent: true,
317 });
@@ -357,27 +325,20 @@ describe('ReactSuspense', () => {
325 ]);
326 expect(root).toMatchRenderedOutput('Loading...');
327
360 - // Resolve A.
361 - jest.advanceTimersByTime(200);
362 - Scheduler.unstable_advanceTime(200);
363 - assertLog(['Promise resolved [A]']);
328 + await resolveText('A');
329 await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
330
331 // By this point, we have enough info to show "A" and "Loading more..."
332 // However, we've just shown the outer fallback. So we'll delay
333 // showing the inner fallback hoping that B will resolve soon enough.
334 expect(root).toMatchRenderedOutput('Loading...');
370 -
371 - // Wait some more. B is still not resolving.
335 + // But if we wait a bit longer, eventually we'll give up and show a
336 + // fallback. The exact value here isn't important. It's a JND ("Just
337 + // Noticeable Difference").
338 jest.advanceTimersByTime(500);
373 - Scheduler.unstable_advanceTime(500);
374 - // Give up and render A with a spinner for B.
339 expect(root).toMatchRenderedOutput('ALoading more...');
340
377 - // Resolve B.
378 - jest.advanceTimersByTime(500);
379 - Scheduler.unstable_advanceTime(500);
380 - assertLog(['Promise resolved [B]']);
341 + await resolveText('B');
342 await waitForAll(['B']);
343 expect(root).toMatchRenderedOutput('AB');
344 });
@@ -423,18 +384,7 @@ describe('ReactSuspense', () => {
384
385 const MemoizedChild = memo(function MemoizedChild() {
386 const text = useContext(ValueContext);
426 - try {
427 - TextResource.read([text, 1000]);
428 - Scheduler.log(text);
429 - return text;
430 - } catch (promise) {
431 - if (typeof promise.then === 'function') {
432 - Scheduler.log(`Suspend! [${text}]`);
433 - } else {
434 - Scheduler.log(`Error! [${text}]`);
435 - }
436 - throw promise;
437 - }
387 + return <Text text={readText(text)} />;
388 });
389
390 let setValue;
@@ -455,17 +405,15 @@ describe('ReactSuspense', () => {
405 unstable_isConcurrent: true,
406 });
407 await waitForAll(['Suspend! [default]', 'Loading...']);
458 - jest.advanceTimersByTime(1000);
408
460 - assertLog(['Promise resolved [default]']);
409 + await resolveText('default');
410 await waitForAll(['default']);
411 expect(root).toMatchRenderedOutput('default');
412
464 - act(() => setValue('new value'));
413 + await act(async () => setValue('new value'));
414 assertLog(['Suspend! [new value]', 'Loading...']);
466 - jest.advanceTimersByTime(1000);
415
468 - assertLog(['Promise resolved [new value]']);
416 + await resolveText('new value');
417 await waitForAll(['new value']);
418 expect(root).toMatchRenderedOutput('new value');
419 });
@@ -478,18 +426,7 @@ describe('ReactSuspense', () => {
426 const MemoizedChild = memo(
427 function MemoizedChild() {
428 const text = useContext(ValueContext);
481 - try {
482 - TextResource.read([text, 1000]);
483 - Scheduler.log(text);
484 - return text;
485 - } catch (promise) {
486 - if (typeof promise.then === 'function') {
487 - Scheduler.log(`Suspend! [${text}]`);
488 - } else {
489 - Scheduler.log(`Error! [${text}]`);
490 - }
491 - throw promise;
492 - }
429 + return <Text text={readText(text)} />;
430 },
431 function areEqual(prevProps, nextProps) {
432 return true;
@@ -514,17 +451,15 @@ describe('ReactSuspense', () => {
451 unstable_isConcurrent: true,
452 });
453 await waitForAll(['Suspend! [default]', 'Loading...']);
517 - jest.advanceTimersByTime(1000);
454
519 - assertLog(['Promise resolved [default]']);
455 + await resolveText('default');
456 await waitForAll(['default']);
457 expect(root).toMatchRenderedOutput('default');
458
523 - act(() => setValue('new value'));
459 + await act(async () => setValue('new value'));
460 assertLog(['Suspend! [new value]', 'Loading...']);
525 - jest.advanceTimersByTime(1000);
461
527 - assertLog(['Promise resolved [new value]']);
462 + await resolveText('new value');
463 await waitForAll(['new value']);
464 expect(root).toMatchRenderedOutput('new value');
465 });
@@ -536,18 +471,7 @@ describe('ReactSuspense', () => {
471
472 function MemoizedChild() {
473 const text = useContext(ValueContext);
539 - try {
540 - TextResource.read([text, 1000]);
541 - Scheduler.log(text);
542 - return text;
543 - } catch (promise) {
544 - if (typeof promise.then === 'function') {
545 - Scheduler.log(`Suspend! [${text}]`);
546 - } else {
547 - Scheduler.log(`Error! [${text}]`);
548 - }
549 - throw promise;
550 - }
474 + return <Text text={readText(text)} />;
475 }
476
477 let setValue;
@@ -571,17 +495,15 @@ describe('ReactSuspense', () => {
495 },
496 );
497 await waitForAll(['Suspend! [default]', 'Loading...']);
574 - jest.advanceTimersByTime(1000);
498
576 - assertLog(['Promise resolved [default]']);
499 + await resolveText('default');
500 await waitForAll(['default']);
501 expect(root).toMatchRenderedOutput('default');
502
580 - act(() => setValue('new value'));
503 + await act(async () => setValue('new value'));
504 assertLog(['Suspend! [new value]', 'Loading...']);
582 - jest.advanceTimersByTime(1000);
505
584 - assertLog(['Promise resolved [new value]']);
506 + await resolveText('new value');
507 await waitForAll(['new value']);
508 expect(root).toMatchRenderedOutput('new value');
509 });
@@ -593,18 +515,7 @@ describe('ReactSuspense', () => {
515
516 const MemoizedChild = forwardRef(() => {
517 const text = useContext(ValueContext);
596 - try {
597 - TextResource.read([text, 1000]);
598 - Scheduler.log(text);
599 - return text;
600 - } catch (promise) {
601 - if (typeof promise.then === 'function') {
602 - Scheduler.log(`Suspend! [${text}]`);
603 - } else {
604 - Scheduler.log(`Error! [${text}]`);
605 - }
606 - throw promise;
607 - }
518 + return <Text text={readText(text)} />;
519 });
520
521 let setValue;
@@ -628,17 +539,15 @@ describe('ReactSuspense', () => {
539 },
540 );
541 await waitForAll(['Suspend! [default]', 'Loading...']);
631 - jest.advanceTimersByTime(1000);
542
633 - assertLog(['Promise resolved [default]']);
543 + await resolveText('default');
544 await waitForAll(['default']);
545 expect(root).toMatchRenderedOutput('default');
546
637 - act(() => setValue('new value'));
547 + await act(async () => setValue('new value'));
548 assertLog(['Suspend! [new value]', 'Loading...']);
639 - jest.advanceTimersByTime(1000);
549
641 - assertLog(['Promise resolved [new value]']);
550 + await resolveText('new value');
551 await waitForAll(['new value']);
552 expect(root).toMatchRenderedOutput('new value');
553 });
@@ -674,12 +583,17 @@ describe('ReactSuspense', () => {
583 await waitForAll(['Child 1', 'create layout']);
584 expect(root).toMatchRenderedOutput('Child 1');
585
677 - act(() => {
586 + await act(async () => {
587 _setShow(true);
588 });
680 - assertLog(['Child 1', 'Suspend! [Child 2]', 'Loading...']);
681 - jest.advanceTimersByTime(1000);
682 - assertLog(['destroy layout', 'Promise resolved [Child 2]']);
589 + assertLog([
590 + 'Child 1',
591 + 'Suspend! [Child 2]',
592 + 'Loading...',
593 + 'destroy layout',
594 + ]);
595 +
596 + await resolveText('Child 2');
597 await waitForAll(['Child 1', 'Child 2', 'create layout']);
598 expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join(''));
599 });
@@ -715,20 +629,8 @@ describe('ReactSuspense', () => {
629 }
630 render() {
631 instance = this;
718 - const text = `${this.props.text}:${this.state.step}`;
719 - const ms = this.props.ms;
720 - try {
721 - TextResource.read([text, ms]);
722 - Scheduler.log(text);
723 - return text;
724 - } catch (promise) {
725 - if (typeof promise.then === 'function') {
726 - Scheduler.log(`Suspend! [${text}]`);
727 - } else {
728 - Scheduler.log(`Error! [${text}]`);
729 - }
730 - throw promise;
731 - }
632 + const text = readText(`${this.props.text}:${this.state.step}`);
633 + return <Text text={text} />;
634 }
635 }
636
@@ -758,9 +660,7 @@ describe('ReactSuspense', () => {
660 ]);
661 expect(root).toMatchRenderedOutput('Loading...');
662
761 - jest.advanceTimersByTime(100);
762 -
763 - assertLog(['Promise resolved [B:1]']);
663 + await resolveText('B:1');
664 await waitForPaint([
665 'B:1',
666 'Unmount [Loading...]',
@@ -773,9 +673,7 @@ describe('ReactSuspense', () => {
673 assertLog(['Suspend! [B:2]', 'Loading...', 'Mount [Loading...]']);
674 expect(root).toMatchRenderedOutput('Loading...');
675
776 - jest.advanceTimersByTime(100);
777 -
778 - assertLog(['Promise resolved [B:2]']);
676 + await resolveText('B:2');
677 await waitForPaint(['B:2', 'Unmount [Loading...]', 'Update [B:2]']);
678 expect(root).toMatchRenderedOutput('AB:2C');
679 });
@@ -803,9 +701,7 @@ describe('ReactSuspense', () => {
701
702 assertLog(['Stateful: 1', 'Suspend! [A]', 'Loading...']);
703
806 - jest.advanceTimersByTime(1000);
807 -
808 - assertLog(['Promise resolved [A]']);
704 + await resolveText('A');
705 await waitForPaint(['A']);
706 expect(root).toMatchRenderedOutput('Stateful: 1A');
707
@@ -817,9 +713,7 @@ describe('ReactSuspense', () => {
713 assertLog(['Stateful: 2', 'Suspend! [B]']);
714 expect(root).toMatchRenderedOutput('Loading...');
715
820 - jest.advanceTimersByTime(1000);
821 -
822 - assertLog(['Promise resolved [B]']);
716 + await resolveText('B');
717 await waitForPaint(['B']);
718 expect(root).toMatchRenderedOutput('Stateful: 2B');
719 });
@@ -855,9 +749,7 @@ describe('ReactSuspense', () => {
749
750 assertLog(['Stateful: 1', 'Suspend! [A]', 'Loading...']);
751
858 - jest.advanceTimersByTime(1000);
859 -
860 - assertLog(['Promise resolved [A]']);
752 + await resolveText('A');
753 await waitForPaint(['A']);
754 expect(root).toMatchRenderedOutput('Stateful: 1A');
755
@@ -876,9 +768,7 @@ describe('ReactSuspense', () => {
768 ]);
769 expect(root).toMatchRenderedOutput('Loading...');
770
879 - jest.advanceTimersByTime(1000);
880 -
881 - assertLog(['Promise resolved [B]']);
771 + await resolveText('B');
772 await waitForPaint(['B']);
773 expect(root).toMatchRenderedOutput('Stateful: 2B');
774 });
@@ -889,20 +779,7 @@ describe('ReactSuspense', () => {
779 Scheduler.log('will unmount');
780 }
781 render() {
892 - const text = this.props.text;
893 - const ms = this.props.ms;
894 - try {
895 - TextResource.read([text, ms]);
896 - Scheduler.log(text);
897 - return text;
898 - } catch (promise) {
899 - if (typeof promise.then === 'function') {
900 - Scheduler.log(`Suspend! [${text}]`);
901 - } else {
902 - Scheduler.log(`Error! [${text}]`);
903 - }
904 - throw promise;
905 - }
782 + return <Text text={readText(this.props.text)} />;
783 }
784 }
785
@@ -932,18 +809,7 @@ describe('ReactSuspense', () => {
809 Scheduler.log('Did commit: ' + text);
810 }, [text]);
811
935 - try {
936 - TextResource.read([props.text, props.ms]);
937 - Scheduler.log(text);
938 - return text;
939 - } catch (promise) {
940 - if (typeof promise.then === 'function') {
941 - Scheduler.log(`Suspend! [${text}]`);
942 - } else {
943 - Scheduler.log(`Error! [${text}]`);
944 - }
945 - throw promise;
946 - }
812 + return <Text text={readText(text)} />;
813 }
814
815 function App({text}) {
@@ -956,9 +822,7 @@ describe('ReactSuspense', () => {
822
823 ReactTestRenderer.create(<App text="A" />);
824 assertLog(['Suspend! [A]', 'Loading...']);
959 - jest.advanceTimersByTime(500);
960 -
961 - assertLog(['Promise resolved [A]']);
825 + await resolveText('A');
826 await waitForPaint(['A', 'Did commit: A']);
827 });
828
@@ -986,15 +850,16 @@ describe('ReactSuspense', () => {
850
851 // Initial render
852 await waitForAll(['Suspend! [Step: 1]', 'Loading...']);
989 - jest.advanceTimersByTime(1000);
990 - assertLog(['Promise resolved [Step: 1]']);
853 +
854 + await resolveText('Step: 1');
855 await waitForAll(['Step: 1']);
856 expect(root).toMatchRenderedOutput('Step: 1');
857
858 // Update that suspends
995 - instance.setState({step: 2});
996 - await waitForAll(['Suspend! [Step: 2]', 'Loading...']);
997 - jest.advanceTimersByTime(500);
859 + await act(async () => {
860 + instance.setState({step: 2});
861 + });
862 + assertLog(['Suspend! [Step: 2]', 'Loading...']);
863 expect(root).toMatchRenderedOutput('Loading...');
864
865 // Update while still suspended
@@ -1002,8 +867,8 @@ describe('ReactSuspense', () => {
867 await waitForAll(['Suspend! [Step: 3]']);
868 expect(root).toMatchRenderedOutput('Loading...');
869
1005 - jest.advanceTimersByTime(1000);
1006 - assertLog(['Promise resolved [Step: 2]', 'Promise resolved [Step: 3]']);
870 + await resolveText('Step: 2');
871 + await resolveText('Step: 3');
872 await waitForAll(['Step: 3']);
873 expect(root).toMatchRenderedOutput('Step: 3');
874 });
@@ -1040,23 +905,17 @@ describe('ReactSuspense', () => {
905 ]);
906 await waitForAll([]);
907
1043 - jest.advanceTimersByTime(1000);
1044 -
1045 - assertLog(['Promise resolved [Child 1]']);
908 + await resolveText('Child 1');
909 await waitForPaint([
910 'Child 1',
911 'Suspend! [Child 2]',
912 'Suspend! [Child 3]',
913 ]);
914
1052 - jest.advanceTimersByTime(1000);
1053 -
1054 - assertLog(['Promise resolved [Child 2]']);
915 + await resolveText('Child 2');
916 await waitForPaint(['Child 2', 'Suspend! [Child 3]']);
917
1057 - jest.advanceTimersByTime(1000);
1058 -
1059 - assertLog(['Promise resolved [Child 3]']);
918 + await resolveText('Child 3');
919 await waitForPaint(['Child 3']);
920 expect(root).toMatchRenderedOutput(
921 ['Child 1', 'Child 2', 'Child 3'].join(''),
@@ -1083,11 +942,12 @@ describe('ReactSuspense', () => {
942 'Suspend! [Child 2]',
943 'Loading...',
944 ]);
1086 - jest.advanceTimersByTime(1000);
1087 - assertLog(['Promise resolved [Child 1]']);
945 + await resolveText('Child 1');
946 await waitForAll(['Child 1', 'Suspend! [Child 2]']);
947 +
948 jest.advanceTimersByTime(6000);
1090 - assertLog(['Promise resolved [Child 2]']);
949 +
950 + await resolveText('Child 2');
951 await waitForAll(['Child 1', 'Child 2']);
952 expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join(''));
953 });
@@ -1111,32 +971,29 @@ describe('ReactSuspense', () => {
971 const root = ReactTestRenderer.create(<App />);
972 assertLog(['Suspend! [Tab: 0]', ' + sibling', 'Loading...']);
973 expect(root).toMatchRenderedOutput('Loading...');
1114 - jest.advanceTimersByTime(1000);
974
1116 - assertLog(['Promise resolved [Tab: 0]']);
975 + await resolveText('Tab: 0');
976 await waitForPaint(['Tab: 0']);
977 expect(root).toMatchRenderedOutput('Tab: 0 + sibling');
978
1120 - act(() => setTab(1));
979 + await act(async () => setTab(1));
980 assertLog(['Suspend! [Tab: 1]', ' + sibling', 'Loading...']);
981 expect(root).toMatchRenderedOutput('Loading...');
1123 - jest.advanceTimersByTime(1000);
982
1125 - assertLog(['Promise resolved [Tab: 1]']);
983 + await resolveText('Tab: 1');
984 await waitForPaint(['Tab: 1']);
985 expect(root).toMatchRenderedOutput('Tab: 1 + sibling');
986
1129 - act(() => setTab(2));
987 + await act(async () => setTab(2));
988 assertLog(['Suspend! [Tab: 2]', ' + sibling', 'Loading...']);
989 expect(root).toMatchRenderedOutput('Loading...');
1132 - jest.advanceTimersByTime(1000);
990
1134 - assertLog(['Promise resolved [Tab: 2]']);
991 + await resolveText('Tab: 2');
992 await waitForPaint(['Tab: 2']);
993 expect(root).toMatchRenderedOutput('Tab: 2 + sibling');
994 });
995
1139 - it('does not warn if an mounted component is pinged', async () => {
996 + it('does not warn if a mounted component is pinged', async () => {
997 const {useState} = React;
998
999 const root = ReactTestRenderer.create(null);
@@ -1146,18 +1003,7 @@ describe('ReactSuspense', () => {
1003 const [step, _setStep] = useState(0);
1004 setStep = _setStep;
1005 const fullText = `${text}:${step}`;
1149 - try {
1150 - TextResource.read([fullText, ms]);
1151 - Scheduler.log(fullText);
1152 - return fullText;
1153 - } catch (promise) {
1154 - if (typeof promise.then === 'function') {
1155 - Scheduler.log(`Suspend! [${fullText}]`);
1156 - } else {
1157 - Scheduler.log(`Error! [${fullText}]`);
1158 - }
1159 - throw promise;
1160 - }
1006 + return <Text text={readText(fullText)} />;
1007 }
1008
1009 root.update(
@@ -1167,19 +1013,18 @@ describe('ReactSuspense', () => {
1013 );
1014
1015 assertLog(['Suspend! [A:0]', 'Loading...']);
1170 - jest.advanceTimersByTime(1000);
1016
1172 - assertLog(['Promise resolved [A:0]']);
1017 + await resolveText('A:0');
1018 await waitForPaint(['A:0']);
1019 expect(root).toMatchRenderedOutput('A:0');
1020
1176 - act(() => setStep(1));
1021 + await act(async () => setStep(1));
1022 assertLog(['Suspend! [A:1]', 'Loading...']);
1023 expect(root).toMatchRenderedOutput('Loading...');
1024
1180 - root.update(null);
1181 - await waitForAll([]);
1182 - jest.advanceTimersByTime(1000);
1025 + await act(async () => {
1026 + root.update(null);
1027 + });
1028 });
1029
1030 it('memoizes promise listeners per thread ID to prevent redundant renders', async () => {
@@ -1199,10 +1044,7 @@ describe('ReactSuspense', () => {
1044
1045 assertLog(['Suspend! [A]', 'Suspend! [B]', 'Suspend! [C]', 'Loading...']);
1046
1202 - // Resolve A
1203 - jest.advanceTimersByTime(1000);
1204 -
1205 - assertLog(['Promise resolved [A]']);
1047 + await resolveText('A');
1048 await waitForPaint([
1049 'A',
1050 // The promises for B and C have now been thrown twice
@@ -1210,10 +1052,7 @@ describe('ReactSuspense', () => {
1052 'Suspend! [C]',
1053 ]);
1054
1213 - // Resolve B
1214 - jest.advanceTimersByTime(1000);
1215 -
1216 - assertLog(['Promise resolved [B]']);
1055 + await resolveText('B');
1056 await waitForPaint([
1057 // Even though the promise for B was thrown twice, we should only
1058 // re-render once.
@@ -1222,10 +1061,7 @@ describe('ReactSuspense', () => {
1061 'Suspend! [C]',
1062 ]);
1063
1225 - // Resolve C
1226 - jest.advanceTimersByTime(1000);
1227 -
1228 - assertLog(['Promise resolved [C]']);
1064 + await resolveText('C');
1065 await waitForPaint([
1066 // Even though the promise for C was thrown three times, we should only
1067 // re-render once.
@@ -1233,7 +1069,7 @@ describe('ReactSuspense', () => {
1069 ]);
1070 });
1071
1236 - it('#14162', () => {
1072 + it('#14162', async () => {
1073 const {lazy} = React;
1074
1075 function Hello() {
@@ -1267,8 +1103,9 @@ describe('ReactSuspense', () => {
1103
1104 const root = ReactTestRenderer.create(null);
1105
1270 - root.update(<App name="world" />);
1271 - jest.advanceTimersByTime(1000);
1106 + await act(async () => {
1107 + root.update(<App name="world" />);
1108 + });
1109 });
1110
1111 it('updates memoized child of suspense component when context updates (simple memo)', async () => {
@@ -1278,18 +1115,7 @@ describe('ReactSuspense', () => {
1115
1116 const MemoizedChild = memo(function MemoizedChild() {
1117 const text = useContext(ValueContext);
1281 - try {
1282 - TextResource.read([text, 1000]);
1283 - Scheduler.log(text);
1284 - return text;
1285 - } catch (promise) {
1286 - if (typeof promise.then === 'function') {
1287 - Scheduler.log(`Suspend! [${text}]`);
1288 - } else {
1289 - Scheduler.log(`Error! [${text}]`);
1290 - }
1291 - throw promise;
1292 - }
1118 + return <Text text={readText(text)} />;
1119 });
1120
1121 let setValue;
@@ -1308,17 +1134,15 @@ describe('ReactSuspense', () => {
1134
1135 const root = ReactTestRenderer.create(<App />);
1136 assertLog(['Suspend! [default]', 'Loading...']);
1311 - jest.advanceTimersByTime(1000);
1137
1313 - assertLog(['Promise resolved [default]']);
1138 + await resolveText('default');
1139 await waitForPaint(['default']);
1140 expect(root).toMatchRenderedOutput('default');
1141
1317 - act(() => setValue('new value'));
1142 + await act(async () => setValue('new value'));
1143 assertLog(['Suspend! [new value]', 'Loading...']);
1319 - jest.advanceTimersByTime(1000);
1144
1321 - assertLog(['Promise resolved [new value]']);
1145 + await resolveText('new value');
1146 await waitForPaint(['new value']);
1147 expect(root).toMatchRenderedOutput('new value');
1148 });
@@ -1331,18 +1155,7 @@ describe('ReactSuspense', () => {
1155 const MemoizedChild = memo(
1156 function MemoizedChild() {
1157 const text = useContext(ValueContext);
1334 - try {
1335 - TextResource.read([text, 1000]);
1336 - Scheduler.log(text);
1337 - return text;
1338 - } catch (promise) {
1339 - if (typeof promise.then === 'function') {
1340 - Scheduler.log(`Suspend! [${text}]`);
1341 - } else {
1342 - Scheduler.log(`Error! [${text}]`);
1343 - }
1344 - throw promise;
1345 - }
1158 + return <Text text={readText(text)} />;
1159 },
1160 function areEqual(prevProps, nextProps) {
1161 return true;
@@ -1365,17 +1178,15 @@ describe('ReactSuspense', () => {
1178
1179 const root = ReactTestRenderer.create(<App />);
1180 assertLog(['Suspend! [default]', 'Loading...']);
1368 - jest.advanceTimersByTime(1000);
1181
1370 - assertLog(['Promise resolved [default]']);
1182 + await resolveText('default');
1183 await waitForPaint(['default']);
1184 expect(root).toMatchRenderedOutput('default');
1185
1374 - act(() => setValue('new value'));
1186 + await act(async () => setValue('new value'));
1187 assertLog(['Suspend! [new value]', 'Loading...']);
1376 - jest.advanceTimersByTime(1000);
1188
1378 - assertLog(['Promise resolved [new value]']);
1189 + await resolveText('new value');
1190 await waitForPaint(['new value']);
1191 expect(root).toMatchRenderedOutput('new value');
1192 });
@@ -1387,18 +1198,7 @@ describe('ReactSuspense', () => {
1198
1199 function MemoizedChild() {
1200 const text = useContext(ValueContext);
1390 - try {
1391 - TextResource.read([text, 1000]);
1392 - Scheduler.log(text);
1393 - return text;
1394 - } catch (promise) {
1395 - if (typeof promise.then === 'function') {
1396 - Scheduler.log(`Suspend! [${text}]`);
1397 - } else {
1398 - Scheduler.log(`Error! [${text}]`);
1399 - }
1400 - throw promise;
1401 - }
1201 + return <Text text={readText(text)} />;
1202 }
1203
1204 let setValue;
@@ -1421,17 +1221,15 @@ describe('ReactSuspense', () => {
1221 </App>,
1222 );
1223 assertLog(['Suspend! [default]', 'Loading...']);
1424 - jest.advanceTimersByTime(1000);
1224
1426 - assertLog(['Promise resolved [default]']);
1225 + await resolveText('default');
1226 await waitForPaint(['default']);
1227 expect(root).toMatchRenderedOutput('default');
1228
1430 - act(() => setValue('new value'));
1229 + await act(async () => setValue('new value'));
1230 assertLog(['Suspend! [new value]', 'Loading...']);
1432 - jest.advanceTimersByTime(1000);
1231
1434 - assertLog(['Promise resolved [new value]']);
1232 + await resolveText('new value');
1233 await waitForPaint(['new value']);
1234 expect(root).toMatchRenderedOutput('new value');
1235 });
@@ -1443,18 +1241,7 @@ describe('ReactSuspense', () => {
1241
1242 const MemoizedChild = forwardRef(function MemoizedChild() {
1243 const text = useContext(ValueContext);
1446 - try {
1447 - TextResource.read([text, 1000]);
1448 - Scheduler.log(text);
1449 - return text;
1450 - } catch (promise) {
1451 - if (typeof promise.then === 'function') {
1452 - Scheduler.log(`Suspend! [${text}]`);
1453 - } else {
1454 - Scheduler.log(`Error! [${text}]`);
1455 - }
1456 - throw promise;
1457 - }
1244 + return <Text text={readText(text)} />;
1245 });
1246
1247 let setValue;
@@ -1473,22 +1260,20 @@ describe('ReactSuspense', () => {
1260
1261 const root = ReactTestRenderer.create(<App />);
1262 assertLog(['Suspend! [default]', 'Loading...']);
1476 - jest.advanceTimersByTime(1000);
1263
1478 - assertLog(['Promise resolved [default]']);
1264 + await resolveText('default');
1265 await waitForPaint(['default']);
1266 expect(root).toMatchRenderedOutput('default');
1267
1482 - act(() => setValue('new value'));
1268 + await act(async () => setValue('new value'));
1269 assertLog(['Suspend! [new value]', 'Loading...']);
1484 - jest.advanceTimersByTime(1000);
1270
1486 - assertLog(['Promise resolved [new value]']);
1271 + await resolveText('new value');
1272 await waitForPaint(['new value']);
1273 expect(root).toMatchRenderedOutput('new value');
1274 });
1275
1491 - it('updates context consumer within child of suspended suspense component when context updates', () => {
1276 + it('updates context consumer within child of suspended suspense component when context updates', async () => {
1277 const {createContext, useState} = React;
1278
1279 const ValueContext = createContext(null);
@@ -1531,11 +1316,11 @@ describe('ReactSuspense', () => {
1316 assertLog(['Received context value [default]', 'default']);
1317 expect(root).toMatchRenderedOutput('default');
1318
1534 - act(() => setValue('new value'));
1319 + await act(async () => setValue('new value'));
1320 assertLog(['Received context value [new value]', 'Loading...']);
1321 expect(root).toMatchRenderedOutput('Loading...');
1322
1538 - act(() => setValue('default'));
1323 + await act(async () => setValue('default'));
1324 assertLog(['Received context value [default]', 'default']);
1325 expect(root).toMatchRenderedOutput('default');
1326 });
packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
+311 -312
@@ -17,6 +17,7 @@ let caches;
17 let seededCache;
18 let ErrorBoundary;
19 let waitForAll;
20 +let waitFor;
21 let assertLog;
22
23 // TODO: These tests don't pass in persistent mode yet. Need to implement.
@@ -35,6 +36,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
36
37 const InternalTestUtils = require('internal-test-utils');
38 waitForAll = InternalTestUtils.waitForAll;
39 + waitFor = InternalTestUtils.waitFor;
40 assertLog = InternalTestUtils.assertLog;
41
42 caches = [];
@@ -372,7 +374,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
374 }
375
376 // Mount and suspend.
375 - act(() => {
377 + await act(async () => {
378 ReactNoop.renderLegacySyncRoot(
379 <App>
380 <AsyncText text="Async" ms={1000} />
@@ -473,7 +475,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
475 }
476
477 // Mount
476 - act(() => {
478 + await act(async () => {
479 ReactNoop.renderLegacySyncRoot(<App />);
480 });
481 assertLog([
@@ -499,7 +501,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
501 );
502
503 // Schedule an update that causes React to suspend.
502 - act(() => {
504 + await act(async () => {
505 ReactNoop.renderLegacySyncRoot(
506 <App>
507 <AsyncText text="Async" ms={1000} />
@@ -629,46 +631,46 @@ describe('ReactSuspenseEffectsSemantics', () => {
631 );
632
633 // Schedule an update that causes React to suspend.
632 - act(() => {
634 + await act(async () => {
635 ReactNoop.render(
636 <App>
637 <AsyncText text="Async" ms={1000} />
638 </App>,
639 );
638 - });
639 - assertLog([
640 - 'App render',
641 - 'Text:Inside:Before render',
642 - 'Suspend:Async',
643 - 'Text:Inside:After render',
644 - 'Text:Fallback render',
645 - 'Text:Outside render',
646 - ]);
647 - expect(ReactNoop).toMatchRenderedOutput(
648 - <>
649 - <span prop="Inside:Before" />
650 - <span prop="Inside:After" />
651 - <span prop="Outside" />
652 - </>,
653 - );
640 + await waitFor([
641 + 'App render',
642 + 'Text:Inside:Before render',
643 + 'Suspend:Async',
644 + 'Text:Inside:After render',
645 + 'Text:Fallback render',
646 + 'Text:Outside render',
647 + ]);
648 + expect(ReactNoop).toMatchRenderedOutput(
649 + <>
650 + <span prop="Inside:Before" />
651 + <span prop="Inside:After" />
652 + <span prop="Outside" />
653 + </>,
654 + );
655
655 - await advanceTimers(1000);
656 + await jest.runAllTimers();
657
657 - // Timing out should commit the fallback and destroy inner layout effects.
658 - assertLog([
659 - 'Text:Inside:Before destroy layout',
660 - 'Text:Inside:After destroy layout',
661 - 'Text:Fallback create layout',
662 - ]);
663 - await waitForAll(['Text:Fallback create passive']);
664 - expect(ReactNoop).toMatchRenderedOutput(
665 - <>
666 - <span prop="Inside:Before" hidden={true} />
667 - <span prop="Inside:After" hidden={true} />
668 - <span prop="Fallback" />
669 - <span prop="Outside" />
670 - </>,
671 - );
658 + // Timing out should commit the fallback and destroy inner layout effects.
659 + assertLog([
660 + 'Text:Inside:Before destroy layout',
661 + 'Text:Inside:After destroy layout',
662 + 'Text:Fallback create layout',
663 + ]);
664 + await waitForAll(['Text:Fallback create passive']);
665 + expect(ReactNoop).toMatchRenderedOutput(
666 + <>
667 + <span prop="Inside:Before" hidden={true} />
668 + <span prop="Inside:After" hidden={true} />
669 + <span prop="Fallback" />
670 + <span prop="Outside" />
671 + </>,
672 + );
673 + });
674
675 // Resolving the suspended resource should re-create inner layout effects.
676 await act(async () => {
@@ -783,46 +785,47 @@ describe('ReactSuspenseEffectsSemantics', () => {
785 );
786
787 // Schedule an update that causes React to suspend.
786 - act(() => {
788 + await act(async () => {
789 ReactNoop.render(
790 <App>
791 <AsyncText text="Async" ms={1000} />
792 </App>,
793 );
792 - });
793 - assertLog([
794 - 'App render',
795 - 'ClassText:Inside:Before render',
796 - 'Suspend:Async',
797 - 'ClassText:Inside:After render',
798 - 'ClassText:Fallback render',
799 - 'ClassText:Outside render',
800 - ]);
801 - expect(ReactNoop).toMatchRenderedOutput(
802 - <>
803 - <span prop="Inside:Before" />
804 - <span prop="Inside:After" />
805 - <span prop="Outside" />
806 - </>,
807 - );
794
809 - await advanceTimers(1000);
795 + await waitFor([
796 + 'App render',
797 + 'ClassText:Inside:Before render',
798 + 'Suspend:Async',
799 + 'ClassText:Inside:After render',
800 + 'ClassText:Fallback render',
801 + 'ClassText:Outside render',
802 + ]);
803 + expect(ReactNoop).toMatchRenderedOutput(
804 + <>
805 + <span prop="Inside:Before" />
806 + <span prop="Inside:After" />
807 + <span prop="Outside" />
808 + </>,
809 + );
810
811 - // Timing out should commit the fallback and destroy inner layout effects.
812 - assertLog([
813 - 'ClassText:Inside:Before componentWillUnmount',
814 - 'ClassText:Inside:After componentWillUnmount',
815 - 'ClassText:Fallback componentDidMount',
816 - 'ClassText:Outside componentDidUpdate',
817 - ]);
818 - expect(ReactNoop).toMatchRenderedOutput(
819 - <>
820 - <span prop="Inside:Before" hidden={true} />
821 - <span prop="Inside:After" hidden={true} />
822 - <span prop="Fallback" />
823 - <span prop="Outside" />
824 - </>,
825 - );
811 + await jest.runAllTimers();
812 +
813 + // Timing out should commit the fallback and destroy inner layout effects.
814 + assertLog([
815 + 'ClassText:Inside:Before componentWillUnmount',
816 + 'ClassText:Inside:After componentWillUnmount',
817 + 'ClassText:Fallback componentDidMount',
818 + 'ClassText:Outside componentDidUpdate',
819 + ]);
820 + expect(ReactNoop).toMatchRenderedOutput(
821 + <>
822 + <span prop="Inside:Before" hidden={true} />
823 + <span prop="Inside:After" hidden={true} />
824 + <span prop="Fallback" />
825 + <span prop="Outside" />
826 + </>,
827 + );
828 + });
829
830 // Resolving the suspended resource should re-create inner layout effects.
831 await act(async () => {
@@ -908,43 +911,43 @@ describe('ReactSuspenseEffectsSemantics', () => {
911 );
912
913 // Schedule an update that causes React to suspend.
911 - act(() => {
914 + await act(async () => {
915 ReactNoop.render(
916 <App>
917 <AsyncText text="Async" ms={1000} />
918 </App>,
919 );
917 - });
918 - assertLog([
919 - 'App render',
920 - 'Suspend:Async',
921 - 'Text:Outer render',
922 - 'Text:Inner render',
923 - 'Text:Fallback render',
924 - ]);
925 - expect(ReactNoop).toMatchRenderedOutput(
926 - <span prop="Outer">
927 - <span prop="Inner" />
928 - </span>,
929 - );
920 + await waitFor([
921 + 'App render',
922 + 'Suspend:Async',
923 + 'Text:Outer render',
924 + 'Text:Inner render',
925 + 'Text:Fallback render',
926 + ]);
927 + expect(ReactNoop).toMatchRenderedOutput(
928 + <span prop="Outer">
929 + <span prop="Inner" />
930 + </span>,
931 + );
932
931 - await advanceTimers(1000);
933 + await jest.runAllTimers();
934
933 - // Timing out should commit the fallback and destroy inner layout effects.
934 - assertLog([
935 - 'Text:Outer destroy layout',
936 - 'Text:Inner destroy layout',
937 - 'Text:Fallback create layout',
938 - ]);
939 - await waitForAll(['Text:Fallback create passive']);
940 - expect(ReactNoop).toMatchRenderedOutput(
941 - <>
942 - <span hidden={true} prop="Outer">
943 - <span prop="Inner" />
944 - </span>
945 - <span prop="Fallback" />
946 - </>,
947 - );
935 + // Timing out should commit the fallback and destroy inner layout effects.
936 + assertLog([
937 + 'Text:Outer destroy layout',
938 + 'Text:Inner destroy layout',
939 + 'Text:Fallback create layout',
940 + ]);
941 + await waitForAll(['Text:Fallback create passive']);
942 + expect(ReactNoop).toMatchRenderedOutput(
943 + <>
944 + <span hidden={true} prop="Outer">
945 + <span prop="Inner" />
946 + </span>
947 + <span prop="Fallback" />
948 + </>,
949 + );
950 + });
951
952 // Resolving the suspended resource should re-create inner layout effects.
953 await act(async () => {
@@ -1035,44 +1038,44 @@ describe('ReactSuspenseEffectsSemantics', () => {
1038 );
1039
1040 // Schedule an update that causes React to suspend.
1038 - act(() => {
1041 + await act(async () => {
1042 ReactNoop.render(
1043 <App>
1044 <AsyncText text="Async" ms={1000} />
1045 </App>,
1046 );
1044 - });
1045 - assertLog([
1046 - 'App render',
1047 - 'Suspend:Async',
1048 - 'Text:Outer render',
1049 - // Text:MemoizedInner is memoized
1050 - 'Text:Fallback render',
1051 - ]);
1052 - expect(ReactNoop).toMatchRenderedOutput(
1053 - <span prop="Outer">
1054 - <span prop="MemoizedInner" />
1055 - </span>,
1056 - );
1047 + await waitFor([
1048 + 'App render',
1049 + 'Suspend:Async',
1050 + 'Text:Outer render',
1051 + // Text:MemoizedInner is memoized
1052 + 'Text:Fallback render',
1053 + ]);
1054 + expect(ReactNoop).toMatchRenderedOutput(
1055 + <span prop="Outer">
1056 + <span prop="MemoizedInner" />
1057 + </span>,
1058 + );
1059
1058 - await advanceTimers(1000);
1060 + await jest.runAllTimers();
1061
1060 - // Timing out should commit the fallback and destroy inner layout effects.
1061 - // Even though the innermost layout effects are beneath a hidden HostComponent.
1062 - assertLog([
1063 - 'Text:Outer destroy layout',
1064 - 'Text:MemoizedInner destroy layout',
1065 - 'Text:Fallback create layout',
1066 - ]);
1067 - await waitForAll(['Text:Fallback create passive']);
1068 - expect(ReactNoop).toMatchRenderedOutput(
1069 - <>
1070 - <span hidden={true} prop="Outer">
1071 - <span prop="MemoizedInner" />
1072 - </span>
1073 - <span prop="Fallback" />
1074 - </>,
1075 - );
1062 + // Timing out should commit the fallback and destroy inner layout effects.
1063 + // Even though the innermost layout effects are beneath a hidden HostComponent.
1064 + assertLog([
1065 + 'Text:Outer destroy layout',
1066 + 'Text:MemoizedInner destroy layout',
1067 + 'Text:Fallback create layout',
1068 + ]);
1069 + await waitForAll(['Text:Fallback create passive']);
1070 + expect(ReactNoop).toMatchRenderedOutput(
1071 + <>
1072 + <span hidden={true} prop="Outer">
1073 + <span prop="MemoizedInner" />
1074 + </span>
1075 + <span prop="Fallback" />
1076 + </>,
1077 + );
1078 + });
1079
1080 // Resolving the suspended resource should re-create inner layout effects.
1081 await act(async () => {
@@ -1147,12 +1150,11 @@ describe('ReactSuspenseEffectsSemantics', () => {
1150 );
1151
1152 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
1150 - act(() => {
1153 + await act(async () => {
1154 ReactNoop.render(
1155 <App innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />} />,
1156 );
1157 });
1155 - await advanceTimers(1000);
1158 assertLog([
1159 'Text:Outer render',
1160 'Text:Inner render',
@@ -1160,8 +1162,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
1162 'Text:InnerFallback render',
1163 'Text:Inner destroy layout',
1164 'Text:InnerFallback create layout',
1165 + 'Text:InnerFallback create passive',
1166 ]);
1164 - await waitForAll(['Text:InnerFallback create passive']);
1167 expect(ReactNoop).toMatchRenderedOutput(
1168 <>
1169 <span prop="Outer" />
@@ -1172,7 +1174,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
1174
1175 // Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
1176 // (This check also ensures we don't destroy effects for mounted inner fallback.)
1175 - act(() => {
1177 + await act(async () => {
1178 ReactNoop.render(
1179 <App
1180 outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
@@ -1191,8 +1193,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
1193 'Text:Outer destroy layout',
1194 'Text:InnerFallback destroy layout',
1195 'Text:OuterFallback create layout',
1196 + 'Text:OuterFallback create passive',
1197 ]);
1195 - await waitForAll(['Text:OuterFallback create passive']);
1198 expect(ReactNoop).toMatchRenderedOutput(
1199 <>
1200 <span prop="Outer" hidden={true} />
@@ -1222,7 +1224,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
1224 );
1225
1226 // Suspend the inner Suspense subtree (no effects should be destroyed)
1225 - act(() => {
1227 + await act(async () => {
1228 ReactNoop.render(
1229 <App
1230 outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
@@ -1297,7 +1299,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
1299 );
1300
1301 // Suspend the outer Suspense subtree (all effects should be destroyed)
1300 - act(() => {
1302 + await act(async () => {
1303 ReactNoop.render(
1304 <App
1305 outerChildren={<AsyncText text="OuterAsync_2" ms={1000} />}
@@ -1305,7 +1307,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1307 />,
1308 );
1309 });
1308 - await advanceTimers(1000);
1310 assertLog([
1311 'Text:Outer render',
1312 'Suspend:OuterAsync_2',
@@ -1317,6 +1318,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
1318 'Text:Inner destroy layout',
1319 'AsyncText:InnerAsync_2 destroy layout',
1320 'Text:OuterFallback create layout',
1321 + 'Text:OuterFallback create passive',
1322 ]);
1323 expect(ReactNoop).toMatchRenderedOutput(
1324 <>
@@ -1333,7 +1335,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1335 await resolveText('OuterAsync_2');
1336 });
1337 assertLog([
1336 - 'Text:OuterFallback create passive',
1338 'Text:Outer render',
1339 'AsyncText:OuterAsync_2 render',
1340 'Text:Inner render',
@@ -1390,12 +1391,11 @@ describe('ReactSuspenseEffectsSemantics', () => {
1391 );
1392
1393 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
1393 - act(() => {
1394 + await act(async () => {
1395 ReactNoop.render(
1396 <App innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />} />,
1397 );
1398 });
1398 - await advanceTimers(1000);
1399 assertLog([
1400 'Text:Outer render',
1401 'Text:Inner render',
@@ -1403,8 +1403,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
1403 'Text:InnerFallback render',
1404 'Text:Inner destroy layout',
1405 'Text:InnerFallback create layout',
1406 + 'Text:InnerFallback create passive',
1407 ]);
1407 - await waitForAll(['Text:InnerFallback create passive']);
1408 expect(ReactNoop).toMatchRenderedOutput(
1409 <>
1410 <span prop="Outer" />
@@ -1415,7 +1415,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
1415
1416 // Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
1417 // (This check also ensures we don't destroy effects for mounted inner fallback.)
1418 - act(() => {
1418 + await act(async () => {
1419 ReactNoop.render(
1420 <App
1421 outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
@@ -1423,7 +1423,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1423 />,
1424 );
1425 });
1426 - await advanceTimers(1000);
1426 assertLog([
1427 'Text:Outer render',
1428 'Suspend:OuterAsync_1',
@@ -1434,8 +1433,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
1433 'Text:Outer destroy layout',
1434 'Text:InnerFallback destroy layout',
1435 'Text:OuterFallback create layout',
1436 + 'Text:OuterFallback create passive',
1437 ]);
1438 - await waitForAll(['Text:OuterFallback create passive']);
1438 expect(ReactNoop).toMatchRenderedOutput(
1439 <>
1440 <span prop="Outer" hidden={true} />
@@ -1518,88 +1517,88 @@ describe('ReactSuspenseEffectsSemantics', () => {
1517 );
1518
1519 // Suspend the outer shell
1521 - act(() => {
1520 + await act(async () => {
1521 ReactNoop.render(
1522 <App outerChildren={<AsyncText text="OutsideAsync" ms={1000} />} />,
1523 );
1525 - });
1526 - assertLog([
1527 - 'Text:Inside render',
1528 - 'Suspend:OutsideAsync',
1529 - 'Text:Fallback:Inside render',
1530 - 'Text:Fallback:Outside render',
1531 - 'Text:Outside render',
1532 - ]);
1533 - expect(ReactNoop).toMatchRenderedOutput(
1534 - <>
1535 - <span prop="Inside" />
1536 - <span prop="Outside" />
1537 - </>,
1538 - );
1524 + await waitFor([
1525 + 'Text:Inside render',
1526 + 'Suspend:OutsideAsync',
1527 + 'Text:Fallback:Inside render',
1528 + 'Text:Fallback:Outside render',
1529 + 'Text:Outside render',
1530 + ]);
1531 + expect(ReactNoop).toMatchRenderedOutput(
1532 + <>
1533 + <span prop="Inside" />
1534 + <span prop="Outside" />
1535 + </>,
1536 + );
1537
1540 - // Timing out should commit the fallback and destroy inner layout effects.
1541 - await advanceTimers(1000);
1542 - assertLog([
1543 - 'Text:Inside destroy layout',
1544 - 'Text:Fallback:Inside create layout',
1545 - 'Text:Fallback:Outside create layout',
1546 - ]);
1547 - await waitForAll([
1548 - 'Text:Fallback:Inside create passive',
1549 - 'Text:Fallback:Outside create passive',
1550 - ]);
1551 - expect(ReactNoop).toMatchRenderedOutput(
1552 - <>
1553 - <span prop="Inside" hidden={true} />
1554 - <span prop="Fallback:Inside" />
1555 - <span prop="Fallback:Outside" />
1556 - <span prop="Outside" />
1557 - </>,
1558 - );
1538 + // Timing out should commit the fallback and destroy inner layout effects.
1539 + await jest.runAllTimers();
1540 + assertLog([
1541 + 'Text:Inside destroy layout',
1542 + 'Text:Fallback:Inside create layout',
1543 + 'Text:Fallback:Outside create layout',
1544 + ]);
1545 + await waitForAll([
1546 + 'Text:Fallback:Inside create passive',
1547 + 'Text:Fallback:Outside create passive',
1548 + ]);
1549 + expect(ReactNoop).toMatchRenderedOutput(
1550 + <>
1551 + <span prop="Inside" hidden={true} />
1552 + <span prop="Fallback:Inside" />
1553 + <span prop="Fallback:Outside" />
1554 + <span prop="Outside" />
1555 + </>,
1556 + );
1557 + });
1558
1559 // Suspend the fallback and verify that it's effects get cleaned up as well
1561 - act(() => {
1560 + await act(async () => {
1561 ReactNoop.render(
1562 <App
1563 fallbackChildren={<AsyncText text="FallbackAsync" ms={1000} />}
1564 outerChildren={<AsyncText text="OutsideAsync" ms={1000} />}
1565 />,
1566 );
1568 - });
1569 - assertLog([
1570 - 'Text:Inside render',
1571 - 'Suspend:OutsideAsync',
1572 - 'Text:Fallback:Inside render',
1573 - 'Suspend:FallbackAsync',
1574 - 'Text:Fallback:Fallback render',
1575 - 'Text:Fallback:Outside render',
1576 - 'Text:Outside render',
1577 - ]);
1578 - expect(ReactNoop).toMatchRenderedOutput(
1579 - <>
1580 - <span prop="Inside" hidden={true} />
1581 - <span prop="Fallback:Inside" />
1582 - <span prop="Fallback:Outside" />
1583 - <span prop="Outside" />
1584 - </>,
1585 - );
1567 + await waitFor([
1568 + 'Text:Inside render',
1569 + 'Suspend:OutsideAsync',
1570 + 'Text:Fallback:Inside render',
1571 + 'Suspend:FallbackAsync',
1572 + 'Text:Fallback:Fallback render',
1573 + 'Text:Fallback:Outside render',
1574 + 'Text:Outside render',
1575 + ]);
1576 + expect(ReactNoop).toMatchRenderedOutput(
1577 + <>
1578 + <span prop="Inside" hidden={true} />
1579 + <span prop="Fallback:Inside" />
1580 + <span prop="Fallback:Outside" />
1581 + <span prop="Outside" />
1582 + </>,
1583 + );
1584
1587 - // Timing out should commit the inner fallback and destroy outer fallback layout effects.
1588 - await advanceTimers(1000);
1589 - assertLog([
1590 - 'Text:Fallback:Inside destroy layout',
1591 - 'Text:Fallback:Fallback create layout',
1592 - ]);
1593 - await waitForAll(['Text:Fallback:Fallback create passive']);
1594 - expect(ReactNoop).toMatchRenderedOutput(
1595 - <>
1596 - <span prop="Inside" hidden={true} />
1597 - <span prop="Fallback:Inside" hidden={true} />
1598 - <span prop="Fallback:Fallback" />
1599 - <span prop="Fallback:Outside" />
1600 - <span prop="Outside" />
1601 - </>,
1602 - );
1585 + // Timing out should commit the inner fallback and destroy outer fallback layout effects.
1586 + await jest.runAllTimers();
1587 + assertLog([
1588 + 'Text:Fallback:Inside destroy layout',
1589 + 'Text:Fallback:Fallback create layout',
1590 + ]);
1591 + await waitForAll(['Text:Fallback:Fallback create passive']);
1592 + expect(ReactNoop).toMatchRenderedOutput(
1593 + <>
1594 + <span prop="Inside" hidden={true} />
1595 + <span prop="Fallback:Inside" hidden={true} />
1596 + <span prop="Fallback:Fallback" />
1597 + <span prop="Fallback:Outside" />
1598 + <span prop="Outside" />
1599 + </>,
1600 + );
1601 + });
1602
1603 // Resolving both resources should cleanup fallback effects and recreate main effects
1604 await act(async () => {
@@ -1670,7 +1669,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
1669 );
1670
1671 // Suspend both the outer boundary and the fallback
1673 - act(() => {
1672 + await act(async () => {
1673 ReactNoop.render(
1674 <App
1675 outerChildren={<AsyncText text="OutsideAsync" ms={1000} />}
@@ -1678,7 +1677,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1677 />,
1678 );
1679 });
1681 - await advanceTimers(1000);
1680 assertLog([
1681 'Text:Inside render',
1682 'Suspend:OutsideAsync',
@@ -1690,8 +1688,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1688 'Text:Inside destroy layout',
1689 'Text:Fallback:Fallback create layout',
1690 'Text:Fallback:Outside create layout',
1693 - ]);
1694 - await waitForAll([
1691 'Text:Fallback:Fallback create passive',
1692 'Text:Fallback:Outside create passive',
1693 ]);
@@ -1795,32 +1791,35 @@ describe('ReactSuspenseEffectsSemantics', () => {
1791
1792 // Suspending a component in the middle of the tree
1793 // should still properly cleanup effects deeper in the tree
1798 - act(() => {
1794 + await act(async () => {
1795 ReactNoop.render(<App shouldSuspend={true} />);
1800 - });
1801 - assertLog([
1802 - 'Suspend:Suspend',
1803 - 'Text:Fallback render',
1804 - 'Text:Outside render',
1805 - ]);
1806 - expect(ReactNoop).toMatchRenderedOutput(
1807 - <>
1808 - <span prop="Inside" />
1809 - <span prop="Outside" />
1810 - </>,
1811 - );
1796 + await waitFor([
1797 + 'Suspend:Suspend',
1798 + 'Text:Fallback render',
1799 + 'Text:Outside render',
1800 + ]);
1801 + expect(ReactNoop).toMatchRenderedOutput(
1802 + <>
1803 + <span prop="Inside" />
1804 + <span prop="Outside" />
1805 + </>,
1806 + );
1807
1813 - // Timing out should commit the inner fallback and destroy outer fallback layout effects.
1814 - await advanceTimers(1000);
1815 - assertLog(['Text:Inside destroy layout', 'Text:Fallback create layout']);
1816 - await waitForAll(['Text:Fallback create passive']);
1817 - expect(ReactNoop).toMatchRenderedOutput(
1818 - <>
1819 - <span prop="Inside" hidden={true} />
1820 - <span prop="Fallback" />
1821 - <span prop="Outside" />
1822 - </>,
1823 - );
1808 + // Timing out should commit the inner fallback and destroy outer fallback layout effects.
1809 + await jest.runAllTimers();
1810 + assertLog([
1811 + 'Text:Inside destroy layout',
1812 + 'Text:Fallback create layout',
1813 + ]);
1814 + await waitForAll(['Text:Fallback create passive']);
1815 + expect(ReactNoop).toMatchRenderedOutput(
1816 + <>
1817 + <span prop="Inside" hidden={true} />
1818 + <span prop="Fallback" />
1819 + <span prop="Outside" />
1820 + </>,
1821 + );
1822 + });
1823
1824 // Resolving should cleanup.
1825 await act(async () => {
@@ -2390,43 +2389,43 @@ describe('ReactSuspenseEffectsSemantics', () => {
2389 );
2390
2391 // Schedule an update that causes React to suspend.
2393 - act(() => {
2392 + await act(async () => {
2393 ReactNoop.render(
2394 <App>
2395 <AsyncText text="Async_1" ms={1000} />
2396 <AsyncText text="Async_2" ms={2000} />
2397 </App>,
2398 );
2400 - });
2401 - assertLog([
2402 - 'Text:Function render',
2403 - 'Suspend:Async_1',
2404 - 'Suspend:Async_2',
2405 - 'ClassText:Class render',
2406 - 'ClassText:Fallback render',
2407 - ]);
2408 - expect(ReactNoop).toMatchRenderedOutput(
2409 - <>
2410 - <span prop="Function" />
2411 - <span prop="Class" />
2412 - </>,
2413 - );
2399 + await waitFor([
2400 + 'Text:Function render',
2401 + 'Suspend:Async_1',
2402 + 'Suspend:Async_2',
2403 + 'ClassText:Class render',
2404 + 'ClassText:Fallback render',
2405 + ]);
2406 + expect(ReactNoop).toMatchRenderedOutput(
2407 + <>
2408 + <span prop="Function" />
2409 + <span prop="Class" />
2410 + </>,
2411 + );
2412
2415 - await advanceTimers(1000);
2413 + await jest.runAllTimers();
2414
2417 - // Timing out should commit the fallback and destroy inner layout effects.
2418 - assertLog([
2419 - 'Text:Function destroy layout',
2420 - 'ClassText:Class componentWillUnmount',
2421 - 'ClassText:Fallback componentDidMount',
2422 - ]);
2423 - expect(ReactNoop).toMatchRenderedOutput(
2424 - <>
2425 - <span prop="Function" hidden={true} />
2426 - <span prop="Class" hidden={true} />
2427 - <span prop="Fallback" />
2428 - </>,
2429 - );
2415 + // Timing out should commit the fallback and destroy inner layout effects.
2416 + assertLog([
2417 + 'Text:Function destroy layout',
2418 + 'ClassText:Class componentWillUnmount',
2419 + 'ClassText:Fallback componentDidMount',
2420 + ]);
2421 + expect(ReactNoop).toMatchRenderedOutput(
2422 + <>
2423 + <span prop="Function" hidden={true} />
2424 + <span prop="Class" hidden={true} />
2425 + <span prop="Fallback" />
2426 + </>,
2427 + );
2428 + });
2429
2430 // Resolving the suspended resource should re-create inner layout effects.
2431 await act(async () => {
@@ -2549,40 +2548,40 @@ describe('ReactSuspenseEffectsSemantics', () => {
2548
2549 // Schedule an update that causes React to suspend.
2550 textToRead = 'A';
2552 - act(() => {
2551 + await act(async () => {
2552 ReactNoop.render(<App />);
2554 - });
2555 - assertLog([
2556 - 'Text:Function render',
2557 - 'Suspender "A" render',
2558 - 'Suspend:A',
2559 - 'ClassText:Class render',
2560 - 'ClassText:Fallback render',
2561 - ]);
2562 - expect(ReactNoop).toMatchRenderedOutput(
2563 - <>
2564 - <span prop="Function" />
2565 - <span prop="Suspender" />
2566 - <span prop="Class" />
2567 - </>,
2568 - );
2553 + await waitFor([
2554 + 'Text:Function render',
2555 + 'Suspender "A" render',
2556 + 'Suspend:A',
2557 + 'ClassText:Class render',
2558 + 'ClassText:Fallback render',
2559 + ]);
2560 + expect(ReactNoop).toMatchRenderedOutput(
2561 + <>
2562 + <span prop="Function" />
2563 + <span prop="Suspender" />
2564 + <span prop="Class" />
2565 + </>,
2566 + );
2567
2570 - await advanceTimers(1000);
2568 + await jest.runAllTimers();
2569
2572 - // Timing out should commit the fallback and destroy inner layout effects.
2573 - assertLog([
2574 - 'Text:Function destroy layout',
2575 - 'ClassText:Class componentWillUnmount',
2576 - 'ClassText:Fallback componentDidMount',
2577 - ]);
2578 - expect(ReactNoop).toMatchRenderedOutput(
2579 - <>
2580 - <span prop="Function" hidden={true} />
2581 - <span prop="Suspender" hidden={true} />
2582 - <span prop="Class" hidden={true} />
2583 - <span prop="Fallback" />
2584 - </>,
2585 - );
2570 + // Timing out should commit the fallback and destroy inner layout effects.
2571 + assertLog([
2572 + 'Text:Function destroy layout',
2573 + 'ClassText:Class componentWillUnmount',
2574 + 'ClassText:Fallback componentDidMount',
2575 + ]);
2576 + expect(ReactNoop).toMatchRenderedOutput(
2577 + <>
2578 + <span prop="Function" hidden={true} />
2579 + <span prop="Suspender" hidden={true} />
2580 + <span prop="Class" hidden={true} />
2581 + <span prop="Fallback" />
2582 + </>,
2583 + );
2584 + });
2585
2586 // Resolving the suspended resource should re-create inner layout effects.
2587 textToRead = 'B';
@@ -2712,7 +2711,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
2711 );
2712 }
2713
2715 - act(() => {
2714 + await act(async () => {
2715 ReactNoop.renderLegacySyncRoot(<App />);
2716 });
2717 assertLog([
@@ -2730,7 +2729,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
2729 expect(ReactNoop).toMatchRenderedOutput(null);
2730
2731 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2733 - act(() => {
2732 + await act(async () => {
2733 ReactNoop.renderLegacySyncRoot(
2734 <App children={<AsyncText text="Async" ms={1000} />} />,
2735 );
@@ -2811,7 +2810,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
2810 );
2811
2812 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2814 - act(() => {
2813 + await act(async () => {
2814 ReactNoop.render(
2815 <App children={<AsyncText text="Async" ms={1000} />} />,
2816 );
@@ -2829,6 +2828,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
2828 'RefCheckerOuter refCallback value? false',
2829 'RefCheckerInner:refCallback destroy layout ref? false',
2830 'Text:Fallback create layout',
2831 + 'Text:Fallback create passive',
2832 ]);
2833 expect(ReactNoop).toMatchRenderedOutput(
2834 <>
@@ -2843,7 +2843,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
2843 await resolveText('Async');
2844 });
2845 assertLog([
2846 - 'Text:Fallback create passive',
2846 'AsyncText:Async render',
2847 'RefCheckerOuter render',
2848 'RefCheckerInner:refObject render',
@@ -2917,7 +2916,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
2916 expect(ReactNoop).toMatchRenderedOutput(null);
2917
2918 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2920 - act(() => {
2919 + await act(async () => {
2920 ReactNoop.render(
2921 <App children={<AsyncText text="Async" ms={1000} />} />,
2922 );
@@ -2937,6 +2936,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
2936 'RefCheckerOuter refCallback value? false',
2937 'RefCheckerInner:refCallback destroy layout ref? false',
2938 'Text:Fallback create layout',
2939 + 'Text:Fallback create passive',
2940 ]);
2941 expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
2942
@@ -2945,7 +2945,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
2945 await resolveText('Async');
2946 });
2947 assertLog([
2948 - 'Text:Fallback create passive',
2948 'AsyncText:Async render',
2949 'RefCheckerOuter render',
2950 'ClassComponent:refObject render',
@@ -3021,7 +3020,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
3020 expect(ReactNoop).toMatchRenderedOutput(null);
3021
3022 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
3024 - act(() => {
3023 + await act(async () => {
3024 ReactNoop.render(
3025 <App children={<AsyncText text="Async" ms={1000} />} />,
3026 );
@@ -3041,6 +3040,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
3040 'RefCheckerOuter refCallback value? false',
3041 'RefCheckerInner:refCallback destroy layout ref? false',
3042 'Text:Fallback create layout',
3043 + 'Text:Fallback create passive',
3044 ]);
3045 expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
3046
@@ -3049,7 +3049,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
3049 await resolveText('Async');
3050 });
3051 assertLog([
3052 - 'Text:Fallback create passive',
3052 'AsyncText:Async render',
3053 'RefCheckerOuter render',
3054 'FunctionComponent render',
@@ -3130,7 +3129,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
3129 expect(ReactNoop).toMatchRenderedOutput(null);
3130
3131 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
3133 - act(() => {
3132 + await act(async () => {
3133 ReactNoop.render(
3134 <App children={<AsyncText text="Async" ms={1000} />} />,
3135 );
@@ -3143,6 +3142,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
3142 'Text:Fallback render',
3143 'RefChecker destroy layout ref? true',
3144 'Text:Fallback create layout',
3145 + 'Text:Fallback create passive',
3146 ]);
3147 expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
3148
@@ -3151,7 +3151,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
3151 await resolveText('Async');
3152 });
3153 assertLog([
3154 - 'Text:Fallback create passive',
3154 'AsyncText:Async render',
3155 'RefChecker render',
3156 'Text:Fallback destroy layout',
packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js
+59 -19
@@ -17,6 +17,7 @@ let act;
17 let container;
18 let waitForAll;
19 let assertLog;
20 +let fakeModuleCache;
21
22 describe('ReactSuspenseEffectsSemanticsDOM', () => {
23 beforeEach(() => {
@@ -34,14 +35,53 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
35
36 container = document.createElement('div');
37 document.body.appendChild(container);
38 +
39 + fakeModuleCache = new Map();
40 });
41
42 afterEach(() => {
43 document.body.removeChild(container);
44 });
45
43 - async function fakeImport(result) {
44 - return {default: result};
46 + async function fakeImport(Component) {
47 + const record = fakeModuleCache.get(Component);
48 + if (record === undefined) {
49 + const newRecord = {
50 + status: 'pending',
51 + value: {default: Component},
52 + pings: [],
53 + then(ping) {
54 + switch (newRecord.status) {
55 + case 'pending': {
56 + newRecord.pings.push(ping);
57 + return;
58 + }
59 + case 'resolved': {
60 + ping(newRecord.value);
61 + return;
62 + }
63 + case 'rejected': {
64 + throw newRecord.value;
65 + }
66 + }
67 + },
68 + };
69 + fakeModuleCache.set(Component, newRecord);
70 + return newRecord;
71 + }
72 + return record;
73 + }
74 +
75 + function resolveFakeImport(moduleName) {
76 + const record = fakeModuleCache.get(moduleName);
77 + if (record === undefined) {
78 + throw new Error('Module not found');
79 + }
80 + if (record.status !== 'pending') {
81 + throw new Error('Module already resolved');
82 + }
83 + record.status = 'resolved';
84 + record.pings.forEach(ping => ping(record.value));
85 }
86
87 function Text(props) {
@@ -49,7 +89,7 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
89 return props.text;
90 }
91
52 - it('should not cause a cycle when combined with a render phase update', () => {
92 + it('should not cause a cycle when combined with a render phase update', async () => {
93 let scheduleSuspendingUpdate;
94
95 function App() {
@@ -79,22 +119,22 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
119 return <div ref={setRef} />;
120 }
121
82 - const promise = Promise.resolve();
122 + const neverResolves = {then() {}};
123
124 function ComponentThatSuspendsOnUpdate({shouldSuspend}) {
125 if (shouldSuspend) {
126 // Fake Suspend
87 - throw promise;
127 + throw neverResolves;
128 }
129 return null;
130 }
131
92 - act(() => {
132 + await act(async () => {
133 const root = ReactDOMClient.createRoot(container);
134 root.render(<App />);
135 });
136
97 - act(() => {
137 + await act(async () => {
138 scheduleSuspendingUpdate();
139 });
140 });
@@ -142,12 +182,12 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
182 }
183
184 const root = ReactDOMClient.createRoot(container);
145 - act(() => {
185 + await act(async () => {
186 root.render(<Parent swap={false} />);
187 });
188 assertLog(['Loading...']);
189
150 - await LazyChildA;
190 + await resolveFakeImport(ChildA);
191 await waitForAll(['A', 'Ref mount: A']);
192 expect(container.innerHTML).toBe('<span>A</span>');
193
@@ -160,7 +200,7 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
200 '<span style="display: none;">A</span>Loading...',
201 );
202
163 - await LazyChildB;
203 + await resolveFakeImport(ChildB);
204 await waitForAll(['B', 'Ref mount: B']);
205 expect(container.innerHTML).toBe('<span>B</span>');
206 });
@@ -202,12 +242,12 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
242 }
243
244 const root = ReactDOMClient.createRoot(container);
205 - act(() => {
245 + await act(async () => {
246 root.render(<Parent swap={false} />);
247 });
248 assertLog(['Loading...']);
249
210 - await LazyChildA;
250 + await resolveFakeImport(ChildA);
251 await waitForAll(['A', 'Did mount: A']);
252 expect(container.innerHTML).toBe('A');
253
@@ -218,7 +258,7 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
258 assertLog(['Loading...', 'Will unmount: A']);
259 expect(container.innerHTML).toBe('Loading...');
260
221 - await LazyChildB;
261 + await resolveFakeImport(ChildB);
262 await waitForAll(['B', 'Did mount: B']);
263 expect(container.innerHTML).toBe('B');
264 });
@@ -254,12 +294,12 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
294 }
295
296 const root = ReactDOMClient.createRoot(container);
257 - act(() => {
297 + await act(async () => {
298 root.render(<Parent swap={false} />);
299 });
300 assertLog(['Loading...']);
301
262 - await LazyChildA;
302 + await resolveFakeImport(ChildA);
303 await waitForAll(['A', 'Did mount: A']);
304 expect(container.innerHTML).toBe('A');
305
@@ -321,12 +361,12 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
361 }
362
363 const root = ReactDOMClient.createRoot(container);
324 - act(() => {
364 + await act(async () => {
365 root.render(<Parent swap={false} />);
366 });
367 assertLog(['Loading...']);
368
329 - await LazyChildA;
369 + await resolveFakeImport(ChildA);
370 await waitForAll(['A', 'Ref mount: A']);
371 expect(container.innerHTML).toBe('<span>A</span>');
372
@@ -384,12 +424,12 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
424 }
425
426 const root = ReactDOMClient.createRoot(container);
387 - act(() => {
427 + await act(async () => {
428 root.render(<Parent swap={false} />);
429 });
430 assertLog(['Loading...']);
431
392 - await LazyChildA;
432 + await resolveFakeImport(ChildA);
433 await waitForAll(['A', 'Did mount: A']);
434 expect(container.innerHTML).toBe('A');
435
packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
+22 -24
@@ -138,14 +138,14 @@ describe('ReactSuspenseFuzz', () => {
138 return resolvedText;
139 }
140
141 - function resolveAllTasks() {
141 + async function resolveAllTasks() {
142 Scheduler.unstable_flushAllWithoutAsserting();
143 let elapsedTime = 0;
144 while (pendingTasks && pendingTasks.size > 0) {
145 if ((elapsedTime += 1000) > 1000000) {
146 throw new Error('Something did not resolve properly.');
147 }
148 - act(() => {
148 + await act(async () => {
149 ReactNoop.batchedUpdates(() => {
150 jest.advanceTimersByTime(1000);
151 });
@@ -154,7 +154,7 @@ describe('ReactSuspenseFuzz', () => {
154 }
155 }
156
157 - function testResolvedOutput(unwrappedChildren) {
157 + async function testResolvedOutput(unwrappedChildren) {
158 const children = (
159 <Suspense fallback="Loading...">{unwrappedChildren}</Suspense>
160 );
@@ -166,17 +166,15 @@ describe('ReactSuspenseFuzz', () => {
166 {children}
167 </ShouldSuspendContext.Provider>,
168 );
169 - resolveAllTasks();
169 + await resolveAllTasks();
170 const expectedOutput = expectedRoot.getChildrenAsJSX();
171
172 - gate(flags => {
173 - resetCache();
174 - ReactNoop.renderLegacySyncRoot(children);
175 - resolveAllTasks();
176 - const legacyOutput = ReactNoop.getChildrenAsJSX();
177 - expect(legacyOutput).toEqual(expectedOutput);
178 - ReactNoop.renderLegacySyncRoot(null);
179 - });
172 + resetCache();
173 + ReactNoop.renderLegacySyncRoot(children);
174 + await resolveAllTasks();
175 + const legacyOutput = ReactNoop.getChildrenAsJSX();
176 + expect(legacyOutput).toEqual(expectedOutput);
177 + ReactNoop.renderLegacySyncRoot(null);
178 }
179
180 function pickRandomWeighted(rand, options) {
@@ -298,10 +296,10 @@ describe('ReactSuspenseFuzz', () => {
296 return {Container, Text, testResolvedOutput, generateTestCase};
297 }
298
301 - it('basic cases', () => {
299 + it('basic cases', async () => {
300 // This demonstrates that the testing primitives work
301 const {Container, Text, testResolvedOutput} = createFuzzer();
304 - testResolvedOutput(
302 + await testResolvedOutput(
303 <Container updates={[{remountAfter: 150}]}>
304 <Text
305 text="Hi"
@@ -312,7 +310,7 @@ describe('ReactSuspenseFuzz', () => {
310 );
311 });
312
315 - it(`generative tests (random seed: ${SEED})`, () => {
313 + it(`generative tests (random seed: ${SEED})`, async () => {
314 const {generateTestCase, testResolvedOutput} = createFuzzer();
315
316 const rand = Random.create(SEED);
@@ -323,7 +321,7 @@ describe('ReactSuspenseFuzz', () => {
321 for (let i = 0; i < NUMBER_OF_TEST_CASES; i++) {
322 const randomTestCase = generateTestCase(rand, ELEMENTS_PER_CASE);
323 try {
326 - testResolvedOutput(randomTestCase);
324 + await testResolvedOutput(randomTestCase);
325 } catch (e) {
326 console.log(`
327 Failed fuzzy test case:
@@ -339,9 +337,9 @@ Random seed is ${SEED}
337 });
338
339 describe('hard-coded cases', () => {
342 - it('1', () => {
340 + it('1', async () => {
341 const {Text, testResolvedOutput} = createFuzzer();
344 - testResolvedOutput(
342 + await testResolvedOutput(
343 <>
344 <Text
345 initialDelay={20}
@@ -360,9 +358,9 @@ Random seed is ${SEED}
358 );
359 });
360
363 - it('2', () => {
361 + it('2', async () => {
362 const {Text, Container, testResolvedOutput} = createFuzzer();
365 - testResolvedOutput(
363 + await testResolvedOutput(
364 <>
365 <Suspense fallback="Loading...">
366 <Text initialDelay={7200} text="A" />
@@ -378,9 +376,9 @@ Random seed is ${SEED}
376 );
377 });
378
381 - it('3', () => {
379 + it('3', async () => {
380 const {Text, Container, testResolvedOutput} = createFuzzer();
383 - testResolvedOutput(
381 + await testResolvedOutput(
382 <>
383 <Suspense fallback="Loading...">
384 <Text
@@ -412,9 +410,9 @@ Random seed is ${SEED}
410 );
411 });
412
415 - it('4', () => {
413 + it('4', async () => {
414 const {Text, testResolvedOutput} = createFuzzer();
417 - testResolvedOutput(
415 + await testResolvedOutput(
416 <React.Suspense fallback="Loading...">
417 <React.Suspense>
418 <React.Suspense>
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+5 -5
@@ -2275,7 +2275,7 @@ describe('ReactSuspenseList', () => {
2275 );
2276
2277 // Update the row adjacent to the list
2278 - act(() => updateAdjacent('C'));
2278 + await act(async () => updateAdjacent('C'));
2279
2280 assertLog(['C']);
2281
@@ -2332,7 +2332,7 @@ describe('ReactSuspenseList', () => {
2332 const previousInst = setAsyncB;
2333
2334 // During an update we suspend on B.
2335 - act(() => setAsyncB(true));
2335 + await act(async () => setAsyncB(true));
2336
2337 assertLog([
2338 'Suspend! [B]',
@@ -2350,7 +2350,7 @@ describe('ReactSuspenseList', () => {
2350
2351 // Before we resolve we'll rerender the whole list.
2352 // This should leave the tree intact.
2353 - act(() => ReactNoop.render(<Foo updateList={true} />));
2353 + await act(async () => ReactNoop.render(<Foo updateList={true} />));
2354
2355 assertLog(['A', 'Suspend! [B]', 'Loading B']);
2356
@@ -2421,7 +2421,7 @@ describe('ReactSuspenseList', () => {
2421 const previousInst = setAsyncB;
2422
2423 // During an update we suspend on B.
2424 - act(() => setAsyncB(true));
2424 + await act(async () => setAsyncB(true));
2425
2426 assertLog([
2427 'Suspend! [B]',
@@ -2439,7 +2439,7 @@ describe('ReactSuspenseList', () => {
2439
2440 // Before we resolve we'll rerender the whole list.
2441 // This should leave the tree intact.
2442 - act(() => ReactNoop.render(<Foo updateList={true} />));
2442 + await act(async () => ReactNoop.render(<Foo updateList={true} />));
2443
2444 assertLog(['A', 'Suspend! [B]', 'Loading B']);
2445
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+4 -4
@@ -2086,7 +2086,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2086 });
2087
2088 // TODO: assert toErrorDev() when the warning is implemented again.
2089 - act(() => {
2089 + await act(async () => {
2090 ReactNoop.flushSync(() => _setShow(true));
2091 });
2092 });
@@ -2113,7 +2113,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2113 });
2114
2115 // TODO: assert toErrorDev() when the warning is implemented again.
2116 - act(() => {
2116 + await act(async () => {
2117 ReactNoop.flushSync(() => show());
2118 });
2119 });
@@ -2142,7 +2142,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2142 assertLog(['Suspend! [A]']);
2143 expect(ReactNoop).toMatchRenderedOutput('Loading...');
2144
2145 - act(() => {
2145 + await act(async () => {
2146 ReactNoop.flushSync(() => showB());
2147 });
2148
@@ -2173,7 +2173,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2173 });
2174
2175 // TODO: assert toErrorDev() when the warning is implemented again.
2176 - act(() => {
2176 + await act(async () => {
2177 ReactNoop.flushSync(() => _setShow(true));
2178 });
2179 },
packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js
+1 -1
@@ -308,7 +308,7 @@ describe('updaters', () => {
308 expect(allSchedulerTypes).toEqual([[null], [Suspender]]);
309
310 expect(resolver).not.toBeNull();
311 - await act(() => {
311 + await act(async () => {
312 resolver('abc');
313 return promise;
314 });
packages/react-reconciler/src/__tests__/useEffectEvent-test.js
+16 -16
@@ -88,7 +88,7 @@ describe('useEffectEvent', () => {
88 </>,
89 );
90
91 - act(button.current.increment);
91 + await act(async () => button.current.increment());
92 assertLog(['Increment', 'Count: 1']);
93 expect(ReactNoop).toMatchRenderedOutput(
94 <>
@@ -97,7 +97,7 @@ describe('useEffectEvent', () => {
97 </>,
98 );
99
100 - act(button.current.increment);
100 + await act(async () => button.current.increment());
101 assertLog([
102 'Increment',
103 // Event should use the updated callback function closed over the new value.
@@ -121,7 +121,7 @@ describe('useEffectEvent', () => {
121 );
122
123 // Event uses the new prop
124 - act(button.current.increment);
124 + await act(async () => button.current.increment());
125 assertLog(['Increment', 'Count: 12']);
126 expect(ReactNoop).toMatchRenderedOutput(
127 <>
@@ -174,7 +174,7 @@ describe('useEffectEvent', () => {
174 </>,
175 );
176
177 - act(button.current.increment);
177 + await act(async () => button.current.increment());
178 assertLog(['Increment', 'Count: 5']);
179 expect(ReactNoop).toMatchRenderedOutput(
180 <>
@@ -183,7 +183,7 @@ describe('useEffectEvent', () => {
183 </>,
184 );
185
186 - act(button.current.multiply);
186 + await act(async () => button.current.multiply());
187 assertLog(['Increment', 'Count: 25']);
188 expect(ReactNoop).toMatchRenderedOutput(
189 <>
@@ -233,7 +233,7 @@ describe('useEffectEvent', () => {
233 </>,
234 );
235
236 - act(button.current.greet);
236 + await act(async () => button.current.greet());
237 assertLog(['Say hej', 'Greeting: undefined says hej']);
238 expect(ReactNoop).toMatchRenderedOutput(
239 <>
@@ -327,7 +327,7 @@ describe('useEffectEvent', () => {
327 </>,
328 );
329
330 - act(button.current.increment);
330 + await act(async () => button.current.increment());
331 assertLog([
332 'Increment',
333 // Effect should not re-run because the dependency hasn't changed.
@@ -340,7 +340,7 @@ describe('useEffectEvent', () => {
340 </>,
341 );
342
343 - act(button.current.increment);
343 + await act(async () => button.current.increment());
344 assertLog([
345 'Increment',
346 // Event should use the updated callback function closed over the new value.
@@ -370,7 +370,7 @@ describe('useEffectEvent', () => {
370 );
371
372 // Event uses the new prop
373 - act(button.current.increment);
373 + await act(async () => button.current.increment());
374 assertLog(['Increment', 'Count: 34']);
375 expect(ReactNoop).toMatchRenderedOutput(
376 <>
@@ -426,7 +426,7 @@ describe('useEffectEvent', () => {
426 </>,
427 );
428
429 - act(button.current.increment);
429 + await act(async () => button.current.increment());
430 assertLog([
431 'Increment',
432 // Effect should not re-run because the dependency hasn't changed.
@@ -439,7 +439,7 @@ describe('useEffectEvent', () => {
439 </>,
440 );
441
442 - act(button.current.increment);
442 + await act(async () => button.current.increment());
443 assertLog([
444 'Increment',
445 // Event should use the updated callback function closed over the new value.
@@ -469,7 +469,7 @@ describe('useEffectEvent', () => {
469 );
470
471 // Event uses the new prop
472 - act(button.current.increment);
472 + await act(async () => button.current.increment());
473 assertLog(['Increment', 'Count: 34']);
474 expect(ReactNoop).toMatchRenderedOutput(
475 <>
@@ -531,7 +531,7 @@ describe('useEffectEvent', () => {
531 </>,
532 );
533
534 - act(button.current.increment);
534 + await act(async () => button.current.increment());
535 assertLog([
536 'Increment',
537 // Effect should not re-run because the dependency hasn't changed.
@@ -544,7 +544,7 @@ describe('useEffectEvent', () => {
544 </>,
545 );
546
547 - act(button.current.increment);
547 + await act(async () => button.current.increment());
548 assertLog([
549 'Increment',
550 // Event should use the updated callback function closed over the new value.
@@ -574,7 +574,7 @@ describe('useEffectEvent', () => {
574 );
575
576 // Event uses the new prop
577 - act(button.current.increment);
577 + await act(async () => button.current.increment());
578 assertLog(['Increment', 'Count: 34']);
579 expect(ReactNoop).toMatchRenderedOutput(
580 <>
@@ -849,7 +849,7 @@ describe('useEffectEvent', () => {
849 ),
850 );
851 assertLog(['Add to cart', 'url: /shop/1, numberOfItems: 0']);
852 - act(button.current.addToCart);
852 + await act(async () => button.current.addToCart());
853 assertLog(['Add to cart']);
854
855 await act(async () =>
packages/react-refresh/src/__tests__/ReactFresh-test.js
+3 -2
@@ -3783,7 +3783,7 @@ describe('ReactFresh', () => {
3783 }
3784
3785 // This simulates the scenario in https://github.com/facebook/react/issues/17626
3786 - it('can inject the runtime after the renderer executes', () => {
3786 + it('can inject the runtime after the renderer executes', async () => {
3787 if (__DEV__) {
3788 initFauxDevToolsHook();
3789
@@ -3792,7 +3792,8 @@ describe('ReactFresh', () => {
3792 React = require('react');
3793 ReactDOM = require('react-dom');
3794 Scheduler = require('scheduler');
3795 - act = require('jest-react').act;
3795 + act = require('react-dom/test-utils').act;
3796 + internalAct = require('jest-react').act;
3797
3798 // Important! Inject into the global hook *after* ReactDOM runs:
3799 ReactFreshRuntime = require('react-refresh/runtime');
packages/react/src/__tests__/ReactProfiler-test.internal.js
+1 -1
@@ -346,7 +346,7 @@ describe(`onRender`, () => {
346 Scheduler.unstable_advanceTime(20); // 30 -> 50
347
348 // Updating a sibling should not report a re-render.
349 - act(updateProfilerSibling);
349 + await act(async () => updateProfilerSibling());
350
351 expect(callback).not.toHaveBeenCalled();
352 });