/**
* Copyright (c) Meta Platforms, Inc. and 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('Store', () => {
let React;
let ReactDOM;
let ReactDOMClient;
let agent;
let act;
let actAsync;
let bridge;
let getRendererID;
let legacyRender;
let store;
let withErrorsOrWarningsIgnored;
beforeEach(() => {
agent = global.agent;
bridge = global.bridge;
store = global.store;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
const utils = require('./utils');
act = utils.act;
actAsync = utils.actAsync;
getRendererID = utils.getRendererID;
legacyRender = utils.legacyRender;
withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
});
// @reactVersion >= 18.0
it('should not allow a root node to be collapsed', () => {
const Component = () =>
Hi
;
act(() =>
legacyRender(, document.createElement('div')),
);
expect(store).toMatchInlineSnapshot(`
[root]
`);
expect(store.roots).toHaveLength(1);
const rootID = store.roots[0];
expect(() => store.toggleIsCollapsed(rootID, true)).toThrow(
'Root nodes cannot be collapsed',
);
});
// @reactVersion >= 18.0
it('should properly handle a root with no visible nodes', () => {
const Root = ({children}) => children;
const container = document.createElement('div');
act(() => legacyRender({null}, container));
expect(store).toMatchInlineSnapshot(`
[root]
`);
act(() => legacyRender(, container));
expect(store).toMatchInlineSnapshot(`[root]`);
});
// This test is not the same cause as what's reported on GitHub,
// but the resulting behavior (owner mounting after descendant) is the same.
// Thec ase below is admittedly contrived and relies on side effects.
// I'mnot yet sure of how to reduce the GitHub reported production case to a test though.
// See https://github.com/facebook/react/issues/21445
// @reactVersion >= 18.0
it('should handle when a component mounts before its owner', () => {
const promise = new Promise(resolve => {});
let Dynamic = null;
const Owner = () => {
Dynamic = ;
throw promise;
};
const Parent = () => {
return Dynamic;
};
const Child = () => null;
const container = document.createElement('div');
act(() =>
legacyRender(
<>
>,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
`);
});
// @reactVersion >= 18.0
it('should handle multibyte character strings', () => {
const Component = () => null;
Component.displayName = '🟩💜🔵';
const container = document.createElement('div');
act(() => legacyRender(, container));
expect(store).toMatchInlineSnapshot(`
[root]
<🟩💜🔵>
`);
});
describe('StrictMode compliance', () => {
it('should mark strict root elements as strict', () => {
const App = () => ;
const Component = () => null;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container, {
unstable_strictMode: true,
});
act(() => {
root.render();
});
expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(false);
expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false);
});
// @reactVersion >= 18.0
it('should mark non strict root elements as not strict', () => {
const App = () => ;
const Component = () => null;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() => {
root.render();
});
expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true);
expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(true);
});
it('should mark StrictMode subtree elements as strict', () => {
const App = () => (
);
const Component = () => null;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() => {
root.render();
});
expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true);
expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false);
});
});
describe('collapseNodesByDefault:false', () => {
beforeEach(() => {
store.collapseNodesByDefault = false;
});
// @reactVersion >= 18.0
it('should support mount and update operations', () => {
const Grandparent = ({count}) => (
);
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => );
const Child = () => Hi!
;
const container = document.createElement('div');
act(() => legacyRender(, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
act(() => legacyRender(, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
act(() => ReactDOM.unmountComponentAtNode(container));
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
it('should support mount and update operations for multiple roots', () => {
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => );
const Child = () => Hi!
;
const containerA = document.createElement('div');
const containerB = document.createElement('div');
act(() => {
legacyRender(, containerA);
legacyRender(, containerB);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾
[root]
▾
`);
act(() => {
legacyRender(, containerA);
legacyRender(, containerB);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾
[root]
▾
`);
act(() => ReactDOM.unmountComponentAtNode(containerB));
expect(store).toMatchInlineSnapshot(`
[root]
▾
`);
act(() => ReactDOM.unmountComponentAtNode(containerA));
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
it('should filter DOM nodes from the store tree', () => {
const Grandparent = () => (
);
const Parent = () => (
);
const Child = () => Hi!
;
act(() =>
legacyRender(, document.createElement('div')),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
});
// @reactVersion >= 18.0
it('should display Suspense nodes properly in various states', () => {
const Loading = () => Loading...
;
const SuspendingComponent = () => {
throw new Promise(() => {});
};
const Component = () => {
return Hello
;
};
const Wrapper = ({shouldSuspense}) => (
}>
{shouldSuspense ? (
) : (
)}
);
const container = document.createElement('div');
act(() => legacyRender(, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
`);
act(() => {
legacyRender(, container);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
`);
});
// @reactVersion >= 18.0
it('should support nested Suspense nodes', () => {
const Component = () => null;
const Loading = () => Loading...
;
const Never = () => {
throw new Promise(() => {});
};
const Wrapper = ({
suspendFirst = false,
suspendSecond = false,
suspendParent = false,
}) => (
}>
}>
{suspendFirst ? (
) : (
)}
}>
{suspendSecond ? (
) : (
)}
}>
{suspendParent && }
);
const container = document.createElement('div');
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
`);
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
`);
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
`);
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
`);
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
`);
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
`);
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
`);
const rendererID = getRendererID();
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(4),
rendererID,
forceFallback: true,
}),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
`);
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(2),
rendererID,
forceFallback: true,
}),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
`);
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
`);
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(2),
rendererID,
forceFallback: false,
}),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
`);
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(4),
rendererID,
forceFallback: false,
}),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
`);
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
`);
});
it('should display a partially rendered SuspenseList', () => {
const Loading = () => Loading...
;
const SuspendingComponent = () => {
throw new Promise(() => {});
};
const Component = () => {
return Hello
;
};
const Wrapper = ({shouldSuspense}) => (
}>
{shouldSuspense ? : }
);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() => {
root.render();
});
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
act(() => {
root.render();
});
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
});
// @reactVersion >= 18.0
it('should support collapsing parts of the tree', () => {
const Grandparent = ({count}) => (
);
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => );
const Child = () => Hi!
;
act(() =>
legacyRender(, document.createElement('div')),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
const grandparentID = store.getElementIDAtIndex(0);
const parentOneID = store.getElementIDAtIndex(1);
const parentTwoID = store.getElementIDAtIndex(4);
act(() => store.toggleIsCollapsed(parentOneID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▸
▾
`);
act(() => store.toggleIsCollapsed(parentTwoID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▸
▸
`);
act(() => store.toggleIsCollapsed(parentOneID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▸
`);
act(() => store.toggleIsCollapsed(grandparentID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▸
`);
act(() => store.toggleIsCollapsed(grandparentID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▸
`);
});
// @reactVersion >= 18.0
it('should support reordering of children', () => {
const Root = ({children}) => children;
const Component = () => null;
const Foo = () => [];
const Bar = () => [, ];
const foo = ;
const bar = ;
const container = document.createElement('div');
act(() => legacyRender({[foo, bar]}, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
act(() => legacyRender({[bar, foo]}, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true));
expect(store).toMatchInlineSnapshot(`
[root]
▸
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
});
});
describe('collapseNodesByDefault:true', () => {
beforeEach(() => {
store.collapseNodesByDefault = true;
});
// @reactVersion >= 18.0
it('should support mount and update operations', () => {
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => );
const Child = () => Hi!
;
const container = document.createElement('div');
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▸
▸
`);
act(() =>
legacyRender(
,
container,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▸
▸
`);
act(() => ReactDOM.unmountComponentAtNode(container));
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
it('should support mount and update operations for multiple roots', () => {
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => );
const Child = () => Hi!
;
const containerA = document.createElement('div');
const containerB = document.createElement('div');
act(() => {
legacyRender(, containerA);
legacyRender(, containerB);
});
expect(store).toMatchInlineSnapshot(`
[root]
▸
[root]
▸
`);
act(() => {
legacyRender(, containerA);
legacyRender(, containerB);
});
expect(store).toMatchInlineSnapshot(`
[root]
▸
[root]
▸
`);
act(() => ReactDOM.unmountComponentAtNode(containerB));
expect(store).toMatchInlineSnapshot(`
[root]
▸
`);
act(() => ReactDOM.unmountComponentAtNode(containerA));
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
it('should filter DOM nodes from the store tree', () => {
const Grandparent = () => (
);
const Parent = () => (
);
const Child = () => Hi!
;
act(() =>
legacyRender(, document.createElement('div')),
);
expect(store).toMatchInlineSnapshot(`
[root]
▸
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▸
▸
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾
▸
`);
});
// @reactVersion >= 18.0
it('should display Suspense nodes properly in various states', () => {
const Loading = () => Loading...
;
const SuspendingComponent = () => {
throw new Promise(() => {});
};
const Component = () => {
return Hello
;
};
const Wrapper = ({shouldSuspense}) => (
}>
{shouldSuspense ? (
) : (
)}
);
const container = document.createElement('div');
act(() => legacyRender(, container));
expect(store).toMatchInlineSnapshot(`
[root]
▸
`);
// This test isn't meaningful unless we expand the suspended tree
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(2), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾
▾