Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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