@samitouri / QOS-React-2 / commits / f36ab0e375

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