Remove timers from ReactDOMSuspensePlaceholder tests (#26346)
Our internal `act` implementation flushes Jest's fake timer queue as a way to force Suspense fallbacks to appear. So we should avoid using timers in our internal tests.
Andrew Clark committed
Mar 8, 2023 at 14:45 UTC
f36ab0e375b6223d4cc5852f649e93c1cb238bac
1 file changed
+73
-36
packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js
+73
-36
@@ -12,10 +12,9 @@
12
let React;
13
let ReactDOM;
14
let Suspense;
15
-let ReactCache;
15
let Scheduler;
17
-let TextResource;
16
let act;
17
+let textCache;
18
19
describe('ReactDOMSuspensePlaceholder', () => {
20
let container;
@@ -24,48 +23,78 @@ describe('ReactDOMSuspensePlaceholder', () => {
23
jest.resetModules();
24
React = require('react');
25
ReactDOM = require('react-dom');
27
- ReactCache = require('react-cache');
26
Scheduler = require('scheduler');
27
act = require('internal-test-utils').act;
28
Suspense = React.Suspense;
29
container = document.createElement('div');
30
document.body.appendChild(container);
31
34
- TextResource = ReactCache.unstable_createResource(
35
- ([text, ms = 0]) => {
36
- return new Promise((resolve, reject) =>
37
- setTimeout(() => {
38
- resolve(text);
39
- }, ms),
40
- );
41
- },
42
- ([text, ms]) => text,
43
- );
32
+ textCache = new Map();
33
});
34
35
afterEach(() => {
36
document.body.removeChild(container);
37
});
38
50
- function advanceTimers(ms) {
51
- // Note: This advances Jest's virtual time but not React's. Use
52
- // ReactNoop.expire for that.
53
- if (typeof ms !== 'number') {
54
- throw new Error('Must specify ms');
39
+ function resolveText(text) {
40
+ const record = textCache.get(text);
41
+ if (record === undefined) {
42
+ const newRecord = {
43
+ status: 'resolved',
44
+ value: text,
45
+ };
46
+ textCache.set(text, newRecord);
47
+ } else if (record.status === 'pending') {
48
+ const thenable = record.value;
49
+ record.status = 'resolved';
50
+ record.value = text;
51
+ thenable.pings.forEach(t => t());
52
+ }
53
+ }
54
+
55
+ function readText(text) {
56
+ const record = textCache.get(text);
57
+ if (record !== undefined) {
58
+ switch (record.status) {
59
+ case 'pending':
60
+ Scheduler.log(`Suspend! [${text}]`);
61
+ throw record.value;
62
+ case 'rejected':
63
+ throw record.value;
64
+ case 'resolved':
65
+ return record.value;
66
+ }
67
+ } else {
68
+ Scheduler.log(`Suspend! [${text}]`);
69
+ const thenable = {
70
+ pings: [],
71
+ then(resolve) {
72
+ if (newRecord.status === 'pending') {
73
+ thenable.pings.push(resolve);
74
+ } else {
75
+ Promise.resolve().then(() => resolve(newRecord.value));
76
+ }
77
+ },
78
+ };
79
+
80
+ const newRecord = {
81
+ status: 'pending',
82
+ value: thenable,
83
+ };
84
+ textCache.set(text, newRecord);
85
+
86
+ throw thenable;
87
}
56
- jest.advanceTimersByTime(ms);
57
- // Wait until the end of the current tick
58
- // We cannot use a timer since we're faking them
59
- return Promise.resolve().then(() => {});
88
}
89
62
- function Text(props) {
63
- return props.text;
90
+ function Text({text}) {
91
+ Scheduler.log(text);
92
+ return text;
93
}
94
66
- function AsyncText(props) {
67
- const text = props.text;
68
- TextResource.read([props.text, props.ms]);
95
+ function AsyncText({text}) {
96
+ readText(text);
97
+ Scheduler.log(text);
98
return text;
99
}
100
@@ -82,7 +111,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
111
<Text text="A" />
112
</div>
113
<div ref={divs[1]}>
85
- <AsyncText ms={500} text="B" />
114
+ <AsyncText text="B" />
115
</div>
116
<div style={{display: 'inline'}} ref={divs[2]}>
117
<Text text="C" />
@@ -95,9 +124,9 @@ describe('ReactDOMSuspensePlaceholder', () => {
124
expect(window.getComputedStyle(divs[1].current).display).toEqual('none');
125
expect(window.getComputedStyle(divs[2].current).display).toEqual('none');
126
98
- await advanceTimers(500);
99
-
100
- Scheduler.unstable_flushAll();
127
+ await act(async () => {
128
+ await resolveText('B');
129
+ });
130
131
expect(window.getComputedStyle(divs[0].current).display).toEqual('block');
132
expect(window.getComputedStyle(divs[1].current).display).toEqual('block');
@@ -110,7 +139,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
139
return (
140
<Suspense fallback={<Text text="Loading..." />}>
141
<Text text="A" />
113
- <AsyncText ms={500} text="B" />
142
+ <AsyncText text="B" />
143
<Text text="C" />
144
</Suspense>
145
);
@@ -118,9 +147,9 @@ describe('ReactDOMSuspensePlaceholder', () => {
147
ReactDOM.render(<App />, container);
148
expect(container.textContent).toEqual('Loading...');
149
121
- await advanceTimers(500);
122
-
123
- Scheduler.unstable_flushAll();
150
+ await act(async () => {
151
+ await resolveText('B');
152
+ });
153
154
expect(container.textContent).toEqual('ABC');
155
});
@@ -147,7 +176,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
176
<Suspense fallback={<Text text="Loading..." />}>
177
<Sibling>Sibling</Sibling>
178
<span>
150
- <AsyncText ms={500} text="Async" />
179
+ <AsyncText text="Async" />
180
</span>
181
</Suspense>
182
);
@@ -161,8 +190,16 @@ describe('ReactDOMSuspensePlaceholder', () => {
190
'"display: none;"></span>Loading...',
191
);
192
193
+ // Update the inline display style. It will be overridden because it's
194
+ // inside a hidden fallback.
195
await act(async () => setIsVisible(true));
196
+ expect(container.innerHTML).toEqual(
197
+ '<span style="display: none;">Sibling</span><span style=' +
198
+ '"display: none;"></span>Loading...',
199
+ );
200
201
+ // Unsuspend. The style should now match the inline prop.
202
+ await act(async () => resolveText('Async'));
203
expect(container.innerHTML).toEqual(
204
'<span style="display: inline;">Sibling</span><span style="">Async</span>',
205
);