Skip to content

Commit f36ab0e

Browse files
authored
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.
1 parent 44d3807 commit f36ab0e

File tree

1 file changed

+73
-36
lines changed

1 file changed

+73
-36
lines changed

packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
let React;
1313
let ReactDOM;
1414
let Suspense;
15-
let ReactCache;
1615
let Scheduler;
17-
let TextResource;
1816
let act;
17+
let textCache;
1918

2019
describe('ReactDOMSuspensePlaceholder', () => {
2120
let container;
@@ -24,48 +23,78 @@ describe('ReactDOMSuspensePlaceholder', () => {
2423
jest.resetModules();
2524
React = require('react');
2625
ReactDOM = require('react-dom');
27-
ReactCache = require('react-cache');
2826
Scheduler = require('scheduler');
2927
act = require('internal-test-utils').act;
3028
Suspense = React.Suspense;
3129
container = document.createElement('div');
3230
document.body.appendChild(container);
3331

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();
4433
});
4534

4635
afterEach(() => {
4736
document.body.removeChild(container);
4837
});
4938

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;
5587
}
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(() => {});
6088
}
6189

62-
function Text(props) {
63-
return props.text;
90+
function Text({text}) {
91+
Scheduler.log(text);
92+
return text;
6493
}
6594

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);
6998
return text;
7099
}
71100

@@ -82,7 +111,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
82111
<Text text="A" />
83112
</div>
84113
<div ref={divs[1]}>
85-
<AsyncText ms={500} text="B" />
114+
<AsyncText text="B" />
86115
</div>
87116
<div style={{display: 'inline'}} ref={divs[2]}>
88117
<Text text="C" />
@@ -95,9 +124,9 @@ describe('ReactDOMSuspensePlaceholder', () => {
95124
expect(window.getComputedStyle(divs[1].current).display).toEqual('none');
96125
expect(window.getComputedStyle(divs[2].current).display).toEqual('none');
97126

98-
await advanceTimers(500);
99-
100-
Scheduler.unstable_flushAll();
127+
await act(async () => {
128+
await resolveText('B');
129+
});
101130

102131
expect(window.getComputedStyle(divs[0].current).display).toEqual('block');
103132
expect(window.getComputedStyle(divs[1].current).display).toEqual('block');
@@ -110,17 +139,17 @@ describe('ReactDOMSuspensePlaceholder', () => {
110139
return (
111140
<Suspense fallback={<Text text="Loading..." />}>
112141
<Text text="A" />
113-
<AsyncText ms={500} text="B" />
142+
<AsyncText text="B" />
114143
<Text text="C" />
115144
</Suspense>
116145
);
117146
}
118147
ReactDOM.render(<App />, container);
119148
expect(container.textContent).toEqual('Loading...');
120149

121-
await advanceTimers(500);
122-
123-
Scheduler.unstable_flushAll();
150+
await act(async () => {
151+
await resolveText('B');
152+
});
124153

125154
expect(container.textContent).toEqual('ABC');
126155
});
@@ -147,7 +176,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
147176
<Suspense fallback={<Text text="Loading..." />}>
148177
<Sibling>Sibling</Sibling>
149178
<span>
150-
<AsyncText ms={500} text="Async" />
179+
<AsyncText text="Async" />
151180
</span>
152181
</Suspense>
153182
);
@@ -161,8 +190,16 @@ describe('ReactDOMSuspensePlaceholder', () => {
161190
'"display: none;"></span>Loading...',
162191
);
163192

193+
// Update the inline display style. It will be overridden because it's
194+
// inside a hidden fallback.
164195
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+
);
165200

201+
// Unsuspend. The style should now match the inline prop.
202+
await act(async () => resolveText('Async'));
166203
expect(container.innerHTML).toEqual(
167204
'<span style="display: inline;">Sibling</span><span style="">Async</span>',
168205
);

0 commit comments

Comments
 (0)