/** * 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'; describe('ReactDOMComponent', () => { let React; let ReactDOM; let ReactDOMClient; let ReactDOMServer; const ReactFeatureFlags = require('shared/ReactFeatureFlags'); let act; let assertLog; let Scheduler; let assertConsoleErrorDev; beforeEach(() => { jest.resetModules(); 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; assertLog = require('internal-test-utils').assertLog; }); afterEach(() => { jest.restoreAllMocks(); }); describe('updateDOM', () => { it('should handle className', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); await act(() => { root.render(
); }); expect(container.firstChild.className).toEqual('foo'); await act(() => { root.render(
); }); expect(container.firstChild.className).toEqual('bar'); await act(() => { root.render(
); }); expect(container.firstChild.className).toEqual(''); }); it('should gracefully handle various style value types', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); const stubStyle = container.firstChild.style; // set initial style const setup = { display: 'block', left: '1px', top: 2, fontFamily: 'Arial', }; await act(() => { root.render(
); }); expect(stubStyle.display).toEqual('block'); expect(stubStyle.left).toEqual('1px'); expect(stubStyle.top).toEqual('2px'); expect(stubStyle.fontFamily).toEqual('Arial'); // reset the style to their default state const reset = {display: '', left: null, top: false, fontFamily: true}; await act(() => { root.render(
); }); expect(stubStyle.display).toEqual(''); expect(stubStyle.left).toEqual(''); expect(stubStyle.top).toEqual(''); expect(stubStyle.fontFamily).toEqual(''); }); it('should not update styles when mutating a proxy style object', async () => { const styleStore = { display: 'none', fontFamily: 'Arial', lineHeight: 1.2, }; // We use a proxy style object so that we can mutate it even if it is // frozen in DEV. const styles = { get display() { return styleStore.display; }, set display(v) { styleStore.display = v; }, get fontFamily() { return styleStore.fontFamily; }, set fontFamily(v) { styleStore.fontFamily = v; }, get lineHeight() { return styleStore.lineHeight; }, set lineHeight(v) { styleStore.lineHeight = v; }, }; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); const stubStyle = container.firstChild.style; stubStyle.display = styles.display; stubStyle.fontFamily = styles.fontFamily; styles.display = 'block'; await act(() => { root.render(
); }); expect(stubStyle.display).toEqual('none'); expect(stubStyle.fontFamily).toEqual('Arial'); expect(stubStyle.lineHeight).toEqual('1.2'); styles.fontFamily = 'Helvetica'; await act(() => { root.render(
); }); expect(stubStyle.display).toEqual('none'); expect(stubStyle.fontFamily).toEqual('Arial'); expect(stubStyle.lineHeight).toEqual('1.2'); styles.lineHeight = 0.5; await act(() => { root.render(
); }); expect(stubStyle.display).toEqual('none'); expect(stubStyle.fontFamily).toEqual('Arial'); expect(stubStyle.lineHeight).toEqual('1.2'); await act(() => { root.render(
); }); expect(stubStyle.display).toBe(''); expect(stubStyle.fontFamily).toBe(''); expect(stubStyle.lineHeight).toBe(''); }); it('should throw when mutating style objects', async () => { const style = {border: '1px solid black'}; class App extends React.Component { state = {style: style}; render() { return
asd
; } } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); if (__DEV__) { expect(() => (style.position = 'absolute')).toThrow(); } }); it('should warn for unknown prop', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
{}} />); }); assertConsoleErrorDev([ 'Invalid value for prop `foo` on
tag. Either remove it ' + 'from the element, or pass a string or number value to keep ' + 'it in the DOM. For details, see https://react.dev/link/attribute-behavior ' + '\n in div (at **)', ]); }); it('should group multiple unknown prop warnings together', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
{}} baz={() => {}} />); }); assertConsoleErrorDev([ 'Invalid values for props `foo`, `baz` on
tag. Either remove ' + 'them from the element, or pass a string or number value to keep ' + 'them in the DOM. For details, see https://react.dev/link/attribute-behavior ' + '\n in div (at **)', ]); }); it('should warn for onDblClick prop', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
{}} />); }); assertConsoleErrorDev([ 'Invalid event handler property `onDblClick`. Did you mean `onDoubleClick`?\n' + ' in div (at **)', ]); }); it('should warn for unknown string event handlers', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); assertConsoleErrorDev([ 'Unknown event handler property `onUnknown`. It will be ignored.\n' + ' in div (at **)', ]); expect(container.firstChild.hasAttribute('onUnknown')).toBe(false); expect(container.firstChild.onUnknown).toBe(undefined); await act(() => { root.render(
); }); assertConsoleErrorDev([ 'Unknown event handler property `onunknown`. It will be ignored.\n' + ' in div (at **)', ]); expect(container.firstChild.hasAttribute('onunknown')).toBe(false); expect(container.firstChild.onunknown).toBe(undefined); await act(() => { root.render(
); }); assertConsoleErrorDev([ 'Unknown event handler property `on-unknown`. It will be ignored.\n' + ' in div (at **)', ]); expect(container.firstChild.hasAttribute('on-unknown')).toBe(false); expect(container.firstChild['on-unknown']).toBe(undefined); }); it('should warn for unknown function event handlers', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); assertConsoleErrorDev([ 'Unknown event handler property `onUnknown`. It will be ignored.\n' + ' in div (at **)', ]); expect(container.firstChild.hasAttribute('onUnknown')).toBe(false); expect(container.firstChild.onUnknown).toBe(undefined); await act(() => { root.render(
); }); assertConsoleErrorDev([ 'Unknown event handler property `onunknown`. It will be ignored.\n' + ' in div (at **)', ]); expect(container.firstChild.hasAttribute('onunknown')).toBe(false); expect(container.firstChild.onunknown).toBe(undefined); await act(() => { root.render(
); }); assertConsoleErrorDev([ 'Unknown event handler property `on-unknown`. It will be ignored.\n' + ' in div (at **)', ]); expect(container.firstChild.hasAttribute('on-unknown')).toBe(false); expect(container.firstChild['on-unknown']).toBe(undefined); }); it('should warn for badly cased React attributes', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); assertConsoleErrorDev([ 'Invalid DOM property `CHILDREN`. Did you mean `children`?\n' + ' in div (at **)', ]); expect(container.firstChild.getAttribute('CHILDREN')).toBe('5'); }); it('should not warn for "0" as a unitless style value', async () => { class Component extends React.Component { render() { return
; } } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); }); it('should warn nicely about NaN in style', async () => { const style = {fontSize: NaN}; const div = document.createElement('div'); const root = ReactDOMClient.createRoot(div); await act(() => { root.render(); }); assertConsoleErrorDev([ '`NaN` is an invalid value for the `fontSize` css style property.\n' + ' in span (at **)', ]); await act(() => { root.render(); }); }); it('throws with Temporal-like objects as style values', async () => { class TemporalLike { valueOf() { // Throwing here is the behavior of ECMAScript "Temporal" date/time API. // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf throw new TypeError('prod message'); } toString() { return '2020-01-01'; } } const style = {fontSize: new TemporalLike()}; const root = ReactDOMClient.createRoot(document.createElement('div')); await expect(async () => { await act(() => { root.render(); }); }).rejects.toThrowError(new TypeError('prod message')); assertConsoleErrorDev([ 'The provided `fontSize` CSS property is an unsupported type TemporalLike.' + ' This value must be coerced to a string before using it here.\n' + ' in span (at **)', 'The provided `fontSize` CSS property is an unsupported type TemporalLike.' + ' This value must be coerced to a string before using it here.\n' + ' in span (at **)', ]); }); it('should update styles if initially null', async () => { let styles = null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); const stubStyle = container.firstChild.style; styles = {display: 'block'}; await act(() => { root.render(
); }); expect(stubStyle.display).toEqual('block'); }); it('should update styles if updated to null multiple times', async () => { let styles = null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); styles = {display: 'block'}; const stubStyle = container.firstChild.style; await act(() => { root.render(
); }); expect(stubStyle.display).toEqual('block'); await act(() => { root.render(
); }); expect(stubStyle.display).toEqual(''); await act(() => { root.render(
); }); expect(stubStyle.display).toEqual('block'); await act(() => { root.render(
); }); expect(stubStyle.display).toEqual(''); }); it('should allow named slot projection on both web components and regular DOM elements', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render( , ); }); const lightDOM = container.firstChild.childNodes; expect(lightDOM[0].getAttribute('slot')).toBe('first'); expect(lightDOM[1].getAttribute('slot')).toBe('second'); }); it('should skip reserved props on web components', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render( , ); }); expect(container.firstChild.hasAttribute('children')).toBe(false); expect( container.firstChild.hasAttribute('suppressContentEditableWarning'), ).toBe(false); expect( container.firstChild.hasAttribute('suppressHydrationWarning'), ).toBe(false); await act(() => { root.render( , ); }); expect(container.firstChild.hasAttribute('children')).toBe(false); expect( container.firstChild.hasAttribute('suppressContentEditableWarning'), ).toBe(false); expect( container.firstChild.hasAttribute('suppressHydrationWarning'), ).toBe(false); }); it('should skip dangerouslySetInnerHTML on web components', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.firstChild.hasAttribute('dangerouslySetInnerHTML')).toBe( false, ); await act(() => { root.render(); }); expect(container.firstChild.hasAttribute('dangerouslySetInnerHTML')).toBe( false, ); }); it('should render null and undefined as empty but print other falsy values', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); expect(container.textContent).toEqual('textContent'); await act(() => { root.render(
); }); expect(container.textContent).toEqual('0'); await act(() => { root.render(
); }); expect(container.textContent).toEqual('false'); await act(() => { root.render(
); }); expect(container.textContent).toEqual(''); await act(() => { root.render(
); }); expect(container.textContent).toEqual(''); await act(() => { root.render(
); }); expect(container.textContent).toEqual(''); }); it('should remove attributes', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.firstChild.hasAttribute('height')).toBe(true); await act(() => { root.render(); }); expect(container.firstChild.hasAttribute('height')).toBe(false); }); it('should remove properties', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); expect(container.firstChild.className).toEqual('monkey'); await act(() => { root.render(
); }); expect(container.firstChild.className).toEqual(''); }); it('should not set null/undefined attributes', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); // Initial render. await act(() => { root.render(); }); const node = container.firstChild; expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Update in one direction. await act(() => { root.render(); }); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Update in another direction. await act(() => { root.render(); }); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Removal. await act(() => { root.render(); }); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Addition. await act(() => { root.render(); }); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); }); it('should not add an empty src attribute', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); assertConsoleErrorDev([ 'An empty string ("") was passed to the src attribute. ' + 'This may cause the browser to download the whole page again over the network. ' + 'To fix this, either do not render the element at all ' + 'or pass null to src instead of an empty string.\n' + ' in img (at **)', ]); const node = container.firstChild; expect(node.hasAttribute('src')).toBe(false); await act(() => { root.render(); }); expect(node.hasAttribute('src')).toBe(true); await act(() => { root.render(); }); assertConsoleErrorDev([ 'An empty string ("") was passed to the src attribute. ' + 'This may cause the browser to download the whole page again over the network. ' + 'To fix this, either do not render the element at all ' + 'or pass null to src instead of an empty string.\n' + ' in img (at **)', ]); expect(node.hasAttribute('src')).toBe(false); }); it('should not add an empty href attribute', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); assertConsoleErrorDev([ 'An empty string ("") was passed to the href attribute. ' + 'To fix this, either do not render the element at all ' + 'or pass null to href instead of an empty string.\n' + ' in link (at **)', ]); const node = container.firstChild; expect(node.hasAttribute('href')).toBe(false); await act(() => { root.render(); }); expect(node.hasAttribute('href')).toBe(true); await act(() => { root.render(); }); assertConsoleErrorDev([ 'An empty string ("") was passed to the href attribute. ' + 'To fix this, either do not render the element at all ' + 'or pass null to href instead of an empty string.\n' + ' in link (at **)', ]); expect(node.hasAttribute('href')).toBe(false); }); it('should allow an empty href attribute on anchors', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const node = container.firstChild; expect(node.getAttribute('href')).toBe(''); }); it('should allow an empty action attribute', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); const node = container.firstChild; expect(node.getAttribute('action')).toBe(''); await act(() => { root.render(); }); expect(node.hasAttribute('action')).toBe(true); await act(() => { root.render(); }); expect(node.getAttribute('action')).toBe(''); }); it('allows empty string of a formAction to override the default of a parent', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
': 'selected'}, null, ), ); }); if (i === 0) { assertConsoleErrorDev([ 'Invalid attribute name: `>
`\n' + ' in div (at **)', ]); } expect(container.firstChild.attributes.length).toBe(0); } }); it('should reject attribute key injection attack on mount for custom elements', async () => { for (let i = 0; i < 3; i++) { const container = document.createElement('div'); let root = ReactDOMClient.createRoot(container); await act(() => { root.render( React.createElement( 'x-foo-component', {'blah" onclick="beevil" noise="hi': 'selected'}, null, ), ); }); if (i === 0) { assertConsoleErrorDev([ 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + ' in x-foo-component (at **)', ]); } expect(container.firstChild.attributes.length).toBe(0); await act(() => { root.unmount(); }); root = ReactDOMClient.createRoot(container); await act(() => { root.render( React.createElement( 'x-foo-component', {'>': 'selected'}, null, ), ); }); if (i === 0) { assertConsoleErrorDev([ 'Invalid attribute name: `>`\n' + ' in x-foo-component (at **)', ]); } expect(container.firstChild.attributes.length).toBe(0); } }); it('should reject attribute key injection attack on update for regular DOM', async () => { for (let i = 0; i < 3; i++) { const container = document.createElement('div'); const beforeUpdate = React.createElement('div', {}, null); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(beforeUpdate); }); await act(() => { root.render( React.createElement( 'div', {'blah" onclick="beevil" noise="hi': 'selected'}, null, ), ); }); if (i === 0) { assertConsoleErrorDev([ 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + ' in div (at **)', ]); } expect(container.firstChild.attributes.length).toBe(0); await act(() => { root.render( React.createElement( 'div', {'>
': 'selected'}, null, ), ); }); if (i === 0) { assertConsoleErrorDev([ 'Invalid attribute name: `>
`\n' + ' in div (at **)', ]); } expect(container.firstChild.attributes.length).toBe(0); } }); it('should reject attribute key injection attack on update for custom elements', async () => { for (let i = 0; i < 3; i++) { const container = document.createElement('div'); const beforeUpdate = React.createElement('x-foo-component', {}, null); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(beforeUpdate); }); await act(() => { root.render( React.createElement( 'x-foo-component', {'blah" onclick="beevil" noise="hi': 'selected'}, null, ), ); }); if (i === 0) { assertConsoleErrorDev([ 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + ' in x-foo-component (at **)', ]); } expect(container.firstChild.attributes.length).toBe(0); await act(() => { root.render( React.createElement( 'x-foo-component', {'>': 'selected'}, null, ), ); }); if (i === 0) { assertConsoleErrorDev([ 'Invalid attribute name: `>`\n' + ' in x-foo-component (at **)', ]); } expect(container.firstChild.attributes.length).toBe(0); } }); it('should update arbitrary attributes for tags containing dashes', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); const beforeUpdate = React.createElement('x-foo-component', {}, null); await act(() => { root.render(beforeUpdate); }); const afterUpdate = ; await act(() => { root.render(afterUpdate); }); expect(container.childNodes[0].getAttribute('myattr')).toBe('myval'); }); it('should clear all the styles when removing `style`', async () => { const styles = {display: 'none', color: 'red'}; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); const stubStyle = container.firstChild.style; await act(() => { root.render(
); }); expect(stubStyle.display).toEqual(''); expect(stubStyle.color).toEqual(''); }); it('should update styles when `style` changes from null to object', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); const styles = {color: 'red'}; await act(() => { root.render(
); }); const stubStyle = container.firstChild.style; expect(stubStyle.color).toBe('red'); await act(() => { root.render(
); }); expect(stubStyle.color).toBe(''); await act(() => { root.render(
); }); expect(stubStyle.color).toBe('red'); }); it('should not reset innerHTML for when children is null', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); container.firstChild.innerHTML = 'bonjour'; expect(container.firstChild.innerHTML).toEqual('bonjour'); await act(() => { root.render(
); }); expect(container.firstChild.innerHTML).toEqual('bonjour'); }); it('should reset innerHTML when switching from a direct text child to an empty child', async () => { const transitionToValues = [null, undefined, false]; // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const transitionToValue of transitionToValues) { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
bonjour
); }); expect(container.firstChild.innerHTML).toEqual('bonjour'); await act(() => { root.render(
{transitionToValue}
); }); expect(container.firstChild.innerHTML).toEqual(''); } }); it('should empty element when removing innerHTML', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); expect(container.firstChild.innerHTML).toEqual(':)'); await act(() => { root.render(
); }); expect(container.firstChild.innerHTML).toEqual(''); }); it('should transition from string content to innerHTML', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
hello
); }); expect(container.firstChild.innerHTML).toEqual('hello'); await act(() => { root.render(
); }); expect(container.firstChild.innerHTML).toEqual('goodbye'); }); it('should transition from innerHTML to string content', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); expect(container.firstChild.innerHTML).toEqual('bonjour'); await act(() => { root.render(
adieu
); }); expect(container.firstChild.innerHTML).toEqual('adieu'); }); it('should transition from innerHTML to children in nested el', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
, ); }); expect(container.textContent).toEqual('bonjour'); await act(() => { root.render(
adieu
, ); }); expect(container.textContent).toEqual('adieu'); }); it('should transition from children to innerHTML in nested el', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
adieu
, ); }); expect(container.textContent).toEqual('adieu'); await act(() => { root.render(
, ); }); expect(container.textContent).toEqual('bonjour'); }); it('should not incur unnecessary DOM mutations for equal innerHTML', async () => { // Regression test for https://github.com/facebook/react/issues/30994. // Reassigning equal innerHTML destroys and recreates the child nodes, // which breaks in-progress gestures (e.g. swallows an in-flight click // when a re-render commits between focus and click) and discards state // like text selection. const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
hi'}} />, ); }); const node = container.firstChild; const child = node.firstChild; // A new object with an equal __html string must not touch the DOM. await act(() => { root.render(
hi'}} />, ); }); expect(node.firstChild).toBe(child); // A different __html string still updates. await act(() => { root.render(
bye'}} />, ); }); expect(node.firstChild).not.toBe(child); expect(node.innerHTML).toEqual('bye'); }); it('should not incur unnecessary DOM mutations for attributes', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); const node = container.firstChild; const nodeSetAttribute = node.setAttribute; node.setAttribute = jest.fn(); node.setAttribute.mockImplementation(nodeSetAttribute); const nodeRemoveAttribute = node.removeAttribute; node.removeAttribute = jest.fn(); node.removeAttribute.mockImplementation(nodeRemoveAttribute); await act(() => { root.render(
); }); expect(node.setAttribute).toHaveBeenCalledTimes(0); expect(node.removeAttribute).toHaveBeenCalledTimes(0); await act(() => { root.render(
); }); expect(node.setAttribute).toHaveBeenCalledTimes(1); expect(node.removeAttribute).toHaveBeenCalledTimes(0); await act(() => { root.render(
); }); expect(node.setAttribute).toHaveBeenCalledTimes(1); expect(node.removeAttribute).toHaveBeenCalledTimes(0); await act(() => { root.render(
); }); expect(node.setAttribute).toHaveBeenCalledTimes(1); expect(node.removeAttribute).toHaveBeenCalledTimes(1); await act(() => { root.render(
); }); expect(node.setAttribute).toHaveBeenCalledTimes(2); expect(node.removeAttribute).toHaveBeenCalledTimes(1); await act(() => { root.render(
); }); expect(node.setAttribute).toHaveBeenCalledTimes(2); expect(node.removeAttribute).toHaveBeenCalledTimes(2); }); it('should not incur unnecessary DOM mutations for string properties', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
); }); const node = container.firstChild; const nodeValueSetter = jest.fn(); const oldSetAttribute = node.setAttribute.bind(node); node.setAttribute = function (key, value) { oldSetAttribute(key, value); nodeValueSetter(key, value); }; await act(() => { root.render(
); }); expect(nodeValueSetter).toHaveBeenCalledTimes(1); await act(() => { root.render(
); }); expect(nodeValueSetter).toHaveBeenCalledTimes(1); await act(() => { root.render(
); }); expect(nodeValueSetter).toHaveBeenCalledTimes(1); await act(() => { root.render(
); }); expect(nodeValueSetter).toHaveBeenCalledTimes(1); await act(() => { root.render(
); }); expect(nodeValueSetter).toHaveBeenCalledTimes(2); await act(() => { root.render(
); }); expect(nodeValueSetter).toHaveBeenCalledTimes(2); }); it('should not incur unnecessary DOM mutations for controlled string properties', async () => { function onChange() {} const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const node = container.firstChild; let nodeValue = ''; const nodeValueSetter = jest.fn(); Object.defineProperty(node, 'value', { get: function () { return nodeValue; }, set: nodeValueSetter.mockImplementation(function (newValue) { nodeValue = newValue; }), }); await act(() => { root.render(); }); expect(nodeValueSetter).toHaveBeenCalledTimes(1); await act(() => { root.render( , ); }); expect(nodeValueSetter).toHaveBeenCalledTimes(1); await act(() => { root.render(); }); assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. This is likely caused by ' + 'the value changing from a defined to undefined, which should not happen. Decide between ' + 'using a controlled or uncontrolled input element for the lifetime of the component. ' + 'More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', ]); expect(nodeValueSetter).toHaveBeenCalledTimes(1); await act(() => { root.render(); }); assertConsoleErrorDev([ '`value` prop on `input` should not be null. Consider using an empty string to clear the ' + 'component or `undefined` for uncontrolled components.\n' + ' in input (at **)', ]); expect(nodeValueSetter).toHaveBeenCalledTimes(1); await act(() => { root.render(); }); assertConsoleErrorDev([ 'A component is changing an uncontrolled input to be controlled. This is likely caused by ' + 'the value changing from undefined to a defined value, which should not happen. Decide between ' + 'using a controlled or uncontrolled input element for the lifetime of the component. ' + 'More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', ]); expect(nodeValueSetter).toHaveBeenCalledTimes(2); await act(() => { root.render(); }); expect(nodeValueSetter).toHaveBeenCalledTimes(2); }); it('should not incur unnecessary DOM mutations for boolean properties', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(