diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index ce71a6334ee64..65f82dcd3690a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -19,6 +19,7 @@ describe('ReactDOMComponent', () => { let act; let assertLog; let Scheduler; + let assertConsoleErrorDev; beforeEach(() => { jest.resetModules(); @@ -28,6 +29,8 @@ describe('ReactDOMComponent', () => { 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; }); @@ -189,73 +192,72 @@ describe('ReactDOMComponent', () => { it('should warn for unknown prop', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(
{}} />); - }); - }).toErrorDev( + 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 expect(async () => { - await act(() => { - root.render(
{}} baz={() => {}} />); - }); - }).toErrorDev( + 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 expect(async () => { - await act(() => { - root.render(
{}} />); - }); - }).toErrorDev( - 'Invalid event handler property `onDblClick`. Did you mean `onDoubleClick`?\n in div (at **)', - ); + 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 expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `onUnknown`. It will be ignored.\n in div (at **)', - ); + 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 expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `onunknown`. It will be ignored.\n in div (at **)', - ); + 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 expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `on-unknown`. It will be ignored.\n in div (at **)', - ); + + 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); }); @@ -263,31 +265,31 @@ describe('ReactDOMComponent', () => { it('should warn for unknown function event handlers', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `onUnknown`. It will be ignored.\n in div (at **)', - ); + 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 expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `onunknown`. It will be ignored.\n in div (at **)', - ); + 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 expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `on-unknown`. It will be ignored.\n in div (at **)', - ); + 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); }); @@ -295,13 +297,13 @@ describe('ReactDOMComponent', () => { it('should warn for badly cased React attributes', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Invalid DOM property `CHILDREN`. Did you mean `children`?\n in div (at **)', - ); + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ + 'Invalid DOM property `CHILDREN`. Did you mean `children`?\n' + + ' in div (at **)', + ]); expect(container.firstChild.getAttribute('CHILDREN')).toBe('5'); }); @@ -323,14 +325,13 @@ describe('ReactDOMComponent', () => { const style = {fontSize: NaN}; const div = document.createElement('div'); const root = ReactDOMClient.createRoot(div); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - '`NaN` is an invalid value for the `fontSize` css style property.' + - '\n in span (at **)', - ); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + '`NaN` is an invalid value for the `fontSize` css style property.\n' + + ' in span (at **)', + ]); await act(() => { root.render(); }); @@ -350,15 +351,18 @@ describe('ReactDOMComponent', () => { const style = {fontSize: new TemporalLike()}; const root = ReactDOMClient.createRoot(document.createElement('div')); await expect(async () => { - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'The provided `fontSize` CSS property is an unsupported type TemporalLike.' + - ' This value must be coerced to a string before using it here.', - ); + 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 () => { @@ -590,16 +594,16 @@ describe('ReactDOMComponent', () => { it('should not add an empty src attribute', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + 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.', - ); + 'or pass null to src instead of an empty string.\n' + + ' in img (at **)', + ]); const node = container.firstChild; expect(node.hasAttribute('src')).toBe(false); @@ -608,31 +612,31 @@ describe('ReactDOMComponent', () => { }); expect(node.hasAttribute('src')).toBe(true); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + 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.', - ); + '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 expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + 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.', - ); + 'or pass null to href instead of an empty string.\n' + + ' in link (at **)', + ]); const node = container.firstChild; expect(node.hasAttribute('href')).toBe(false); @@ -641,15 +645,15 @@ describe('ReactDOMComponent', () => { }); expect(node.hasAttribute('href')).toBe(true); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + 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.', - ); + 'or pass null to href instead of an empty string.\n' + + ' in link (at **)', + ]); expect(node.hasAttribute('href')).toBe(false); }); @@ -871,204 +875,235 @@ describe('ReactDOMComponent', () => { }); it('should reject attribute key injection attack on markup for regular DOM (SSR)', () => { - expect(() => { - for (let i = 0; i < 3; i++) { - const element1 = React.createElement( - 'div', - {'blah" onclick="beevil" noise="hi': 'selected'}, - null, - ); - const element2 = React.createElement( - 'div', - {'>
': 'selected'}, - null, - ); - const result1 = ReactDOMServer.renderToString(element1); - const result2 = ReactDOMServer.renderToString(element2); - expect(result1.toLowerCase()).not.toContain('onclick'); - expect(result2.toLowerCase()).not.toContain('script'); - } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>
`', + for (let i = 0; i < 3; i++) { + const element1 = React.createElement( + 'div', + {'blah" onclick="beevil" noise="hi': 'selected'}, + null, + ); + const element2 = React.createElement( + 'div', + {'>
': 'selected'}, + null, + ); + const result1 = ReactDOMServer.renderToString(element1); + const result2 = ReactDOMServer.renderToString(element2); + expect(result1.toLowerCase()).not.toContain('onclick'); + expect(result2.toLowerCase()).not.toContain('script'); + } + assertConsoleErrorDev([ + 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + + ' in div (at **)', + 'Invalid attribute name: `>
`\n' + + ' in div (at **)', ]); }); it('should reject attribute key injection attack on markup for custom elements (SSR)', () => { - expect(() => { - for (let i = 0; i < 3; i++) { - const element1 = React.createElement( - 'x-foo-component', - {'blah" onclick="beevil" noise="hi': 'selected'}, - null, - ); - const element2 = React.createElement( - 'x-foo-component', - {'>': 'selected'}, - null, - ); - const result1 = ReactDOMServer.renderToString(element1); - const result2 = ReactDOMServer.renderToString(element2); - expect(result1.toLowerCase()).not.toContain('onclick'); - expect(result2.toLowerCase()).not.toContain('script'); - } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>`', + for (let i = 0; i < 3; i++) { + const element1 = React.createElement( + 'x-foo-component', + {'blah" onclick="beevil" noise="hi': 'selected'}, + null, + ); + const element2 = React.createElement( + 'x-foo-component', + {'>': 'selected'}, + null, + ); + const result1 = ReactDOMServer.renderToString(element1); + const result2 = ReactDOMServer.renderToString(element2); + expect(result1.toLowerCase()).not.toContain('onclick'); + expect(result2.toLowerCase()).not.toContain('script'); + } + assertConsoleErrorDev([ + 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + + ' in x-foo-component (at **)', + 'Invalid attribute name: `>`\n' + + ' in x-foo-component (at **)', ]); }); it('should reject attribute key injection attack on mount for regular DOM', async () => { - await expect(async () => { - for (let i = 0; i < 3; i++) { - const container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - React.createElement( - 'div', - {'blah" onclick="beevil" noise="hi': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); - await act(() => { - root.unmount(); - }); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - React.createElement( - 'div', - {'>
': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); + for (let i = 0; i < 3; i++) { + const container = document.createElement('div'); + let root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + React.createElement( + 'div', + {'blah" onclick="beevil" noise="hi': 'selected'}, + null, + ), + ); + }); + + expect(container.firstChild.attributes.length).toBe(0); + if (i === 0) { + assertConsoleErrorDev([ + 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + + ' in div (at **)', + ]); } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>
`', - ]); + await act(() => { + root.unmount(); + }); + root = ReactDOMClient.createRoot(container); + 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 mount for custom elements', async () => { - await expect(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, - ), - ); - }); - - 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, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); + 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 **)', + ]); } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>`', - ]); + 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 () => { - await expect(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, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); - await act(() => { - root.render( - React.createElement( - 'div', - {'>
': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); + 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 **)', + ]); } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>
`', - ]); + 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 () => { - await expect(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, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); - await act(() => { - root.render( - React.createElement( - 'x-foo-component', - {'>': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); + 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 **)', + ]); } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>`', - ]); + 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 () => { @@ -1382,36 +1417,38 @@ describe('ReactDOMComponent', () => { }); expect(nodeValueSetter).toHaveBeenCalledTimes(1); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + 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.', - ); + '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 expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'value` prop on `input` should not be null. Consider using an empty string to clear the ' + - 'component or `undefined` for uncontrolled components.', - ); + 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 expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + 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.', - ); + '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(() => { @@ -1462,14 +1499,14 @@ describe('ReactDOMComponent', () => { it('should warn about non-string "is" attribute', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(