@samitouri / QOS-React / commits / 0ae2b13412

[Test] Update `flushSync` tests to use react-dom (#28490)

Updates the `flushSync` tests to use react-dom instead of ReactNoop. flushSync is primarily a react-dom API and asserting the implementation of ReactNoop is not really ideal especially since we are going to be refactoring flushSync soon to not be implemented in the reconciler internals. ReactNoop still have a flushSync api and it can still be used in other tests that are primarily about testing other functionlity and use ReactNoop as the renderer.

Josh Story committed Mar 4, 2024 at 21:40 UTC 0ae2b13412e0e4cf10a799ec645035213834556c
2 files changed +158 -55
packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
+102 -46
@@ -1,5 +1,6 @@
1 let React;
2 -let ReactNoop;
2 +let ReactDOM;
3 +let ReactDOMClient;
4 let Scheduler;
5 let act;
6 let useState;
@@ -15,7 +16,8 @@ describe('ReactFlushSync', () => {
16 jest.resetModules();
17
18 React = require('react');
18 - ReactNoop = require('react-noop-renderer');
19 + ReactDOM = require('react-dom');
20 + ReactDOMClient = require('react-dom/client');
21 Scheduler = require('scheduler');
22 act = require('internal-test-utils').act;
23 useState = React.useState;
@@ -32,7 +34,49 @@ describe('ReactFlushSync', () => {
34 return text;
35 }
36
35 - test('changes priority of updates in useEffect', async () => {
37 + function getVisibleChildren(element: Element): React$Node {
38 + const children = [];
39 + let node: any = element.firstChild;
40 + while (node) {
41 + if (node.nodeType === 1) {
42 + if (
43 + ((node.tagName !== 'SCRIPT' && node.tagName !== 'script') ||
44 + node.hasAttribute('data-meaningful')) &&
45 + node.tagName !== 'TEMPLATE' &&
46 + node.tagName !== 'template' &&
47 + !node.hasAttribute('hidden') &&
48 + !node.hasAttribute('aria-hidden')
49 + ) {
50 + const props: any = {};
51 + const attributes = node.attributes;
52 + for (let i = 0; i < attributes.length; i++) {
53 + if (
54 + attributes[i].name === 'id' &&
55 + attributes[i].value.includes(':')
56 + ) {
57 + // We assume this is a React added ID that's a non-visual implementation detail.
58 + continue;
59 + }
60 + props[attributes[i].name] = attributes[i].value;
61 + }
62 + props.children = getVisibleChildren(node);
63 + children.push(
64 + require('react').createElement(node.tagName.toLowerCase(), props),
65 + );
66 + }
67 + } else if (node.nodeType === 3) {
68 + children.push(node.data);
69 + }
70 + node = node.nextSibling;
71 + }
72 + return children.length === 0
73 + ? undefined
74 + : children.length === 1
75 + ? children[0]
76 + : children;
77 + }
78 +
79 + it('changes priority of updates in useEffect', async () => {
80 spyOnDev(console, 'error').mockImplementation(() => {});
81
82 function App() {
@@ -41,13 +85,14 @@ describe('ReactFlushSync', () => {
85 useEffect(() => {
86 if (syncState !== 1) {
87 setState(1);
44 - ReactNoop.flushSync(() => setSyncState(1));
88 + ReactDOM.flushSync(() => setSyncState(1));
89 }
90 }, [syncState, state]);
91 return <Text text={`${syncState}, ${state}`} />;
92 }
93
50 - const root = ReactNoop.createRoot();
94 + const container = document.createElement('div');
95 + const root = ReactDOMClient.createRoot(container);
96 await act(async () => {
97 React.startTransition(() => {
98 root.render(<App />);
@@ -62,7 +107,7 @@ describe('ReactFlushSync', () => {
107 );
108
109 // The remaining update is not sync
65 - ReactNoop.flushSync();
110 + ReactDOM.flushSync();
111 assertLog([]);
112
113 if (gate(flags => flags.enableUnifiedSyncLane)) {
@@ -72,7 +117,7 @@ describe('ReactFlushSync', () => {
117 await waitForPaint(['1, 1']);
118 }
119 });
75 - expect(root).toMatchRenderedOutput('1, 1');
120 + expect(getVisibleChildren(container)).toEqual('1, 1');
121
122 if (__DEV__) {
123 expect(console.error.mock.calls[0][0]).toContain(
@@ -83,7 +128,7 @@ describe('ReactFlushSync', () => {
128 }
129 });
130
86 - test('nested with startTransition', async () => {
131 + it('supports nested flushSync with startTransition', async () => {
132 let setSyncState;
133 let setState;
134 function App() {
@@ -94,20 +139,21 @@ describe('ReactFlushSync', () => {
139 return <Text text={`${syncState}, ${state}`} />;
140 }
141
97 - const root = ReactNoop.createRoot();
142 + const container = document.createElement('div');
143 + const root = ReactDOMClient.createRoot(container);
144 await act(() => {
145 root.render(<App />);
146 });
147 assertLog(['0, 0']);
102 - expect(root).toMatchRenderedOutput('0, 0');
148 + expect(getVisibleChildren(container)).toEqual('0, 0');
149
150 await act(() => {
105 - ReactNoop.flushSync(() => {
151 + ReactDOM.flushSync(() => {
152 startTransition(() => {
153 // This should be async even though flushSync is on the stack, because
154 // startTransition is closer.
155 setState(1);
110 - ReactNoop.flushSync(() => {
156 + ReactDOM.flushSync(() => {
157 // This should be async even though startTransition is on the stack,
158 // because flushSync is closer.
159 setSyncState(1);
@@ -116,14 +162,14 @@ describe('ReactFlushSync', () => {
162 });
163 // Only the sync update should have flushed
164 assertLog(['1, 0']);
119 - expect(root).toMatchRenderedOutput('1, 0');
165 + expect(getVisibleChildren(container)).toEqual('1, 0');
166 });
167 // Now the async update has flushed, too.
168 assertLog(['1, 1']);
123 - expect(root).toMatchRenderedOutput('1, 1');
169 + expect(getVisibleChildren(container)).toEqual('1, 1');
170 });
171
126 - test('flushes passive effects synchronously when they are the result of a sync render', async () => {
172 + it('flushes passive effects synchronously when they are the result of a sync render', async () => {
173 function App() {
174 useEffect(() => {
175 Scheduler.log('Effect');
@@ -131,9 +177,10 @@ describe('ReactFlushSync', () => {
177 return <Text text="Child" />;
178 }
179
134 - const root = ReactNoop.createRoot();
180 + const container = document.createElement('div');
181 + const root = ReactDOMClient.createRoot(container);
182 await act(() => {
136 - ReactNoop.flushSync(() => {
183 + ReactDOM.flushSync(() => {
184 root.render(<App />);
185 });
186 assertLog([
@@ -142,11 +189,12 @@ describe('ReactFlushSync', () => {
189 // flushSync should flush it.
190 'Effect',
191 ]);
145 - expect(root).toMatchRenderedOutput('Child');
192 + expect(getVisibleChildren(container)).toEqual('Child');
193 });
194 });
195
149 - test('do not flush passive effects synchronously after render in legacy mode', async () => {
196 + // @gate !disableLegacyMode
197 + it('does not flush passive effects synchronously after render in legacy mode', async () => {
198 function App() {
199 useEffect(() => {
200 Scheduler.log('Effect');
@@ -154,23 +202,24 @@ describe('ReactFlushSync', () => {
202 return <Text text="Child" />;
203 }
204
157 - const root = ReactNoop.createLegacyRoot();
205 + const container = document.createElement('div');
206 await act(() => {
159 - ReactNoop.flushSync(() => {
160 - root.render(<App />);
207 + ReactDOM.flushSync(() => {
208 + ReactDOM.render(<App />, container);
209 });
210 assertLog([
211 'Child',
212 // Because we're in legacy mode, we shouldn't have flushed the passive
213 // effects yet.
214 ]);
167 - expect(root).toMatchRenderedOutput('Child');
215 + expect(getVisibleChildren(container)).toEqual('Child');
216 });
217 // Effect flushes after paint.
218 assertLog(['Effect']);
219 });
220
173 - test('flush pending passive effects before scope is called in legacy mode', async () => {
221 + // @gate !disableLegacyMode
222 + it('flushes pending passive effects before scope is called in legacy mode', async () => {
223 let currentStep = 0;
224
225 function App({step}) {
@@ -181,30 +230,30 @@ describe('ReactFlushSync', () => {
230 return <Text text={step} />;
231 }
232
184 - const root = ReactNoop.createLegacyRoot();
233 + const container = document.createElement('div');
234 await act(() => {
186 - ReactNoop.flushSync(() => {
187 - root.render(<App step={1} />);
235 + ReactDOM.flushSync(() => {
236 + ReactDOM.render(<App step={1} />, container);
237 });
238 assertLog([
239 1,
240 // Because we're in legacy mode, we shouldn't have flushed the passive
241 // effects yet.
242 ]);
194 - expect(root).toMatchRenderedOutput('1');
243 + expect(getVisibleChildren(container)).toEqual('1');
244
196 - ReactNoop.flushSync(() => {
245 + ReactDOM.flushSync(() => {
246 // This should render step 2 because the passive effect has already
247 // fired, before the scope function is called.
199 - root.render(<App step={currentStep + 1} />);
248 + ReactDOM.render(<App step={currentStep + 1} />, container);
249 });
250 assertLog(['Effect: 1', 2]);
202 - expect(root).toMatchRenderedOutput('2');
251 + expect(getVisibleChildren(container)).toEqual('2');
252 });
253 assertLog(['Effect: 2']);
254 });
255
207 - test("do not flush passive effects synchronously when they aren't the result of a sync render", async () => {
256 + it("does not flush passive effects synchronously when they aren't the result of a sync render", async () => {
257 function App() {
258 useEffect(() => {
259 Scheduler.log('Effect');
@@ -212,7 +261,8 @@ describe('ReactFlushSync', () => {
261 return <Text text="Child" />;
262 }
263
215 - const root = ReactNoop.createRoot();
264 + const container = document.createElement('div');
265 + const root = ReactDOMClient.createRoot(container);
266 await act(async () => {
267 root.render(<App />);
268 await waitForPaint([
@@ -220,13 +270,13 @@ describe('ReactFlushSync', () => {
270 // Because the passive effect was not the result of a sync update, it
271 // should not flush before paint.
272 ]);
223 - expect(root).toMatchRenderedOutput('Child');
273 + expect(getVisibleChildren(container)).toEqual('Child');
274 });
275 // Effect flushes after paint.
276 assertLog(['Effect']);
277 });
278
229 - test('does not flush pending passive effects', async () => {
279 + it('does not flush pending passive effects', async () => {
280 function App() {
281 useEffect(() => {
282 Scheduler.log('Effect');
@@ -234,14 +284,15 @@ describe('ReactFlushSync', () => {
284 return <Text text="Child" />;
285 }
286
237 - const root = ReactNoop.createRoot();
287 + const container = document.createElement('div');
288 + const root = ReactDOMClient.createRoot(container);
289 await act(async () => {
290 root.render(<App />);
291 await waitForPaint(['Child']);
241 - expect(root).toMatchRenderedOutput('Child');
292 + expect(getVisibleChildren(container)).toEqual('Child');
293
294 // Passive effects are pending. Calling flushSync should not affect them.
244 - ReactNoop.flushSync();
295 + ReactDOM.flushSync();
296 // Effects still haven't fired.
297 assertLog([]);
298 });
@@ -249,14 +300,19 @@ describe('ReactFlushSync', () => {
300 assertLog(['Effect']);
301 });
302
252 - test('completely exhausts synchronous work queue even if something throws', async () => {
303 + it('completely exhausts synchronous work queue even if something throws', async () => {
304 function Throws({error}) {
305 throw error;
306 }
307
257 - const root1 = ReactNoop.createRoot();
258 - const root2 = ReactNoop.createRoot();
259 - const root3 = ReactNoop.createRoot();
308 + const container1 = document.createElement('div');
309 + const root1 = ReactDOMClient.createRoot(container1);
310 +
311 + const container2 = document.createElement('div');
312 + const root2 = ReactDOMClient.createRoot(container2);
313 +
314 + const container3 = document.createElement('div');
315 + const root3 = ReactDOMClient.createRoot(container3);
316
317 await act(async () => {
318 root1.render(<Text text="Hi" />);
@@ -270,7 +326,7 @@ describe('ReactFlushSync', () => {
326
327 let error;
328 try {
273 - ReactNoop.flushSync(() => {
329 + ReactDOM.flushSync(() => {
330 root1.render(<Throws error={aahh} />);
331 root2.render(<Throws error={nooo} />);
332 root3.render(<Text text="aww" />);
@@ -283,9 +339,9 @@ describe('ReactFlushSync', () => {
339 // earlier updates errored.
340 assertLog(['aww']);
341 // Roots 1 and 2 were unmounted.
286 - expect(root1).toMatchRenderedOutput(null);
287 - expect(root2).toMatchRenderedOutput(null);
288 - expect(root3).toMatchRenderedOutput('aww');
342 + expect(getVisibleChildren(container1)).toEqual(undefined);
343 + expect(getVisibleChildren(container2)).toEqual(undefined);
344 + expect(getVisibleChildren(container3)).toEqual('aww');
345
346 // Because there were multiple errors, React threw an AggregateError.
347 // eslint-disable-next-line no-undef
packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js
+56 -9
@@ -1,5 +1,6 @@
1 let React;
2 -let ReactNoop;
2 +let ReactDOM;
3 +let ReactDOMClient;
4 let Scheduler;
5 let act;
6 let assertLog;
@@ -36,7 +37,8 @@ describe('ReactFlushSync (AggregateError not available)', () => {
37 };
38
39 React = require('react');
39 - ReactNoop = require('react-noop-renderer');
40 + ReactDOM = require('react-dom');
41 + ReactDOMClient = require('react-dom/client');
42 Scheduler = require('scheduler');
43 act = require('internal-test-utils').act;
44
@@ -49,14 +51,59 @@ describe('ReactFlushSync (AggregateError not available)', () => {
51 return text;
52 }
53
54 + function getVisibleChildren(element: Element): React$Node {
55 + const children = [];
56 + let node: any = element.firstChild;
57 + while (node) {
58 + if (node.nodeType === 1) {
59 + if (
60 + ((node.tagName !== 'SCRIPT' && node.tagName !== 'script') ||
61 + node.hasAttribute('data-meaningful')) &&
62 + node.tagName !== 'TEMPLATE' &&
63 + node.tagName !== 'template' &&
64 + !node.hasAttribute('hidden') &&
65 + !node.hasAttribute('aria-hidden')
66 + ) {
67 + const props: any = {};
68 + const attributes = node.attributes;
69 + for (let i = 0; i < attributes.length; i++) {
70 + if (
71 + attributes[i].name === 'id' &&
72 + attributes[i].value.includes(':')
73 + ) {
74 + // We assume this is a React added ID that's a non-visual implementation detail.
75 + continue;
76 + }
77 + props[attributes[i].name] = attributes[i].value;
78 + }
79 + props.children = getVisibleChildren(node);
80 + children.push(
81 + require('react').createElement(node.tagName.toLowerCase(), props),
82 + );
83 + }
84 + } else if (node.nodeType === 3) {
85 + children.push(node.data);
86 + }
87 + node = node.nextSibling;
88 + }
89 + return children.length === 0
90 + ? undefined
91 + : children.length === 1
92 + ? children[0]
93 + : children;
94 + }
95 +
96 test('completely exhausts synchronous work queue even if something throws', async () => {
97 function Throws({error}) {
98 throw error;
99 }
100
57 - const root1 = ReactNoop.createRoot();
58 - const root2 = ReactNoop.createRoot();
59 - const root3 = ReactNoop.createRoot();
101 + const container1 = document.createElement('div');
102 + const root1 = ReactDOMClient.createRoot(container1);
103 + const container2 = document.createElement('div');
104 + const root2 = ReactDOMClient.createRoot(container2);
105 + const container3 = document.createElement('div');
106 + const root3 = ReactDOMClient.createRoot(container3);
107
108 await act(async () => {
109 root1.render(<Text text="Hi" />);
@@ -72,7 +119,7 @@ describe('ReactFlushSync (AggregateError not available)', () => {
119 overrideQueueMicrotask = true;
120 let error;
121 try {
75 - ReactNoop.flushSync(() => {
122 + ReactDOM.flushSync(() => {
123 root1.render(<Throws error={aahh} />);
124 root2.render(<Throws error={nooo} />);
125 root3.render(<Text text="aww" />);
@@ -85,9 +132,9 @@ describe('ReactFlushSync (AggregateError not available)', () => {
132 // earlier updates errored.
133 assertLog(['aww']);
134 // Roots 1 and 2 were unmounted.
88 - expect(root1).toMatchRenderedOutput(null);
89 - expect(root2).toMatchRenderedOutput(null);
90 - expect(root3).toMatchRenderedOutput('aww');
135 + expect(getVisibleChildren(container1)).toEqual(undefined);
136 + expect(getVisibleChildren(container2)).toEqual(undefined);
137 + expect(getVisibleChildren(container3)).toEqual('aww');
138
139 // In modern environments, React would throw an AggregateError. Because
140 // AggregateError is not available, React throws the first error, then