Add a stress test for tree traversal
Dan committed
Apr 15, 2019 at 01:45 UTC
f569d27f86e19f11a9fca809198c0fb70397dddb
2 files changed
+194
-1
src/__tests__/__snapshots__/store-test.js.snap
+21
@@ -25,6 +25,27 @@ exports[`Store should filter DOM nodes from the store tree: 1: mount 1`] = `
25
<Child>
26
`;
27
28
+exports[`Store should handle a stress test with different tree operations: 1: abcde 1`] = `
29
+[root]
30
+ ▾ <Parent>
31
+ <A key="a">
32
+ <B key="b">
33
+ <C key="c">
34
+ <D key="d">
35
+ <E key="e">
36
+`;
37
+
38
+exports[`Store should handle a stress test with different tree operations: 2: abxde 1`] = `
39
+[root]
40
+ ▾ <Parent>
41
+ <A key="a">
42
+ <B key="b">
43
+ ▾ <C key="c">
44
+ <X>
45
+ <D key="d">
46
+ <E key="e">
47
+`;
48
+
49
exports[`Store should support collapsing parts of the tree: 1: mount 1`] = `
50
[root]
51
▾ <Grandparent>
src/__tests__/store-test.js
+173
-1
@@ -5,12 +5,12 @@ describe('Store', () => {
5
let ReactDOM;
6
let TestUtils;
7
let store;
8
+ let print;
9
10
const act = (callback: Function) => {
11
TestUtils.act(() => {
12
callback();
13
});
13
-
14
jest.runAllTimers(); // Flush Bridge operations
15
};
16
@@ -20,6 +20,8 @@ describe('Store', () => {
20
React = require('react');
21
ReactDOM = require('react-dom');
22
TestUtils = require('react-dom/test-utils');
23
+
24
+ print = require('./storeSerializer').print;
25
});
26
27
it('should support mount and update operations', () => {
@@ -163,4 +165,174 @@ describe('Store', () => {
165
act(() => store.toggleIsCollapsed(grandparentID, false));
166
expect(store).toMatchSnapshot('6: expand Grandparent');
167
});
168
+
169
+ // This is a stress test for the tree mount/update/unmount traversal.
170
+ // It renders different trees that should produce the same output.
171
+ it('should handle a stress test with different tree operations', () => {
172
+ let setShowX;
173
+ const A = () => 'a';
174
+ const B = () => 'b';
175
+ const C = () => {
176
+ // We'll be manually flipping this component back and forth in the test.
177
+ // We only do this for a single node in order to verify that DevTools
178
+ // can handle a subtree switching alternates while other subtrees are memoized.
179
+ let [showX, _setShowX] = React.useState(false);
180
+ setShowX = _setShowX;
181
+ return showX ? <X /> : 'c';
182
+ };
183
+ const D = () => 'd';
184
+ const E = () => 'e';
185
+ const X = () => 'x';
186
+ const a = <A key="a" />;
187
+ const b = <B key="b" />;
188
+ const c = <C key="c" />;
189
+ const d = <D key="d" />;
190
+ const e = <E key="e" />;
191
+
192
+ function Parent({ children }) {
193
+ return children;
194
+ }
195
+
196
+ // 1. Render a normal version of [a, b, c, d, e].
197
+ let container = document.createElement('div');
198
+ act(() => ReactDOM.render(<Parent>{[a, b, c, d, e]}</Parent>, container));
199
+ expect(store).toMatchSnapshot('1: abcde');
200
+ expect(container.textContent).toMatch('abcde');
201
+ const snapshotForABCDE = print(store);
202
+
203
+ // 2. Render a version where <C /> renders an <X /> child instead of 'c'.
204
+ // This is how we'll test an update to a single component.
205
+ act(() => {
206
+ setShowX(true);
207
+ });
208
+ expect(store).toMatchSnapshot('2: abxde');
209
+ expect(container.textContent).toMatch('abxde');
210
+ const snapshotForABXDE = print(store);
211
+
212
+ // 3. Verify flipping it back produces the original result.
213
+ act(() => {
214
+ setShowX(false);
215
+ });
216
+ expect(container.textContent).toMatch('abcde');
217
+ expect(print(store)).toBe(snapshotForABCDE);
218
+
219
+ // 4. Clean up.
220
+ act(() => ReactDOM.unmountComponentAtNode(container));
221
+ expect(print(store)).toBe('');
222
+
223
+ // Now comes the interesting part.
224
+ // All of these cases are equivalent to [a, b, c, d, e] in output.
225
+ // We'll verify that DevTools produces the same snapshots for them.
226
+ // These cases are picked so that rendering them sequentially in the same
227
+ // container results in a combination of mounts, updates, unmounts, and reorders.
228
+ // prettier-ignore
229
+ let cases = [
230
+ [a, b, c, d, e],
231
+ [[a], b, c, d, e],
232
+ [[a, b], c, d, e],
233
+ [[a, b], c, [d, e]],
234
+ [[a, b], c, [d, '', e]],
235
+ [[a], b, c, d, [e]],
236
+ [a, b, [[c]], d, e],
237
+ [[a, ''], [b], [c], [d], [e]],
238
+ [a, b, [c, [d, ['', e]]]],
239
+ [a, b, c, d, e],
240
+ [<div key="0">{a}</div>, b, c, d, e],
241
+ [<div key="0">{a}{b}</div>, c, d, e],
242
+ [<div key="0">{a}{b}</div>, c, <div key="1">{d}{e}</div>],
243
+ [<div key="1">{a}{b}</div>, c, <div key="0">{d}{e}</div>],
244
+ [<div key="0">{a}{b}</div>, c, <div key="1">{d}{e}</div>],
245
+ [<div key="2">{a}{b}</div>, c, <div key="3">{d}{e}</div>],
246
+ [<span key="0">{a}</span>, b, c, d, [e]],
247
+ [a, b, <span key="0"><span>{c}</span></span>, d, e],
248
+ [<div key="0">{a}</div>, [b], <span key="1">{c}</span>, [d], <div key="2">{e}</div>],
249
+ [a, b, [c, <div key="0">{d}<span>{e}</span></div>], ''],
250
+ [a, [[]], b, c, [d, [[]], e]],
251
+ [[[a, b, c, d], e]],
252
+ [a, b, c, d, e]
253
+ ];
254
+
255
+ // 5. Test fresh mount for each case.
256
+ for (let i = 0; i < cases.length; i++) {
257
+ // Ensure fresh mount.
258
+ container = document.createElement('div');
259
+
260
+ // Verify mounting 'abcde'.
261
+ act(() => ReactDOM.render(<Parent>{cases[i]}</Parent>, container));
262
+ expect(container.textContent).toMatch('abcde');
263
+ expect(print(store)).toEqual(snapshotForABCDE);
264
+
265
+ // Verify switching to 'abxde'.
266
+ act(() => {
267
+ setShowX(true);
268
+ });
269
+ expect(container.textContent).toMatch('abxde');
270
+ expect(print(store)).toBe(snapshotForABXDE);
271
+
272
+ // Verify switching back to 'abcde'.
273
+ act(() => {
274
+ setShowX(false);
275
+ });
276
+ expect(container.textContent).toMatch('abcde');
277
+ expect(print(store)).toBe(snapshotForABCDE);
278
+
279
+ // Clean up.
280
+ act(() => ReactDOM.unmountComponentAtNode(container));
281
+ expect(print(store)).toBe('');
282
+ }
283
+
284
+ // 6. Verify *updates* by reusing the container between iterations.
285
+ // There'll be no unmounting until the very end.
286
+ container = document.createElement('div');
287
+ for (let i = 0; i < cases.length; i++) {
288
+ // Verify mounting 'abcde'.
289
+ act(() => ReactDOM.render(<Parent>{cases[i]}</Parent>, container));
290
+ expect(container.textContent).toMatch('abcde');
291
+ expect(print(store)).toEqual(snapshotForABCDE);
292
+
293
+ // Verify switching to 'abxde'.
294
+ act(() => {
295
+ setShowX(true);
296
+ });
297
+ expect(container.textContent).toMatch('abxde');
298
+ expect(print(store)).toBe(snapshotForABXDE);
299
+
300
+ // Verify switching back to 'abcde'.
301
+ act(() => {
302
+ setShowX(false);
303
+ });
304
+ expect(container.textContent).toMatch('abcde');
305
+ expect(print(store)).toBe(snapshotForABCDE);
306
+ // Don't unmount. Reuse the container between iterations.
307
+ }
308
+ act(() => ReactDOM.unmountComponentAtNode(container));
309
+ expect(print(store)).toBe('');
310
+
311
+ // 7. Same as the previous step, but for Concurrent Mode.
312
+ container = document.createElement('div');
313
+ let root = ReactDOM.unstable_createRoot(container);
314
+ for (let i = 0; i < cases.length; i++) {
315
+ // Verify mounting 'abcde'.
316
+ act(() => root.render(<Parent>{cases[i]}</Parent>));
317
+ expect(container.textContent).toMatch('abcde');
318
+ expect(print(store)).toEqual(snapshotForABCDE);
319
+
320
+ // Verify switching to 'abxde'.
321
+ act(() => {
322
+ setShowX(true);
323
+ });
324
+ expect(container.textContent).toMatch('abxde');
325
+ expect(print(store)).toBe(snapshotForABXDE);
326
+
327
+ // Verify switching back to 'abcde'.
328
+ act(() => {
329
+ setShowX(false);
330
+ });
331
+ expect(container.textContent).toMatch('abcde');
332
+ expect(print(store)).toBe(snapshotForABCDE);
333
+ // Don't unmount. Reuse the container between iterations.
334
+ }
335
+ act(() => root.unmount());
336
+ expect(print(store)).toBe('');
337
+ });
338
});