/** * 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 */ import {getVersionedRenderImplementation} from './utils'; 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(() => { global.IS_REACT_ACT_ENVIRONMENT = true; 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; }); const {render, unmount, createContainer} = getVersionedRenderImplementation(); // @reactVersion >= 18.0 it('should not allow a root node to be collapsed', () => { const Component = () =>
Hi
; act(() => render()); 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; act(() => render({null})); expect(store).toMatchInlineSnapshot(` [root] `); act(() => render(
)); 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; act(() => render( <> , ), ); expect(store).toMatchInlineSnapshot(` [root] `); }); // @reactVersion >= 18.0 it('should handle multibyte character strings', () => { const Component = () => null; Component.displayName = '🟩💜🔵'; act(() => render()); 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!
; act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode it('should support mount and update operations for multiple roots (legacy render)', () => { 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 support mount and update operations for multiple roots (createRoot)', () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; const containerA = document.createElement('div'); const containerB = document.createElement('div'); const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); act(() => { rootA.render(); rootB.render(); }); expect(store).toMatchInlineSnapshot(` [root] ▾ [root] ▾ `); act(() => { rootA.render(); rootB.render(); }); expect(store).toMatchInlineSnapshot(` [root] ▾ [root] ▾ `); act(() => rootB.unmount()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => rootA.unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 it('should filter DOM nodes from the store tree', () => { const Grandparent = () => (
); const Parent = () => (
); const Child = () =>
Hi!
; act(() => render()); 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 ? ( ) : ( )} ); act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => { render(); }); 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 && } ); act(() => render( , ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => render( , ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => render( , ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => render( , ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => render( , ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => render( , ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => render( , ), ); 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(() => render( , ), ); 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(() => render( , ), ); 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(() => render()); 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 = ; act(() => render({[foo, bar]})); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => render({[bar, foo]})); 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!
; act(() => render( , ), ); expect(store).toMatchInlineSnapshot(` [root] ▸ `); act(() => render( , ), ); expect(store).toMatchInlineSnapshot(` [root] ▸ `); act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode it('should support mount and update operations for multiple roots (legacy render)', () => { 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 support mount and update operations for multiple roots (createRoot)', () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; const containerA = document.createElement('div'); const containerB = document.createElement('div'); const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); act(() => { rootA.render(); rootB.render(); }); expect(store).toMatchInlineSnapshot(` [root] ▸ [root] ▸ `); act(() => { rootA.render(); rootB.render(); }); expect(store).toMatchInlineSnapshot(` [root] ▸ [root] ▸ `); act(() => rootB.unmount()); expect(store).toMatchInlineSnapshot(` [root] ▸ `); act(() => rootA.unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 it('should filter DOM nodes from the store tree', () => { const Grandparent = () => (
); const Parent = () => (
); const Child = () =>
Hi!
; act(() => render()); 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 ? ( ) : ( )} ); act(() => render()); 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] ▾ `); act(() => { render(); }); expect(store).toMatchInlineSnapshot(` [root] ▾ `); }); // @reactVersion >= 18.0 it('should support expanding parts of the tree', () => { const Grandparent = ({count}) => ( ); const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ `); const grandparentID = store.getElementIDAtIndex(0); act(() => store.toggleIsCollapsed(grandparentID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); const parentOneID = store.getElementIDAtIndex(1); const parentTwoID = store.getElementIDAtIndex(2); act(() => store.toggleIsCollapsed(parentOneID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => store.toggleIsCollapsed(parentTwoID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => store.toggleIsCollapsed(parentOneID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => store.toggleIsCollapsed(parentTwoID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => store.toggleIsCollapsed(grandparentID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ `); }); // @reactVersion >= 18.0 it('should support expanding deep parts of the tree', () => { const Wrapper = ({forwardedRef}) => ( ); const Nested = ({depth, forwardedRef}) => depth > 0 ? ( ) : (
); const ref = React.createRef(); act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ `); const deepestedNodeID = agent.getIDForHostInstance(ref.current); act(() => store.toggleIsCollapsed(deepestedNodeID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); const rootID = store.getElementIDAtIndex(0); act(() => store.toggleIsCollapsed(rootID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ `); act(() => store.toggleIsCollapsed(rootID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); const id = store.getElementIDAtIndex(1); act(() => store.toggleIsCollapsed(id, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => store.toggleIsCollapsed(id, 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 = ; act(() => render({[foo, bar]})); expect(store).toMatchInlineSnapshot(` [root] ▸ `); act(() => render({[bar, foo]})); expect(store).toMatchInlineSnapshot(` [root] ▸ `); act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => { store.toggleIsCollapsed(store.getElementIDAtIndex(2), false); store.toggleIsCollapsed(store.getElementIDAtIndex(1), false); }); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); expect(store).toMatchInlineSnapshot(` [root] ▸ `); }); // @reactVersion >= 18.0 it('should not add new nodes when suspense is toggled', () => { const SuspenseTree = () => { return ( Loading outer}> ); }; const Fallback = () => null; const Parent = () => ; const Child = () => null; act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ `); act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); const rendererID = getRendererID(); const suspenseID = store.getElementIDAtIndex(1); act(() => agent.overrideSuspense({ id: suspenseID, rendererID, forceFallback: true, }), ); expect(store).toMatchInlineSnapshot(` [root] ▾ `); act(() => agent.overrideSuspense({ id: suspenseID, rendererID, forceFallback: false, }), ); expect(store).toMatchInlineSnapshot(` [root] ▾ `); }); }); describe('getIndexOfElementID', () => { beforeEach(() => { store.collapseNodesByDefault = false; }); // @reactVersion >= 18.0 it('should support a single root with a single child', () => { const Grandparent = () => ( ); const Parent = () => ; const Child = () => null; act(() => render()); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); } }); // @reactVersion >= 18.0 it('should support multiple roots with one children each', () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; act(() => { render(); render(); }); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); } }); // @reactVersion >= 18.0 it('should support a single root with multiple top level children', () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; act(() => render( , ), ); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); } }); // @reactVersion >= 18.0 it('should support multiple roots with multiple top level children', () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; act(() => { render( , ); createContainer(); render( , ); }); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); } }); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode it('detects and updates profiling support based on the attached roots (legacy render)', () => { const Component = () => null; const containerA = document.createElement('div'); const containerB = document.createElement('div'); expect(store.rootSupportsBasicProfiling).toBe(false); act(() => legacyRender(, containerA)); expect(store.rootSupportsBasicProfiling).toBe(true); act(() => legacyRender(, containerB)); act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store.rootSupportsBasicProfiling).toBe(true); act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store.rootSupportsBasicProfiling).toBe(false); }); // @reactVersion >= 18 it('detects and updates profiling support based on the attached roots (createRoot)', () => { const Component = () => null; const containerA = document.createElement('div'); const containerB = document.createElement('div'); const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); expect(store.rootSupportsBasicProfiling).toBe(false); act(() => rootA.render()); expect(store.rootSupportsBasicProfiling).toBe(true); act(() => rootB.render()); act(() => rootA.unmount()); expect(store.rootSupportsBasicProfiling).toBe(true); act(() => rootB.unmount()); expect(store.rootSupportsBasicProfiling).toBe(false); }); // @reactVersion >= 18.0 it('should properly serialize non-string key values', () => { const Child = () => null; // Bypass React element's automatic stringifying of keys intentionally. // This is pretty hacky. const fauxElement = Object.assign({}, , {key: 123}); act(() => render([fauxElement])); expect(store).toMatchInlineSnapshot(` [root] `); }); it('should show the right display names for special component types', async () => { const MyComponent = (props, ref) => null; const ForwardRefComponent = React.forwardRef(MyComponent); const MyComponent2 = (props, ref) => null; const ForwardRefComponentWithAnonymousFunction = React.forwardRef(() => ( )); const MyComponent3 = (props, ref) => null; const ForwardRefComponentWithCustomDisplayName = React.forwardRef(MyComponent3); ForwardRefComponentWithCustomDisplayName.displayName = 'Custom'; const MyComponent4 = (props, ref) => null; const MemoComponent = React.memo(MyComponent4); const MemoForwardRefComponent = React.memo(ForwardRefComponent); const FakeHigherOrderComponent = () => null; FakeHigherOrderComponent.displayName = 'withFoo(withBar(Baz))'; const MemoizedFakeHigherOrderComponent = React.memo( FakeHigherOrderComponent, ); const ForwardRefFakeHigherOrderComponent = React.forwardRef( FakeHigherOrderComponent, ); const MemoizedFakeHigherOrderComponentWithDisplayNameOverride = React.memo( FakeHigherOrderComponent, ); MemoizedFakeHigherOrderComponentWithDisplayNameOverride.displayName = 'memoRefOverride'; const ForwardRefFakeHigherOrderComponentWithDisplayNameOverride = React.forwardRef(FakeHigherOrderComponent); ForwardRefFakeHigherOrderComponentWithDisplayNameOverride.displayName = 'forwardRefOverride'; const App = () => ( ); // Render once to start fetching the lazy component act(() => render()); await Promise.resolve(); // Render again after it resolves act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ [ForwardRef] ▾ [ForwardRef] [Memo] ▾ [Memo] [ForwardRef] [withFoo][withBar] [Memo][withFoo][withBar] [ForwardRef][withFoo][withBar] `); }); describe('Lazy', () => { async function fakeImport(result) { return {default: result}; } const LazyInnerComponent = () => null; const App = ({renderChildren}) => { if (renderChildren) { return ( ); } else { return null; } }; let LazyComponent; beforeEach(() => { LazyComponent = React.lazy(() => fakeImport(LazyInnerComponent)); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode it('should support Lazy components (legacy render)', async () => { const container = document.createElement('div'); // Render once to start fetching the lazy component act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); await Promise.resolve(); // Render again after it resolves act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); // Render again to unmount it act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] `); }); // @reactVersion >= 18.0 it('should support Lazy components in (createRoot)', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); // Render once to start fetching the lazy component act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); await Promise.resolve(); // Render again after it resolves act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); // Render again to unmount it act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] `); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode it('should support Lazy components that are unmounted before they finish loading (legacy render)', async () => { const container = document.createElement('div'); // Render once to start fetching the lazy component act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); // Render again to unmount it before it finishes loading act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] `); }); // @reactVersion >= 18.0 // @reactVersion < 19 it('should support Lazy components that are unmounted before they finish loading in (createRoot)', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); // Render once to start fetching the lazy component act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); // Render again to unmount it before it finishes loading act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] `); }); }); describe('inline errors and warnings', () => { // @reactVersion >= 18.0 it('during render are counted', () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render()); }); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render()); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] ✕⚠ `); }); // @reactVersion >= 18.0 it('during layout get counted', () => { function Example() { React.useLayoutEffect(() => { console.error('test-only: layout error'); console.warn('test-only: layout warning'); }); return null; } withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render()); }); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render()); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] ✕⚠ `); }); describe('during passive effects', () => { function flushPendingBridgeOperations() { jest.runOnlyPendingTimers(); } // @reactVersion >= 18.0 it('are counted (after no delay)', () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); console.warn('test-only: passive warning'); }); return null; } withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => { render(); }, false); }); flushPendingBridgeOperations(); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] ✕⚠ `); act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 it('are flushed early when there is a new commit', () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); console.warn('test-only: passive warning'); }); return null; } function Noop() { return null; } withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => { render( <> , ); }, false); flushPendingBridgeOperations(); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] ✕⚠ `); // Before warnings and errors have flushed, flush another commit. act(() => { render( <> , ); }, false); flushPendingBridgeOperations(); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] ✕⚠ `); }); act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); }); // In React 19, JSX warnings were moved into the renderer - https://github.com/facebook/react/pull/29088 // The warning is moved to the Child instead of the Parent. // @reactVersion >= 19.0.1 it('from react get counted [React >= 19.0.1]', () => { function Example() { return []; } function Child() { return null; } withErrorsOrWarningsIgnored( ['Each child in a list should have a unique "key" prop'], () => { act(() => render()); }, ); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 0 [root] ▾ ✕ `); }); // @reactVersion >= 18.0 // @reactVersion < 19.0 it('from react get counted [React 18.x]', () => { function Example() { return []; } function Child() { return null; } withErrorsOrWarningsIgnored( ['Each child in a list should have a unique "key" prop'], () => { act(() => render()); }, ); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 0 [root] ▾ `); }); // @reactVersion >= 18.0 it('can be cleared for the whole app', () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render( , ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] ✕⚠ ✕⚠ `); const { clearErrorsAndWarnings, } = require('react-devtools-shared/src/backendAPI'); clearErrorsAndWarnings({bridge, store}); // flush events to the renderer jest.runAllTimers(); expect(store).toMatchInlineSnapshot(` [root] `); }); // @reactVersion >= 18.0 it('can be cleared for particular Fiber (only warnings)', () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render( , ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] ✕⚠ ✕⚠ `); const id = ((store.getElementIDAtIndex(1): any): number); const rendererID = store.getRendererIDForElement(id); const { clearWarningsForElement, } = require('react-devtools-shared/src/backendAPI'); clearWarningsForElement({bridge, id, rendererID}); // Flush events to the renderer. jest.runAllTimers(); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 1 [root] ✕⚠ ✕ `); }); // @reactVersion >= 18.0 it('can be cleared for a particular Fiber (only errors)', () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render( , ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] ✕⚠ ✕⚠ `); const id = ((store.getElementIDAtIndex(1): any): number); const rendererID = store.getRendererIDForElement(id); const { clearErrorsForElement, } = require('react-devtools-shared/src/backendAPI'); clearErrorsForElement({bridge, id, rendererID}); // Flush events to the renderer. jest.runAllTimers(); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 2 [root] ✕⚠ ⚠ `); }); // @reactVersion >= 18.0 it('are updated when fibers are removed from the tree', () => { function ComponentWithWarning() { console.warn('test-only: render warning'); return null; } function ComponentWithError() { console.error('test-only: render error'); return null; } function ComponentWithWarningAndError() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render( , ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render( , ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 2 [root] ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render( , ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 0, ⚠ 2 [root] ⚠ `); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => render()); }); expect(store).toMatchInlineSnapshot(`[root]`); expect(store.componentWithErrorCount).toBe(0); expect(store.componentWithWarningCount).toBe(0); }); // Regression test for https://github.com/facebook/react/issues/23202 // @reactVersion >= 18.0 it('suspense boundary children should not double unmount and error', async () => { async function fakeImport(result) { return {default: result}; } const ChildA = () => null; const ChildB = () => null; const LazyChildA = React.lazy(() => fakeImport(ChildA)); const LazyChildB = React.lazy(() => fakeImport(ChildB)); function App({renderA}) { return ( {renderA ? : } ); } await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); }); }); // @reactVersion > 18.2 it('does not show server components without any children reified children', async () => { // A Server Component that doesn't render into anything on the client doesn't show up. const ServerPromise = Promise.resolve(null); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, }, ]; const App = () => ServerPromise; await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] `); }); // @reactVersion > 18.2 it('does show a server component that renders into a filtered node', async () => { const ServerPromise = Promise.resolve(
); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, }, ]; const App = () => ServerPromise; await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ [Server] `); }); it('can render the same server component twice', async () => { function ClientComponent() { return
; } const ServerPromise = Promise.resolve(); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, }, ]; const App = () => ( <> {ServerPromise} {ServerPromise} ); await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ [Server] [Server] `); }); // @reactVersion > 18.2 it('collapses multiple parent server components into one', async () => { function ClientComponent() { return
; } const ServerPromise = Promise.resolve(); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, }, ]; const ServerPromise2 = Promise.resolve(); ServerPromise2._debugInfo = [ { name: 'ServerComponent2', env: 'Server', owner: null, }, ]; const App = ({initial}) => ( <> {ServerPromise} {ServerPromise} {ServerPromise2} {initial ? null : ServerPromise2} ); await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ [Server] [Server] `); await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ [Server] [Server] `); }); // @reactVersion > 18.2 it('can reparent a child when the server components change', async () => { function ClientComponent() { return
; } const ServerPromise = Promise.resolve(); ServerPromise._debugInfo = [ { name: 'ServerAB', env: 'Server', owner: null, }, ]; const ServerPromise2 = Promise.resolve(); ServerPromise2._debugInfo = [ { name: 'ServerA', env: 'Server', owner: null, }, { name: 'ServerB', env: 'Server', owner: null, }, ]; const App = ({initial}) => (initial ? ServerPromise : ServerPromise2); await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ [Server] `); await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ [Server] ▾ [Server] `); }); // @reactVersion > 18.2 it('splits a server component parent when a different child appears between', async () => { function ClientComponent() { return
; } const ServerPromise = Promise.resolve(); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, }, ]; const App = ({initial}) => initial ? ( <> {ServerPromise} {null} {ServerPromise} ) : ( <> {ServerPromise} {ServerPromise} ); await actAsync(() => render()); // Initially the Server Component only appears once because the children // are consecutive. expect(store).toMatchInlineSnapshot(` [root] ▾ [Server] `); // Later the same instance gets split into two when it is no longer // consecutive so we need two virtual instances to represent two parents. await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ [Server] [Server] `); }); // @reactVersion > 18.2 it('can reorder keyed server components', async () => { function ClientComponent({text}) { return
{text}
; } function getServerComponent(key) { const ServerPromise = Promise.resolve( , ); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, key: key, }, ]; return ServerPromise; } const set1 = ['A', 'B', 'C'].map(getServerComponent); const set2 = ['B', 'A', 'D'].map(getServerComponent); const App = ({initial}) => (initial ? set1 : set2); await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ [Server] [Server] [Server] `); await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ [Server] [Server] [Server] `); }); });