Skip to content

Commit

Permalink
Remove timers from ReactDOMSuspensePlaceholder tests (#26346)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite authored Mar 8, 2023
1 parent 44d3807 commit f36ab0e
Showing 1 changed file with 73 additions and 36 deletions.
109 changes: 73 additions & 36 deletions packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
let React;
let ReactDOM;
let Suspense;
let ReactCache;
let Scheduler;
let TextResource;
let act;
let textCache;

describe('ReactDOMSuspensePlaceholder', () => {
let container;
Expand All @@ -24,48 +23,78 @@ describe('ReactDOMSuspensePlaceholder', () => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactCache = require('react-cache');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
Suspense = React.Suspense;
container = document.createElement('div');
document.body.appendChild(container);

TextResource = ReactCache.unstable_createResource(
([text, ms = 0]) => {
return new Promise((resolve, reject) =>
setTimeout(() => {
resolve(text);
}, ms),
);
},
([text, ms]) => text,
);
textCache = new Map();
});

afterEach(() => {
document.body.removeChild(container);
});

function advanceTimers(ms) {
// Note: This advances Jest's virtual time but not React's. Use
// ReactNoop.expire for that.
if (typeof ms !== 'number') {
throw new Error('Must specify ms');
function resolveText(text) {
const record = textCache.get(text);
if (record === undefined) {
const newRecord = {
status: 'resolved',
value: text,
};
textCache.set(text, newRecord);
} else if (record.status === 'pending') {
const thenable = record.value;
record.status = 'resolved';
record.value = text;
thenable.pings.forEach(t => t());
}
}

function readText(text) {
const record = textCache.get(text);
if (record !== undefined) {
switch (record.status) {
case 'pending':
Scheduler.log(`Suspend! [${text}]`);
throw record.value;
case 'rejected':
throw record.value;
case 'resolved':
return record.value;
}
} else {
Scheduler.log(`Suspend! [${text}]`);
const thenable = {
pings: [],
then(resolve) {
if (newRecord.status === 'pending') {
thenable.pings.push(resolve);
} else {
Promise.resolve().then(() => resolve(newRecord.value));
}
},
};

const newRecord = {
status: 'pending',
value: thenable,
};
textCache.set(text, newRecord);

throw thenable;
}
jest.advanceTimersByTime(ms);
// Wait until the end of the current tick
// We cannot use a timer since we're faking them
return Promise.resolve().then(() => {});
}

function Text(props) {
return props.text;
function Text({text}) {
Scheduler.log(text);
return text;
}

function AsyncText(props) {
const text = props.text;
TextResource.read([props.text, props.ms]);
function AsyncText({text}) {
readText(text);
Scheduler.log(text);
return text;
}

Expand All @@ -82,7 +111,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
<Text text="A" />
</div>
<div ref={divs[1]}>
<AsyncText ms={500} text="B" />
<AsyncText text="B" />
</div>
<div style={{display: 'inline'}} ref={divs[2]}>
<Text text="C" />
Expand All @@ -95,9 +124,9 @@ describe('ReactDOMSuspensePlaceholder', () => {
expect(window.getComputedStyle(divs[1].current).display).toEqual('none');
expect(window.getComputedStyle(divs[2].current).display).toEqual('none');

await advanceTimers(500);

Scheduler.unstable_flushAll();
await act(async () => {
await resolveText('B');
});

expect(window.getComputedStyle(divs[0].current).display).toEqual('block');
expect(window.getComputedStyle(divs[1].current).display).toEqual('block');
Expand All @@ -110,17 +139,17 @@ describe('ReactDOMSuspensePlaceholder', () => {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Text text="A" />
<AsyncText ms={500} text="B" />
<AsyncText text="B" />
<Text text="C" />
</Suspense>
);
}
ReactDOM.render(<App />, container);
expect(container.textContent).toEqual('Loading...');

await advanceTimers(500);

Scheduler.unstable_flushAll();
await act(async () => {
await resolveText('B');
});

expect(container.textContent).toEqual('ABC');
});
Expand All @@ -147,7 +176,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
<Suspense fallback={<Text text="Loading..." />}>
<Sibling>Sibling</Sibling>
<span>
<AsyncText ms={500} text="Async" />
<AsyncText text="Async" />
</span>
</Suspense>
);
Expand All @@ -161,8 +190,16 @@ describe('ReactDOMSuspensePlaceholder', () => {
'"display: none;"></span>Loading...',
);

// Update the inline display style. It will be overridden because it's
// inside a hidden fallback.
await act(async () => setIsVisible(true));
expect(container.innerHTML).toEqual(
'<span style="display: none;">Sibling</span><span style=' +
'"display: none;"></span>Loading...',
);

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

0 comments on commit f36ab0e

Please sign in to comment.