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
});