mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
fe7163e73d
We've been shipping unprefixed experimental APIs (like `createRoot` and `useTransition`) to the Experimental release channel, with the rationale that because these APIs do not appear in any stable release, we're free to change or remove them later without breaking any downstream projects. What we didn't consider is that downstream projects might be tempted to use feature detection: ```js const useTransition = React.useTransition || fallbackUseTransition; ``` This pattern assumes that the version of `useTransition` that exists in the Experimental channel today has the same API contract as the final `useTransition` API that we'll eventually ship to stable. To discourage feature detection, I've added an `unstable_` prefix to all of our unstable APIs. The Facebook builds still have the unprefixed APIs, though. We will continue to support those; if we make any breaking changes, we'll migrate the internal callers like we usually do. To make testing easier, I added the `unstable_`-prefixed APIs to the www builds, too. That way our tests can always use the prefixed ones without gating on the release channel.
1374 lines
38 KiB
JavaScript
1374 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 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;
|
|
|
|
print = require('./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.unstable_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.unstable_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.unstable_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');
|
|
// $FlowFixMe
|
|
let root = ReactDOM.unstable_createRoot(container);
|
|
for (let i = 0; i < steps.length; i++) {
|
|
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
|
|
root = ReactDOM.unstable_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
|
|
root = ReactDOM.unstable_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');
|
|
// $FlowFixMe
|
|
let root = ReactDOM.unstable_createRoot(container);
|
|
for (let i = 0; i < steps.length; i++) {
|
|
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++) {
|
|
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
|
|
root = ReactDOM.unstable_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
|
|
root = ReactDOM.unstable_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
|
|
root = ReactDOM.unstable_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
|
|
root = ReactDOM.unstable_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
|
|
root = ReactDOM.unstable_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]);
|
|
act(() => {
|
|
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(
|
|
<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.
|
|
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('');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should handle a stress test for Suspense without type change (Concurrent Mode)', () => {
|
|
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');
|
|
// $FlowFixMe
|
|
let root = ReactDOM.unstable_createRoot(container);
|
|
for (let i = 0; i < steps.length; i++) {
|
|
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++) {
|
|
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
|
|
root = ReactDOM.unstable_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
|
|
root = ReactDOM.unstable_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
|
|
root = ReactDOM.unstable_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
|
|
root = ReactDOM.unstable_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
|
|
root = ReactDOM.unstable_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]);
|
|
act(() => {
|
|
bridge.send('overrideSuspense', {
|
|
id: suspenseID,
|
|
rendererID: store.getRendererIDForElement(suspenseID),
|
|
forceFallback: true,
|
|
});
|
|
});
|
|
expect(print(store)).toEqual(fallbackSnapshots[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(
|
|
<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.
|
|
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('');
|
|
}
|
|
}
|
|
});
|
|
});
|