/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
480 строк
14 KB
Sebastian "Sebbie" Silbermann
Add Owner Stack to attribute hydration mismatches (#32538)
06 мар 2025, 19:12
Не верифицирован
06 мар 2025, 19:12
029e8bd
Код
Авторство
О чём код?
/** * 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. * * @emails react-core */ 'use strict'; let React = require('react'); let ReactDOM = require('react-dom'); let ReactDOMClient = require('react-dom/client'); let ReactDOMServer = require('react-dom/server'); let Scheduler = require('scheduler'); let act; let useEffect; let assertLog; let waitForAll; let assertConsoleErrorDev; describe('ReactDOMRoot', () => { let container; beforeEach(() => { jest.resetModules(); container = document.createElement('div'); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); Scheduler = require('scheduler'); act = require('internal-test-utils').act; assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev; useEffect = React.useEffect; const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; waitForAll = InternalTestUtils.waitForAll; }); it('renders children', async () => { const root = ReactDOMClient.createRoot(container); root.render(<div>Hi</div>); await waitForAll([]); expect(container.textContent).toEqual('Hi'); }); it('warns if a callback parameter is provided to render', async () => { const callback = jest.fn(); const root = ReactDOMClient.createRoot(container); root.render(<div>Hi</div>, callback); assertConsoleErrorDev( [ 'does not support the second callback argument. ' + 'To execute a side effect after rendering, declare it in a component body with useEffect().', ], {withoutStack: true}, ); await waitForAll([]); expect(callback).not.toHaveBeenCalled(); }); it('warn if a object is passed to root.render(...)', async () => { function App() { return 'Child'; } const root = ReactDOMClient.createRoot(container); root.render(<App />, {}); assertConsoleErrorDev( [ 'You passed a second argument to root.render(...) but it only accepts ' + 'one argument.', ], { withoutStack: true, }, ); }); it('warn if a container is passed to root.render(...)', async () => { function App() { return 'Child'; } const root = ReactDOMClient.createRoot(container); root.render(<App />, container); assertConsoleErrorDev( [ 'You passed a container to the second argument of root.render(...). ' + "You don't need to pass it again since you already passed it to create " + 'the root.', ], { withoutStack: true, }, ); }); it('warns if a callback parameter is provided to unmount', async () => { const callback = jest.fn(); const root = ReactDOMClient.createRoot(container); root.render(<div>Hi</div>); root.unmount(callback); assertConsoleErrorDev( [ 'does not support a callback argument. ' + 'To execute a side effect after rendering, declare it in a component body with useEffect().', ], {withoutStack: true}, ); await waitForAll([]); expect(callback).not.toHaveBeenCalled(); }); it('unmounts children', async () => { const root = ReactDOMClient.createRoot(container); root.render(<div>Hi</div>); await waitForAll([]); expect(container.textContent).toEqual('Hi'); root.unmount(); await waitForAll([]); expect(container.textContent).toEqual(''); }); it('can be immediately unmounted', async () => { const root = ReactDOMClient.createRoot(container); await act(() => { root.unmount(); }); }); it('supports hydration', async () => { const markup = await new Promise(resolve => resolve( ReactDOMServer.renderToString( <div> <span className="extra" /> </div>, ), ), ); // Does not hydrate by default const container1 = document.createElement('div'); container1.innerHTML = markup; const root1 = ReactDOMClient.createRoot(container1); root1.render( <div> <span /> </div>, ); await waitForAll([]); const container2 = document.createElement('div'); container2.innerHTML = markup; ReactDOMClient.hydrateRoot( container2, <div> <span /> </div>, ); await waitForAll([]); assertConsoleErrorDev([ "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" + '\n' + "- A server/client branch `if (typeof window !== 'undefined')`.\n" + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" + "- Date formatting in a user's locale which doesn't match the server.\n" + '- External changing data without sending a snapshot of it along with the HTML.\n' + '- Invalid HTML tag nesting.\n' + '\n' + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' + '\n' + 'https://react.dev/link/hydration-mismatch\n' + '\n' + ' <div>\n' + ' <span\n' + '- className="extra"\n' + ' >\n' + '\n in span (at **)', ]); }); it('clears existing children', async () => { container.innerHTML = '<div>a</div><div>b</div>'; const root = ReactDOMClient.createRoot(container); root.render( <div> <span>c</span> <span>d</span> </div>, ); await waitForAll([]); expect(container.textContent).toEqual('cd'); root.render( <div> <span>d</span> <span>c</span> </div>, ); await waitForAll([]); expect(container.textContent).toEqual('dc'); }); it('throws a good message on invalid containers', () => { expect(() => { ReactDOMClient.createRoot(<div>Hi</div>); }).toThrow('Target container is not a DOM element.'); }); it('warns when creating two roots managing the same container', () => { ReactDOMClient.createRoot(container); ReactDOMClient.createRoot(container); assertConsoleErrorDev( [ 'You are calling ReactDOMClient.createRoot() on a container that ' + 'has already been passed to createRoot() before. Instead, call ' + 'root.render() on the existing root instead if you want to update it.', ], {withoutStack: true}, ); }); it('does not warn when creating second root after first one is unmounted', async () => { const root = ReactDOMClient.createRoot(container); root.unmount(); await waitForAll([]); ReactDOMClient.createRoot(container); // No warning }); it('warns if creating a root on the document.body', async () => { // we no longer expect an error for this if float is enabled ReactDOMClient.createRoot(document.body); }); it('warns if updating a root that has had its contents removed', async () => { const root = ReactDOMClient.createRoot(container); root.render(<div>Hi</div>); await waitForAll([]); container.innerHTML = ''; // When either of these flags are on this validation is turned off so we // expect there to be no warnings root.render(<div>Hi</div>); }); it('should render different components in same root', async () => { document.body.appendChild(container); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(<div />); }); expect(container.firstChild.nodeName).toBe('DIV'); await act(() => { root.render(<span />); }); expect(container.firstChild.nodeName).toBe('SPAN'); }); it('should not warn if mounting into non-empty node', async () => { container.innerHTML = '<div></div>'; const root = ReactDOMClient.createRoot(container); await act(() => { root.render(<div />); }); expect(true).toBe(true); }); it('should reuse markup if rendering to the same target twice', async () => { const root = ReactDOMClient.createRoot(container); await act(() => { root.render(<div />); }); const firstElm = container.firstChild; await act(() => { root.render(<div />); }); expect(firstElm).toBe(container.firstChild); }); it('should unmount and remount if the key changes', async () => { function Component({text}) { useEffect(() => { Scheduler.log('Mount'); return () => { Scheduler.log('Unmount'); }; }, []); return <span>{text}</span>; } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(<Component text="orange" key="A" />); }); expect(container.firstChild.innerHTML).toBe('orange'); assertLog(['Mount']); // If we change the key, the component is unmounted and remounted await act(() => { root.render(<Component text="green" key="B" />); }); expect(container.firstChild.innerHTML).toBe('green'); assertLog(['Unmount', 'Mount']); // But if we don't change the key, the component instance is reused await act(() => { root.render(<Component text="blue" key="B" />); }); expect(container.firstChild.innerHTML).toBe('blue'); assertLog([]); }); it('throws if unmounting a root that has had its contents removed', async () => { const root = ReactDOMClient.createRoot(container); await act(() => { root.render(<div>Hi</div>); }); container.innerHTML = ''; await expect(async () => { await act(() => { root.unmount(); }); }).rejects.toThrow('The node to be removed is not a child of this node.'); }); it('unmount is synchronous', async () => { const root = ReactDOMClient.createRoot(container); await act(() => { root.render('Hi'); }); expect(container.textContent).toEqual('Hi'); await act(() => { root.unmount(); // Should have already unmounted expect(container.textContent).toEqual(''); }); }); it('throws if an unmounted root is updated', async () => { const root = ReactDOMClient.createRoot(container); await act(() => { root.render('Hi'); }); expect(container.textContent).toEqual('Hi'); root.unmount(); expect(() => root.render("I'm back")).toThrow( 'Cannot update an unmounted root.', ); }); it('warns if root is unmounted inside an effect', async () => { const container1 = document.createElement('div'); const root1 = ReactDOMClient.createRoot(container1); const container2 = document.createElement('div'); const root2 = ReactDOMClient.createRoot(container2); function App({step}) { useEffect(() => { if (step === 2) { root2.unmount(); } }, [step]); return 'Hi'; } await act(() => { root1.render(<App step={1} />); }); expect(container1.textContent).toEqual('Hi'); ReactDOM.flushSync(() => { root1.render(<App step={2} />); }); assertConsoleErrorDev([ 'Attempted to synchronously unmount a root while React was already rendering. ' + 'React cannot finish unmounting the root until the current render has completed, ' + 'which may lead to a race condition.\n' + ' in App (at **)', ]); }); // @gate disableCommentsAsDOMContainers it('errors if container is a comment node', () => { // This is an old feature used by www. Disabled in the open source build. const div = document.createElement('div'); div.innerHTML = '<!-- react-mount-point-unstable -->'; const commentNode = div.childNodes[0]; expect(() => ReactDOMClient.createRoot(commentNode)).toThrow( 'Target container is not a DOM element.', ); expect(() => ReactDOMClient.hydrateRoot(commentNode)).toThrow( 'Target container is not a DOM element.', ); }); it('warn if no children passed to hydrateRoot', async () => { ReactDOMClient.hydrateRoot(container); assertConsoleErrorDev( [ 'Must provide initial children as second argument to hydrateRoot. ' + 'Example usage: hydrateRoot(domContainer, <App />)', ], {withoutStack: true}, ); }); it('warn if JSX passed to createRoot', async () => { function App() { return 'Child'; } ReactDOMClient.createRoot(container, <App />); assertConsoleErrorDev( [ 'You passed a JSX element to createRoot. You probably meant to call root.render instead. ' + 'Example usage:\n' + '\n' + ' let root = createRoot(domContainer);\n' + ' root.render(<App />);', ], { withoutStack: true, }, ); }); it('warns when given a function', () => { function Component() { return <div />; } const root = ReactDOMClient.createRoot(document.createElement('div')); ReactDOM.flushSync(() => { root.render(Component); }); assertConsoleErrorDev( [ 'Functions are not valid as a React child. ' + 'This may happen if you return Component instead of <Component /> from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + ' root.render(Component)', ], {withoutStack: true}, ); }); it('warns when given a symbol', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); ReactDOM.flushSync(() => { root.render(Symbol('foo')); }); assertConsoleErrorDev( [ 'Symbols are not valid as a React child.\n' + ' root.render(Symbol(foo))', ], {withoutStack: true}, ); }); });