diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index 3ee19c72e2e09..6a5b2dddc6808 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -13,15 +13,20 @@ describe('ReactDOMComponent', () => { let React; let ReactTestUtils; let ReactDOM; + let ReactDOMClient; let ReactDOMServer; const ReactFeatureFlags = require('shared/ReactFeatureFlags'); + let act; + beforeEach(() => { jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); + act = require('internal-test-utils').act; }); afterEach(() => { @@ -29,21 +34,33 @@ describe('ReactDOMComponent', () => { }); describe('updateDOM', () => { - it('should handle className', () => { + it('should handle className', async () => { const container = document.createElement('div'); - ReactDOM.render(
, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(
); + }); - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(container.firstChild.className).toEqual('foo'); - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(container.firstChild.className).toEqual('bar'); - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(container.firstChild.className).toEqual(''); }); - it('should gracefully handle various style value types', () => { + it('should gracefully handle various style value types', async () => { const container = document.createElement('div'); - ReactDOM.render(
, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(
); + }); const stubStyle = container.firstChild.style; // set initial style @@ -53,7 +70,9 @@ describe('ReactDOMComponent', () => { top: 2, fontFamily: 'Arial', }; - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(stubStyle.display).toEqual('block'); expect(stubStyle.left).toEqual('1px'); expect(stubStyle.top).toEqual('2px'); @@ -61,14 +80,16 @@ describe('ReactDOMComponent', () => { // reset the style to their default state const reset = {display: '', left: null, top: false, fontFamily: true}; - ReactDOM.render(
, container); + 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', () => { + it('should not update styles when mutating a proxy style object', async () => { const styleStore = { display: 'none', fontFamily: 'Arial', @@ -97,7 +118,10 @@ describe('ReactDOMComponent', () => { }, }; const container = document.createElement('div'); - ReactDOM.render(
, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(
); + }); const stubStyle = container.firstChild.style; stubStyle.display = styles.display; @@ -105,26 +129,34 @@ describe('ReactDOMComponent', () => { styles.display = 'block'; - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(stubStyle.display).toEqual('none'); expect(stubStyle.fontFamily).toEqual('Arial'); expect(stubStyle.lineHeight).toEqual('1.2'); styles.fontFamily = 'Helvetica'; - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(stubStyle.display).toEqual('none'); expect(stubStyle.fontFamily).toEqual('Arial'); expect(stubStyle.lineHeight).toEqual('1.2'); styles.lineHeight = 0.5; - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(stubStyle.display).toEqual('none'); expect(stubStyle.fontFamily).toEqual('Arial'); expect(stubStyle.lineHeight).toEqual('1.2'); - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(stubStyle.display).toBe(''); expect(stubStyle.fontFamily).toBe(''); expect(stubStyle.lineHeight).toBe(''); @@ -147,11 +179,14 @@ describe('ReactDOMComponent', () => { } }); - it('should warn for unknown prop', () => { + it('should warn for unknown prop', async () => { const container = document.createElement('div'); - expect(() => - ReactDOM.render(
{}} />, container), - ).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(
{}} />); + }); + }).toErrorDev( 'Warning: 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://reactjs.org/link/attribute-behavior ' + @@ -159,11 +194,14 @@ describe('ReactDOMComponent', () => { ); }); - it('should group multiple unknown prop warnings together', () => { + it('should group multiple unknown prop warnings together', async () => { const container = document.createElement('div'); - expect(() => - ReactDOM.render(
{}} baz={() => {}} />, container), - ).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(
{}} baz={() => {}} />); + }); + }).toErrorDev( 'Warning: 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://reactjs.org/link/attribute-behavior ' + @@ -171,68 +209,90 @@ describe('ReactDOMComponent', () => { ); }); - it('should warn for onDblClick prop', () => { + it('should warn for onDblClick prop', async () => { const container = document.createElement('div'); - expect(() => - ReactDOM.render(
{}} />, container), - ).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(
{}} />); + }); + }).toErrorDev( 'Warning: Invalid event handler property `onDblClick`. Did you mean `onDoubleClick`?\n in div (at **)', ); }); - it('should warn for unknown string event handlers', () => { + it('should warn for unknown string event handlers', async () => { const container = document.createElement('div'); - expect(() => - ReactDOM.render(
, container), - ).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(
); + }); + }).toErrorDev( 'Warning: 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); - expect(() => - ReactDOM.render(
, container), - ).toErrorDev( + await expect(async () => { + await act(() => { + root.render(
); + }); + }).toErrorDev( 'Warning: 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); - expect(() => - ReactDOM.render(
, container), - ).toErrorDev( + await expect(async () => { + await act(() => { + root.render(
); + }); + }).toErrorDev( 'Warning: 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', () => { + it('should warn for unknown function event handlers', async () => { const container = document.createElement('div'); - expect(() => - ReactDOM.render(
, container), - ).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(
); + }); + }).toErrorDev( 'Warning: 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); - expect(() => - ReactDOM.render(
, container), - ).toErrorDev( + await expect(async () => { + await act(() => { + root.render(
); + }); + }).toErrorDev( 'Warning: 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); - expect(() => - ReactDOM.render(
, container), - ).toErrorDev( + await expect(async () => { + await act(() => { + root.render(
); + }); + }).toErrorDev( 'Warning: 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', () => { + it('should warn for badly cased React attributes', async () => { const container = document.createElement('div'); - expect(() => ReactDOM.render(
, container)).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(
); + }); + }).toErrorDev( 'Warning: Invalid DOM property `CHILDREN`. Did you mean `children`?\n in div (at **)', ); expect(container.firstChild.getAttribute('CHILDREN')).toBe('5'); @@ -248,14 +308,21 @@ describe('ReactDOMComponent', () => { ReactTestUtils.renderIntoDocument(); }); - it('should warn nicely about NaN in style', () => { + it('should warn nicely about NaN in style', async () => { const style = {fontSize: NaN}; const div = document.createElement('div'); - expect(() => ReactDOM.render(, div)).toErrorDev( + const root = ReactDOMClient.createRoot(div); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: `NaN` is an invalid value for the `fontSize` css style property.' + '\n in span (at **)', ); - ReactDOM.render(, div); + await act(() => { + root.render(); + }); }); it('throws with Temporal-like objects as style values', () => { @@ -280,50 +347,68 @@ describe('ReactDOMComponent', () => { ); }); - it('should update styles if initially null', () => { + it('should update styles if initially null', async () => { let styles = null; const container = document.createElement('div'); - ReactDOM.render(
, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(
); + }); const stubStyle = container.firstChild.style; styles = {display: 'block'}; - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(stubStyle.display).toEqual('block'); }); - it('should update styles if updated to null multiple times', () => { + it('should update styles if updated to null multiple times', async () => { let styles = null; const container = document.createElement('div'); - ReactDOM.render(
, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(
); + }); styles = {display: 'block'}; const stubStyle = container.firstChild.style; - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(stubStyle.display).toEqual('block'); - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(stubStyle.display).toEqual(''); - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(stubStyle.display).toEqual('block'); - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(stubStyle.display).toEqual(''); }); - it('should allow named slot projection on both web components and regular DOM elements', () => { + 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); - ReactDOM.render( - - - - , - container, - ); + await act(() => { + root.render( + + + + , + ); + }); const lightDOM = container.firstChild.childNodes; @@ -331,17 +416,19 @@ describe('ReactDOMComponent', () => { expect(lightDOM[1].getAttribute('slot')).toBe('second'); }); - it('should skip reserved props on web components', () => { + it('should skip reserved props on web components', async () => { const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); - ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); expect(container.firstChild.hasAttribute('children')).toBe(false); expect( container.firstChild.hasAttribute('suppressContentEditableWarning'), @@ -350,14 +437,15 @@ describe('ReactDOMComponent', () => { container.firstChild.hasAttribute('suppressHydrationWarning'), ).toBe(false); - ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); expect(container.firstChild.hasAttribute('children')).toBe(false); expect( container.firstChild.hasAttribute('suppressContentEditableWarning'), @@ -367,110 +455,133 @@ describe('ReactDOMComponent', () => { ).toBe(false); }); - it('should skip dangerouslySetInnerHTML on web components', () => { + it('should skip dangerouslySetInnerHTML on web components', async () => { const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); - ReactDOM.render( - , - container, - ); + await act(() => { + root.render(); + }); expect(container.firstChild.hasAttribute('dangerouslySetInnerHTML')).toBe( false, ); - ReactDOM.render( - , - container, - ); + await act(() => { + root.render(); + }); expect(container.firstChild.hasAttribute('dangerouslySetInnerHTML')).toBe( false, ); }); - it('should render null and undefined as empty but print other falsy values', () => { + it('should render null and undefined as empty but print other falsy values', async () => { const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); - ReactDOM.render( -
, - container, - ); + await act(() => { + root.render(
); + }); expect(container.textContent).toEqual('textContent'); - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(container.textContent).toEqual('0'); - ReactDOM.render( -
, - container, - ); + await act(() => { + root.render(
); + }); expect(container.textContent).toEqual('false'); - ReactDOM.render( -
, - container, - ); + await act(() => { + root.render(
); + }); expect(container.textContent).toEqual(''); - ReactDOM.render( -
, - container, - ); + await act(() => { + root.render(
); + }); expect(container.textContent).toEqual(''); - ReactDOM.render( -
, - container, - ); + await act(() => { + root.render(
); + }); expect(container.textContent).toEqual(''); }); - it('should remove attributes', () => { + it('should remove attributes', async () => { const container = document.createElement('div'); - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); expect(container.firstChild.hasAttribute('height')).toBe(true); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(container.firstChild.hasAttribute('height')).toBe(false); }); - it('should remove properties', () => { + it('should remove properties', async () => { const container = document.createElement('div'); - ReactDOM.render(
, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(
); + }); expect(container.firstChild.className).toEqual('monkey'); - ReactDOM.render(
, container); + await act(() => { + root.render(
); + }); expect(container.firstChild.className).toEqual(''); }); - it('should not set null/undefined attributes', () => { + it('should not set null/undefined attributes', async () => { const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); // Initial render. - ReactDOM.render(, container); + 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. - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Update in another direction. - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Removal. - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Addition. - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); }); if (ReactFeatureFlags.enableFilterEmptyStringAttributesDOM) { - it('should not add an empty src attribute', () => { + it('should not add an empty src attribute', async () => { const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( '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 ' + @@ -479,10 +590,16 @@ describe('ReactDOMComponent', () => { const node = container.firstChild; expect(node.hasAttribute('src')).toBe(false); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(node.hasAttribute('src')).toBe(true); - expect(() => ReactDOM.render(, container)).toErrorDev( + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( '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 ' + @@ -491,9 +608,14 @@ describe('ReactDOMComponent', () => { expect(node.hasAttribute('src')).toBe(false); }); - it('should not add an empty href attribute', () => { + it('should not add an empty href attribute', async () => { const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( '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.', @@ -501,10 +623,16 @@ describe('ReactDOMComponent', () => { const node = container.firstChild; expect(node.hasAttribute('href')).toBe(false); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(node.hasAttribute('href')).toBe(true); - expect(() => ReactDOM.render(, container)).toErrorDev( + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( '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.', @@ -512,38 +640,49 @@ describe('ReactDOMComponent', () => { expect(node.hasAttribute('href')).toBe(false); }); - it('should allow an empty action attribute', () => { + it('should allow an empty action attribute', async () => { const container = document.createElement('div'); - ReactDOM.render(
, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); const node = container.firstChild; expect(node.getAttribute('action')).toBe(''); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(node.hasAttribute('action')).toBe(true); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(node.getAttribute('action')).toBe(''); }); - it('allows empty string of a formAction to override the default of a parent', () => { + it('allows empty string of a formAction to override the default of a parent', async () => { const container = document.createElement('div'); - ReactDOM.render( - -