/** * 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 semver from 'semver'; import {getVersionedRenderImplementation} from './utils'; import {ReactVersion} from '../../../../ReactVersions'; const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion; describe('Store', () => { let React; let ReactDOM; let ReactDOMClient; let agent; let act; let actAsync; let bridge; let createDisplayNameFilter; let getRendererID; let legacyRender; let previousComponentFilters; let store; let withErrorsOrWarningsIgnored; function readValue(promise) { if (typeof React.use === 'function') { return React.use(promise); } // Support for React < 19.0 switch (promise.status) { case 'fulfilled': return promise.value; case 'rejected': throw promise.reason; case 'pending': throw promise; default: promise.status = 'pending'; promise.then( value => { promise.status = 'fulfilled'; promise.value = value; }, reason => { promise.status = 'rejected'; promise.reason = reason; }, ); throw promise; } } beforeAll(() => { // JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes Element.prototype.getClientRects = function (this: Element) { const textContent = this.textContent; return [ new DOMRect(1, 2, textContent.length, textContent.split('\n').length), ]; }; }); beforeEach(() => { global.IS_REACT_ACT_ENVIRONMENT = true; agent = global.agent; bridge = global.bridge; store = global.store; previousComponentFilters = store.componentFilters; 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; createDisplayNameFilter = utils.createDisplayNameFilter; withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored; }); afterEach(() => { store.componentFilters = previousComponentFilters; }); const {render, unmount, createContainer} = getVersionedRenderImplementation(); // @reactVersion >= 18.0 it('should not allow a root node to be collapsed', async () => { const Component = () =>
Hi
; await 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', async () => { const Root = ({children}) => children; await act(() => render({null})); expect(store).toMatchInlineSnapshot(` [root] `); await act(() => render(
)); expect(store).toMatchInlineSnapshot(`[root]`); }); it('throws when a transition timeline is requested during initial paint', () => { const errorListener = jest.fn(); store.addListener('error', errorListener); expect(() => store.getSuspendableDocumentOrderSuspenseTransition(false, 1), ).toThrow( 'Cannot get a transition timeline during the initial paint. This is a bug in React DevTools.', ); expect(errorListener).toHaveBeenCalledTimes(1); store.removeListener('error', errorListener); }); // @reactVersion >= 18.0 it('throws before removing a node that is not a child of its parent', () => { function FirstChild() { return null; } function SecondChild() { return null; } function Parent({showFirstChild}) { return ( <> {showFirstChild && } ); } act(() => render()); const parent = store.getElementAtIndex(0); expect(parent.displayName).toBe('Parent'); const firstChildIndex = parent.children.findIndex(id => { const child = store.getElementByID(id); return child !== null && child.displayName === 'FirstChild'; }); expect(firstChildIndex).not.toBe(-1); const firstChildID = parent.children[firstChildIndex]; // Corrupt only the frontend relationship. The removal operation below is // still produced canonically by rendering React. parent.children.splice(firstChildIndex, 1); const errorListener = jest.fn(); store.addListener('error', errorListener); let caughtError = null; try { act(() => render()); } catch (error) { caughtError = error; } finally { // The test Bridge invokes listeners synchronously, so discard the batch // whose Store listener intentionally threw. bridge._messageQueue.length = 0; } const expectedMessage = `Cannot remove node "${firstChildID}" from parent "${parent.id}" ` + `because it is not a child of the parent.`; expect(caughtError).toMatchObject({message: expectedMessage}); expect(errorListener).toHaveBeenCalledWith(caughtError); expect(store.containsElement(firstChildID)).toBe(true); parent.children.splice(firstChildIndex, 0, firstChildID); store.removeListener('error', errorListener); }); // @reactVersion >= 18.0 it('receives operations queued while the frontend transport reconnects', () => { const App = ({children}) => children ?? null; const Parent = ({children}) => children ?? null; const Child = () => null; act(() => render()); const bridgeWall = (bridge.wall: any); bridgeWall.disconnect(); try { act(() => render( , ), ); expect(store).toMatchInlineSnapshot(` [root] `); } finally { bridgeWall.reconnect(); } 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', async () => { const promise = new Promise(resolve => {}); let Dynamic = null; const Owner = () => { Dynamic = ; readValue(promise); }; const Parent = () => { return Dynamic; }; const Child = () => null; await act(() => render( <> , ), ); expect(store).toMatchInlineSnapshot(` [root] [suspense-root] rects={null} `); }); // @reactVersion >= 18.0 it('should handle multibyte character strings', async () => { const Component = () => null; Component.displayName = '🟩💜🔵'; await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] <🟩💜🔵> `); }); it('should handle reorder of filtered elements', async () => { function IgnoreMePassthrough({children}) { return children; } function PassThrough({children}) { return children; } await actAsync( async () => (store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]), ); await act(() => { render(

e1

e2
, ); }); expect(store).toMatchInlineSnapshot(` [root] ▾

`); await act(() => { render(
e2

e1

, ); }); expect(store).toMatchInlineSnapshot(` [root] ▾

`); }); describe('StrictMode compliance', () => { it('should mark strict root elements as strict', async () => { const App = () => ; const Component = () => null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container, { unstable_strictMode: true, }); await 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', async () => { const App = () => ; const Component = () => null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await 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', async () => { const App = () => ( ); const Component = () => null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true); expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false); }); }); describe('Activity hidden state', () => { // @reactVersion >= 19 it('should mark Activity subtree elements as hidden when mode is hidden', async () => { const Activity = React.Activity || React.unstable_Activity; function Child() { return

child
; } function App({hidden}) { return ( ); } await actAsync(() => { render(