[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