/** * 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] ▾ `); act(() => { legacyRender(, container); }); 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(() => legacyRender(, document.createElement('div')), ); 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(() => legacyRender( , document.createElement('div'), ), ); expect(store).toMatchInlineSnapshot(` [root] ▸ `); const deepestedNodeID = agent.getIDForNode(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 = ; 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), 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(() => legacyRender(, document.createElement('div'))); 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(() => legacyRender(, document.createElement('div'))); 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(() => { legacyRender(, document.createElement('div')); legacyRender(, document.createElement('div')); }); 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(() => legacyRender( , document.createElement('div'), ), ); 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(() => { legacyRender( , document.createElement('div'), ); legacyRender( , document.createElement('div'), ); }); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); } }); }); // @reactVersion >= 18.0 it('detects and updates profiling support based on the attached roots', () => { 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.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(() => legacyRender([fauxElement], document.createElement('div'))); 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 = () => ( ); const container = document.createElement('div'); // Render once to start fetching the lazy component act(() => legacyRender(, container)); await Promise.resolve(); // Render again after it resolves act(() => legacyRender(, container)); 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 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 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 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; } const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender(, container)); }); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender(, container)); }); 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; } const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender(, container)); }); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender(, container)); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] ✕⚠ `); }); describe('during passive effects', () => { function flushPendingBridgeOperations() { jest.runOnlyPendingTimers(); } // Gross abstraction around pending passive warning/error delay. function flushPendingPassiveErrorAndWarningCounts() { jest.advanceTimersByTime(1000); } // @reactVersion >= 18.0 it('are counted (after a delay)', () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); console.warn('test-only: passive warning'); }); return null; } const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => { legacyRender(, container); }, false); }); flushPendingBridgeOperations(); expect(store).toMatchInlineSnapshot(` [root] `); // After a delay, passive effects should be committed as well act(flushPendingPassiveErrorAndWarningCounts, false); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] ✕⚠ `); act(() => ReactDOM.unmountComponentAtNode(container)); 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; } const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => { legacyRender( <> , container, ); }, false); flushPendingBridgeOperations(); expect(store).toMatchInlineSnapshot(` [root] `); // Before warnings and errors have flushed, flush another commit. act(() => { legacyRender( <> , container, ); }, false); flushPendingBridgeOperations(); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] ✕⚠ `); }); // After a delay, passive effects should be committed as well act(flushPendingPassiveErrorAndWarningCounts, false); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] ✕⚠ `); act(() => ReactDOM.unmountComponentAtNode(container)); expect(store).toMatchInlineSnapshot(``); }); }); // @reactVersion >= 18.0 it('from react get counted', () => { const container = document.createElement('div'); function Example() { return []; } function Child() { return null; } withErrorsOrWarningsIgnored( ['Warning: Each child in a list should have a unique "key" prop'], () => { act(() => legacyRender(, container)); }, ); 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; } const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender( , container, ), ); }); 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; } const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender( , container, ), ); }); 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; } const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender( , container, ), ); }); 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; } const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender( , container, ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender( , container, ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 2 [root] ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender( , container, ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 0, ⚠ 2 [root] ⚠ `); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => legacyRender(, container)); }); expect(store).toMatchInlineSnapshot(`[root]`); expect(store.errorCount).toBe(0); expect(store.warningCount).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 ? : } ); } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await actAsync(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); await actAsync(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] ▾ `); }); }); });