Files
react/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js
T
Brian Vaughn a637084320 DevTools: Add Jest snapshot serializer for number formatting (#23139)
Numbers in JavaScript can have precision issues due to how they are encoded. This shows up in snapshot tests sometimes with values like 0.0009999999999999992, which makes the tests hard to read and visually diff.

This PR adds a new snapshot serializers which clamps numbers at 3 decimal points (e.g. the above number 0.0009999999999999992 is serialized as 0.001). This new serializer does not impact non-numeric values, integers, and special numbers like NaN and Infinity.
2022-01-20 15:52:17 -05:00

1382 lines
38 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
describe('StoreStressConcurrent', () => {
let React;
let ReactDOM;
let act;
let actAsync;
let bridge;
let store;
let print;
beforeEach(() => {
bridge = global.bridge;
store = global.store;
store.collapseNodesByDefault = false;
React = require('react');
ReactDOM = require('react-dom');
act = require('./utils').act;
// TODO: Figure out recommendation for concurrent mode tests, then replace
// this helper with the real thing.
actAsync = require('./utils').actAsync;
print = require('./__serializers__/storeSerializer').print;
});
// TODO: Remove this in favor of @gate pragma
if (!__EXPERIMENTAL__) {
it("empty test so Jest doesn't complain", () => {});
return;
}
// 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.
const [showX, _setShowX] = React.useState(false);
setShowX = _setShowX;
return showX ? <X /> : 'c';
};
const D = () => 'd';
const E = () => 'e';
const X = () => 'x';
const a = <A key="a" />;
const b = <B key="b" />;
const c = <C key="c" />;
const d = <D key="d" />;
const e = <E key="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.createRoot(container);
act(() => root.render(<Parent>{[a, b, c, d, e]}</Parent>));
expect(store).toMatchInlineSnapshot(
`
[root]
▾ <Parent>
<A key="a">
<B key="b">
<C key="c">
<D key="d">
<E key="e">
`,
);
expect(container.textContent).toMatch('abcde');
const snapshotForABCDE = print(store);
// 2. Render a version where <C /> renders an <X /> child instead of 'c'.
// This is how we'll test an update to a single component.
act(() => {
setShowX(true);
});
expect(store).toMatchInlineSnapshot(
`
[root]
▾ <Parent>
<A key="a">
<B key="b">
▾ <C key="c">
<X>
<D key="d">
<E key="e">
`,
);
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
const 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],
[<div key="0">{a}</div>, b, c, d, e],
[<div key="0">{a}{b}</div>, c, d, e],
[<div key="0">{a}{b}</div>, c, <div key="1">{d}{e}</div>],
[<div key="1">{a}{b}</div>, c, <div key="0">{d}{e}</div>],
[<div key="0">{a}{b}</div>, c, <div key="1">{d}{e}</div>],
[<div key="2">{a}{b}</div>, c, <div key="3">{d}{e}</div>],
[<span key="0">{a}</span>, b, c, d, [e]],
[a, b, <span key="0"><span>{c}</span></span>, d, e],
[<div key="0">{a}</div>, [b], <span key="1">{c}</span>, [d], <div key="2">{e}</div>],
[a, b, [c, <div key="0">{d}<span>{e}</span></div>], ''],
[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.createRoot(container);
// Verify mounting 'abcde'.
act(() => root.render(<Parent>{cases[i]}</Parent>));
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.createRoot(container);
for (let i = 0; i < cases.length; i++) {
// Verify mounting 'abcde'.
act(() => root.render(<Parent>{cases[i]}</Parent>));
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 = <A key="a" />;
const b = <B key="b" />;
const c = <C key="c" />;
const d = <D key="d" />;
const e = <E key="e" />;
// prettier-ignore
const 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.
const snapshots = [];
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
// $FlowFixMe
const root = ReactDOM.createRoot(container);
act(() => root.render(<Root>{steps[i]}</Root>));
// We snapshot each step once so it doesn't regress.
snapshots.push(print(store));
act(() => root.unmount());
expect(print(store)).toBe('');
}
expect(snapshots).toMatchInlineSnapshot(`
Array [
"[root]
▾ <Root>
<A key=\\"a\\">",
"[root]
▾ <Root>
<B key=\\"b\\">",
"[root]
▾ <Root>
<C key=\\"c\\">",
"[root]
▾ <Root>
<D key=\\"d\\">",
"[root]
▾ <Root>
<E key=\\"e\\">",
"[root]
▾ <Root>
<A key=\\"a\\">",
"[root]
▾ <Root>
<B key=\\"b\\">",
"[root]
▾ <Root>
<C key=\\"c\\">",
"[root]
▾ <Root>
<D key=\\"d\\">",
"[root]
▾ <Root>
<E key=\\"e\\">",
"[root]
▾ <Root>
<A key=\\"a\\">
<B key=\\"b\\">",
"[root]
▾ <Root>
<B key=\\"b\\">
<A key=\\"a\\">",
"[root]
▾ <Root>
<B key=\\"b\\">
<C key=\\"c\\">",
"[root]
▾ <Root>
<C key=\\"c\\">
<B key=\\"b\\">",
"[root]
▾ <Root>
<A key=\\"a\\">
<C key=\\"c\\">",
"[root]
▾ <Root>
<C key=\\"c\\">
<A key=\\"a\\">",
]
`);
// 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++) {
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOM.createRoot(container);
act(() => root.render(<Root>{steps[i]}</Root>));
expect(print(store)).toMatch(snapshots[i]);
act(() => root.render(<Root>{steps[j]}</Root>));
expect(print(store)).toMatch(snapshots[j]);
act(() => root.render(<Root>{steps[i]}</Root>));
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++) {
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<div>{steps[i]}</div>
</Root>,
),
);
expect(print(store)).toMatch(snapshots[i]);
act(() =>
root.render(
<Root>
<div>{steps[j]}</div>
</Root>,
),
);
expect(print(store)).toMatch(snapshots[j]);
act(() =>
root.render(
<Root>
<div>{steps[i]}</div>
</Root>,
),
);
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 = <A key="a" />;
const b = <B key="b" />;
const c = <C key="c" />;
const z = <Z key="z" />;
// prettier-ignore
const steps = [
a,
[a],
[a, b, c],
[c, b, a],
[c, null, a],
<React.Fragment>{c}{a}</React.Fragment>,
<div>{c}{a}</div>,
<div><span>{a}</span>{b}</div>,
[[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.
const snapshots = [];
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
// $FlowFixMe
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>{steps[i]}</React.Suspense>
<Y />
</Root>,
),
);
// We snapshot each step once so it doesn't regress.d
snapshots.push(print(store));
act(() => root.unmount());
expect(print(store)).toBe('');
}
expect(snapshots).toMatchInlineSnapshot(`
Array [
"[root]
▾ <Root>
<X>
▾ <Suspense>
<A key=\\"a\\">
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
<A key=\\"a\\">
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
<A key=\\"a\\">
<B key=\\"b\\">
<C key=\\"c\\">
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
<C key=\\"c\\">
<B key=\\"b\\">
<A key=\\"a\\">
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
<C key=\\"c\\">
<A key=\\"a\\">
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
<C key=\\"c\\">
<A key=\\"a\\">
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
<C key=\\"c\\">
<A key=\\"a\\">
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
<A key=\\"a\\">
<B key=\\"b\\">
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
<A key=\\"a\\">
<Y>",
"[root]
▾ <Root>
<X>
<Suspense>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
<B key=\\"b\\">
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
<A key=\\"a\\">
<Y>",
]
`);
// 2. Verify check Suspense can render same steps as initial fallback content.
for (let i = 0; i < steps.length; i++) {
// $FlowFixMe
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
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
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>{steps[i]}</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>{steps[j]}</React.Suspense>
<Y />
</Root>,
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>{steps[i]}</React.Suspense>
<Y />
</Root>,
),
);
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
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
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
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>{steps[i]}</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>{steps[i]}</React.Suspense>
<Y />
</Root>,
),
);
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
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>{steps[j]}</React.Suspense>
<Y />
</Root>,
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
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
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>{steps[i]}</React.Suspense>
<Y />
</Root>,
),
);
// 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]);
await actAsync(async () => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: true,
});
});
expect(print(store)).toEqual(snapshots[j]);
// Stop forcing fallback.
await actAsync(async () => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: false,
});
});
expect(print(store)).toEqual(snapshots[i]);
// Trigger actual fallback.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
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(
<Root>
<X />
<React.Suspense fallback={steps[j]}>{steps[i]}</React.Suspense>
<Y />
</Root>,
),
);
// Fallback is still forced though.
expect(print(store)).toEqual(snapshots[j]);
// Stop forcing fallback. This reverts to primary content.
await actAsync(async () => {
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.
await actAsync(async () => root.unmount());
expect(print(store)).toBe('');
}
}
});
it('should handle a stress test for Suspense without type change (Concurrent Mode)', async () => {
const A = () => 'a';
const B = () => 'b';
const C = () => 'c';
const X = () => 'x';
const Y = () => 'y';
const Z = () => 'z';
const a = <A key="a" />;
const b = <B key="b" />;
const c = <C key="c" />;
const z = <Z key="z" />;
// prettier-ignore
const steps = [
a,
[a],
[a, b, c],
[c, b, a],
[c, null, a],
<React.Fragment>{c}{a}</React.Fragment>,
<div>{c}{a}</div>,
<div><span>{a}</span>{b}</div>,
[[a]],
null,
b,
a,
];
const Never = () => {
throw new Promise(() => {});
};
const MaybeSuspend = ({children, suspend}) => {
if (suspend) {
return (
<div>
{children}
<Never />
<X />
</div>
);
}
return (
<div>
{children}
<Z />
</div>
);
};
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.
const snapshots = [];
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
// $FlowFixMe
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
// We snapshot each step once so it doesn't regress.
snapshots.push(print(store));
act(() => root.unmount());
expect(print(store)).toBe('');
}
// 2. Verify check Suspense can render same steps as initial fallback content.
// We don't actually assert here because the tree includes <MaybeSuspend>
// which is different from the snapshots above. So we take more snapshots.
const fallbackSnapshots = [];
for (let i = 0; i < steps.length; i++) {
// $FlowFixMe
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
// We snapshot each step once so it doesn't regress.
fallbackSnapshots.push(print(store));
act(() => root.unmount());
expect(print(store)).toBe('');
}
expect(snapshots).toMatchInlineSnapshot(`
Array [
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key=\\"a\\">
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key=\\"a\\">
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key=\\"a\\">
<B key=\\"b\\">
<C key=\\"c\\">
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key=\\"c\\">
<B key=\\"b\\">
<A key=\\"a\\">
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key=\\"c\\">
<A key=\\"a\\">
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key=\\"c\\">
<A key=\\"a\\">
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key=\\"c\\">
<A key=\\"a\\">
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key=\\"a\\">
<B key=\\"b\\">
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key=\\"a\\">
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<B key=\\"b\\">
<Z>
<Y>",
"[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key=\\"a\\">
<Z>
<Y>",
]
`);
// 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
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
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
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<MaybeSuspend suspend={true}>
<X />
<Y />
</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<Z />
<MaybeSuspend suspend={true}>
<Y />
<X />
</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<MaybeSuspend suspend={true}>
<X />
<Y />
</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[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
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
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
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<MaybeSuspend suspend={true}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<MaybeSuspend suspend={false}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<MaybeSuspend suspend={true}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[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
const root = ReactDOM.createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
// 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]);
await actAsync(async () => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: true,
});
});
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback.
await actAsync(async () => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: false,
});
});
expect(print(store)).toEqual(snapshots[i]);
// Trigger actual fallback.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[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(fallbackSnapshots[j]);
// Switch to primary mode.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
),
);
// Fallback is still forced though.
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback. This reverts to primary content.
await actAsync(async () => {
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('');
}
}
});
});