From 24309fde06f0603f968532d4e41ec421e3434861 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 18 Apr 2019 16:10:44 +0100 Subject: [PATCH] Add failing Concurrent Mode stress tests --- .../storeStressTestConcurrent-test.js.snap | 239 +++++++ .../storeStressTestConcurrent-test.js | 671 ++++++++++++++++++ 2 files changed, 910 insertions(+) create mode 100644 src/__tests__/__snapshots__/storeStressTestConcurrent-test.js.snap create mode 100644 src/__tests__/storeStressTestConcurrent-test.js diff --git a/src/__tests__/__snapshots__/storeStressTestConcurrent-test.js.snap b/src/__tests__/__snapshots__/storeStressTestConcurrent-test.js.snap new file mode 100644 index 0000000000..22f2fdf406 --- /dev/null +++ b/src/__tests__/__snapshots__/storeStressTestConcurrent-test.js.snap @@ -0,0 +1,239 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 1`] = ` +[root] + ▾ + + ▾ + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 2`] = ` +[root] + ▾ + + ▾ + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 3`] = ` +[root] + ▾ + + ▾ + + + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 4`] = ` +[root] + ▾ + + ▾ + + + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 5`] = ` +[root] + ▾ + + ▾ + + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 6`] = ` +[root] + ▾ + + ▾ + + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 7`] = ` +[root] + ▾ + + ▾ + + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 8`] = ` +[root] + ▾ + + ▾ + + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 9`] = ` +[root] + ▾ + + ▾ + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 10`] = ` +[root] + ▾ + + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 11`] = ` +[root] + ▾ + + ▾ + + +`; + +exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 12`] = ` +[root] + ▾ + + ▾ + + +`; + +exports[`StoreStressConcurrent should handle a stress test with different tree operations (Concurrent Mode): 1: abcde 1`] = ` +[root] + ▾ + + + + + +`; + +exports[`StoreStressConcurrent should handle a stress test with different tree operations (Concurrent Mode): 2: abxde 1`] = ` +[root] + ▾ + + + ▾ + + + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 1`] = ` +[root] + ▾ + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 2`] = ` +[root] + ▾ + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 3`] = ` +[root] + ▾ + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 4`] = ` +[root] + ▾ + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 5`] = ` +[root] + ▾ + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 6`] = ` +[root] + ▾ + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 7`] = ` +[root] + ▾ + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 8`] = ` +[root] + ▾ + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 9`] = ` +[root] + ▾ + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 10`] = ` +[root] + ▾ + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 11`] = ` +[root] + ▾ + + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 12`] = ` +[root] + ▾ + + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 13`] = ` +[root] + ▾ + + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 14`] = ` +[root] + ▾ + + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 15`] = ` +[root] + ▾ + + +`; + +exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 16`] = ` +[root] + ▾ + + +`; diff --git a/src/__tests__/storeStressTestConcurrent-test.js b/src/__tests__/storeStressTestConcurrent-test.js new file mode 100644 index 0000000000..6bfd867337 --- /dev/null +++ b/src/__tests__/storeStressTestConcurrent-test.js @@ -0,0 +1,671 @@ +// @flow + +describe('StoreStressConcurrent', () => { + let React; + let ReactDOM; + let TestUtils; + let bridge; + let store; + let print; + + const act = (callback: Function) => { + TestUtils.act(() => { + callback(); + }); + jest.advanceTimersByTime(1000); // Flush rendering and Suspense + jest.runAllTimers(); // Flush Bridge operations + }; + + beforeEach(() => { + bridge = global.bridge; + store = global.store; + store.collapseNodesByDefault = false; + + React = require('react'); + ReactDOM = require('react-dom'); + TestUtils = require('react-dom/test-utils'); + + print = require('./storeSerializer').print; + }); + + // This is a stress test for the tree mount/update/unmount traversal. + // It renders different trees that should produce the same output. + it('should handle a stress test with different tree operations (Concurrent Mode)', () => { + let setShowX; + const A = () => 'a'; + const B = () => 'b'; + const C = () => { + // We'll be manually flipping this component back and forth in the test. + // We only do this for a single node in order to verify that DevTools + // can handle a subtree switching alternates while other subtrees are memoized. + let [showX, _setShowX] = React.useState(false); + setShowX = _setShowX; + return showX ? : 'c'; + }; + const D = () => 'd'; + const E = () => 'e'; + const X = () => 'x'; + const a = ; + const b = ; + const c = ; + const d = ; + const e = ; + + function Parent({ children }) { + return children; + } + + // 1. Render a normal version of [a, b, c, d, e]. + let container = document.createElement('div'); + // $FlowFixMe + let root = ReactDOM.unstable_createRoot(container); + act(() => root.render({[a, b, c, d, e]})); + expect(store).toMatchSnapshot('1: abcde'); + expect(container.textContent).toMatch('abcde'); + const snapshotForABCDE = print(store); + + // 2. Render a version where renders an child instead of 'c'. + // This is how we'll test an update to a single component. + act(() => { + setShowX(true); + }); + expect(store).toMatchSnapshot('2: abxde'); + expect(container.textContent).toMatch('abxde'); + const snapshotForABXDE = print(store); + + // 3. Verify flipping it back produces the original result. + act(() => { + setShowX(false); + }); + expect(container.textContent).toMatch('abcde'); + expect(print(store)).toBe(snapshotForABCDE); + + // 4. Clean up. + act(() => root.unmount()); + expect(print(store)).toBe(''); + + // Now comes the interesting part. + // All of these cases are equivalent to [a, b, c, d, e] in output. + // We'll verify that DevTools produces the same snapshots for them. + // These cases are picked so that rendering them sequentially in the same + // container results in a combination of mounts, updates, unmounts, and reorders. + // prettier-ignore + let cases = [ + [a, b, c, d, e], + [[a], b, c, d, e], + [[a, b], c, d, e], + [[a, b], c, [d, e]], + [[a, b], c, [d, '', e]], + [[a], b, c, d, [e]], + [a, b, [[c]], d, e], + [[a, ''], [b], [c], [d], [e]], + [a, b, [c, [d, ['', e]]]], + [a, b, c, d, e], + [
{a}
, b, c, d, e], + [
{a}{b}
, c, d, e], + [
{a}{b}
, c,
{d}{e}
], + [
{a}{b}
, c,
{d}{e}
], + [
{a}{b}
, c,
{d}{e}
], + [
{a}{b}
, c,
{d}{e}
], + [{a}, b, c, d, [e]], + [a, b, {c}, d, e], + [
{a}
, [b], {c}, [d],
{e}
], + [a, b, [c,
{d}{e}
], ''], + [a, [[]], b, c, [d, [[]], e]], + [[[a, b, c, d], e]], + [a, b, c, d, e] + ]; + + // 5. Test fresh mount for each case. + for (let i = 0; i < cases.length; i++) { + // Ensure fresh mount. + container = document.createElement('div'); + // $FlowFixMe + root = ReactDOM.unstable_createRoot(container); + + // Verify mounting 'abcde'. + act(() => root.render({cases[i]})); + expect(container.textContent).toMatch('abcde'); + expect(print(store)).toEqual(snapshotForABCDE); + + // Verify switching to 'abxde'. + act(() => { + setShowX(true); + }); + expect(container.textContent).toMatch('abxde'); + expect(print(store)).toBe(snapshotForABXDE); + + // Verify switching back to 'abcde'. + act(() => { + setShowX(false); + }); + expect(container.textContent).toMatch('abcde'); + expect(print(store)).toBe(snapshotForABCDE); + + // Clean up. + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + + // 6. Verify *updates* by reusing the container between iterations. + // There'll be no unmounting until the very end. + container = document.createElement('div'); + // $FlowFixMe + root = ReactDOM.unstable_createRoot(container); + for (let i = 0; i < cases.length; i++) { + // Verify mounting 'abcde'. + act(() => root.render({cases[i]})); + expect(container.textContent).toMatch('abcde'); + expect(print(store)).toEqual(snapshotForABCDE); + + // Verify switching to 'abxde'. + act(() => { + setShowX(true); + }); + expect(container.textContent).toMatch('abxde'); + expect(print(store)).toBe(snapshotForABXDE); + + // Verify switching back to 'abcde'. + act(() => { + setShowX(false); + }); + expect(container.textContent).toMatch('abcde'); + expect(print(store)).toBe(snapshotForABCDE); + // Don't unmount. Reuse the container between iterations. + } + act(() => root.unmount()); + expect(print(store)).toBe(''); + }); + + it('should handle stress test with reordering (Concurrent Mode)', () => { + const A = () => 'a'; + const B = () => 'b'; + const C = () => 'c'; + const D = () => 'd'; + const E = () => 'e'; + const a =
; + const b = ; + const c = ; + const d = ; + const e = ; + + // prettier-ignore + let steps = [ + a, + b, + c, + d, + e, + [a], + [b], + [c], + [d], + [e], + [a, b], + [b, a], + [b, c], + [c, b], + [a, c], + [c, a], + ]; + + const Root = ({ children }) => { + return children; + }; + + // 1. Capture the expected render result. + let snapshots = []; + let container = document.createElement('div'); + // $FlowFixMe + let root = ReactDOM.unstable_createRoot(container); + for (let i = 0; i < steps.length; i++) { + act(() => root.render({steps[i]})); + // We snapshot each step once so it doesn't regress. + expect(store).toMatchSnapshot(); + snapshots.push(print(store)); + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + + // 2. Verify that we can update from every step to every other step and back. + for (let i = 0; i < steps.length; i++) { + for (let j = 0; j < steps.length; j++) { + let container = document.createElement('div'); + // $FlowFixMe + let root = ReactDOM.unstable_createRoot(container); + act(() => root.render({steps[i]})); + expect(print(store)).toMatch(snapshots[i]); + act(() => root.render({steps[j]})); + expect(print(store)).toMatch(snapshots[j]); + act(() => root.render({steps[i]})); + expect(print(store)).toMatch(snapshots[i]); + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + } + + // 3. Same test as above, but this time we wrap children in a host component. + for (let i = 0; i < steps.length; i++) { + for (let j = 0; j < steps.length; j++) { + let container = document.createElement('div'); + // $FlowFixMe + let root = ReactDOM.unstable_createRoot(container); + act(() => + root.render( + +
{steps[i]}
+
+ ) + ); + expect(print(store)).toMatch(snapshots[i]); + act(() => + root.render( + +
{steps[j]}
+
+ ) + ); + expect(print(store)).toMatch(snapshots[j]); + act(() => + root.render( + +
{steps[i]}
+
+ ) + ); + expect(print(store)).toMatch(snapshots[i]); + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + } + }); + + it('should handle a stress test for Suspense (Concurrent Mode)', async () => { + const A = () => 'a'; + const B = () => 'b'; + const C = () => 'c'; + const X = () => 'x'; + const Y = () => 'y'; + const Z = () => 'z'; + const a =
; + const b = ; + const c = ; + const z = ; + + // prettier-ignore + const steps = [ + a, + [a], + [a, b, c], + [c, b, a], + [c, null, a], + {c}{a}, +
{c}{a}
, +
{a}{b}
, + [[a]], + null, + b, + a + ]; + + const Never = () => { + throw new Promise(() => {}); + }; + + const Root = ({ children }) => { + return children; + }; + + // 1. For each step, check Suspense can render them as initial primary content. + // This is the only step where we use Jest snapshots. + let snapshots = []; + let container = document.createElement('div'); + // $FlowFixMe + let root = ReactDOM.unstable_createRoot(container); + for (let i = 0; i < steps.length; i++) { + act(() => + root.render( + + + {steps[i]} + + + ) + ); + // We snapshot each step once so it doesn't regress. + expect(store).toMatchSnapshot(); + snapshots.push(print(store)); + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + + // 2. Verify check Suspense can render same steps as initial fallback content. + for (let i = 0; i < steps.length; i++) { + act(() => + root.render( + + + + + + + + + + ) + ); + expect(print(store)).toEqual(snapshots[i]); + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + + // 3. Verify we can update from each step to each step in primary mode. + for (let i = 0; i < steps.length; i++) { + for (let j = 0; j < steps.length; j++) { + // Always start with a fresh container and steps[i]. + container = document.createElement('div'); + // $FlowFixMe + root = ReactDOM.unstable_createRoot(container); + act(() => + root.render( + + + {steps[i]} + + + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Re-render with steps[j]. + act(() => + root.render( + + + {steps[j]} + + + ) + ); + // Verify the successful transition to steps[j]. + expect(print(store)).toEqual(snapshots[j]); + // Check that we can transition back again. + act(() => + root.render( + + + {steps[i]} + + + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Clean up after every iteration. + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + } + + // 4. Verify we can update from each step to each step in fallback mode. + for (let i = 0; i < steps.length; i++) { + for (let j = 0; j < steps.length; j++) { + // Always start with a fresh container and steps[i]. + container = document.createElement('div'); + // $FlowFixMe + root = ReactDOM.unstable_createRoot(container); + act(() => + root.render( + + + + + + + + + + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Re-render with steps[j]. + act(() => + root.render( + + + + + + + + + + ) + ); + // Verify the successful transition to steps[j]. + expect(print(store)).toEqual(snapshots[j]); + // Check that we can transition back again. + act(() => + root.render( + + + + + + + + + + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Clean up after every iteration. + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + } + + // 5. Verify we can update from each step to each step when moving primary -> fallback. + for (let i = 0; i < steps.length; i++) { + for (let j = 0; j < steps.length; j++) { + // Always start with a fresh container and steps[i]. + container = document.createElement('div'); + // $FlowFixMe + root = ReactDOM.unstable_createRoot(container); + act(() => + root.render( + + + {steps[i]} + + + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Re-render with steps[j]. + act(() => + root.render( + + + + + + + + + + ) + ); + // Verify the successful transition to steps[j]. + expect(print(store)).toEqual(snapshots[j]); + // Check that we can transition back again. + act(() => + root.render( + + + {steps[i]} + + + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Clean up after every iteration. + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + } + + // 6. Verify we can update from each step to each step when moving fallback -> primary. + for (let i = 0; i < steps.length; i++) { + for (let j = 0; j < steps.length; j++) { + // Always start with a fresh container and steps[i]. + container = document.createElement('div'); + // $FlowFixMe + root = ReactDOM.unstable_createRoot(container); + act(() => + root.render( + + + + + + + + + + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Re-render with steps[j]. + act(() => + root.render( + + + {steps[j]} + + + ) + ); + // Verify the successful transition to steps[j]. + expect(print(store)).toEqual(snapshots[j]); + // Check that we can transition back again. + act(() => + root.render( + + + + + + + + + + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Clean up after every iteration. + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + } + + // 7. Verify we can update from each step to each step when toggling Suspense. + for (let i = 0; i < steps.length; i++) { + for (let j = 0; j < steps.length; j++) { + // Always start with a fresh container and steps[i]. + container = document.createElement('div'); + // $FlowFixMe + root = ReactDOM.unstable_createRoot(container); + act(() => + root.render( + + + {steps[i]} + + + ) + ); + + // We get ID from the index in the tree above: + // Root, X, Suspense, ... + // ^ (index is 2) + const suspenseID = store.getElementIDAtIndex(2); + + // Force fallback. + expect(print(store)).toEqual(snapshots[i]); + act(() => { + const suspenseID = store.getElementIDAtIndex(2); + bridge.send('overrideSuspense', { + id: suspenseID, + rendererID: store.getRendererIDForElement(suspenseID), + forceFallback: true, + }); + }); + expect(print(store)).toEqual(snapshots[j]); + + // Stop forcing fallback. + act(() => { + bridge.send('overrideSuspense', { + id: suspenseID, + rendererID: store.getRendererIDForElement(suspenseID), + forceFallback: false, + }); + }); + expect(print(store)).toEqual(snapshots[i]); + + // Trigger actual fallback. + act(() => + root.render( + + + + + + + + + + ) + ); + expect(print(store)).toEqual(snapshots[j]); + + // Force fallback while we're in fallback mode. + act(() => { + bridge.send('overrideSuspense', { + id: suspenseID, + rendererID: store.getRendererIDForElement(suspenseID), + forceFallback: true, + }); + }); + // Keep seeing fallback content. + expect(print(store)).toEqual(snapshots[j]); + + // Switch to primary mode. + act(() => + root.render( + + + {steps[i]} + + + ) + ); + // Fallback is still forced though. + expect(print(store)).toEqual(snapshots[j]); + + // Stop forcing fallback. This reverts to primary content. + act(() => { + bridge.send('overrideSuspense', { + id: suspenseID, + rendererID: store.getRendererIDForElement(suspenseID), + forceFallback: false, + }); + }); + // Now we see primary content. + expect(print(store)).toEqual(snapshots[i]); + + // Clean up after every iteration. + act(() => root.unmount()); + expect(print(store)).toBe(''); + } + } + }); +});