{}} />);
});
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(
,
);
});
const node = container.firstChild.firstChild;
expect(node.hasAttribute('formaction')).toBe(true);
expect(node.getAttribute('formaction')).toBe('');
});
it('should not filter attributes for custom elements', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
,
);
});
const node = container.firstChild;
expect(node.hasAttribute('action')).toBe(true);
expect(node.hasAttribute('formAction')).toBe(true);
expect(node.hasAttribute('href')).toBe(true);
expect(node.hasAttribute('src')).toBe(true);
});
it('should apply React-specific aliases to HTML elements', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
const node = container.firstChild;
// Test attribute initialization.
expect(node.getAttribute('accept-charset')).toBe('foo');
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Test attribute update.
await act(() => {
root.render();
});
expect(node.getAttribute('accept-charset')).toBe('boo');
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Test attribute removal by setting to null.
await act(() => {
root.render();
});
expect(node.hasAttribute('accept-charset')).toBe(false);
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Restore.
await act(() => {
root.render();
});
expect(node.getAttribute('accept-charset')).toBe('foo');
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Test attribute removal by setting to undefined.
await act(() => {
root.render();
});
expect(node.hasAttribute('accept-charset')).toBe(false);
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Restore.
await act(() => {
root.render();
});
expect(node.getAttribute('accept-charset')).toBe('foo');
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Test attribute removal.
await act(() => {
root.render();
});
expect(node.hasAttribute('accept-charset')).toBe(false);
expect(node.hasAttribute('acceptCharset')).toBe(false);
});
it('should apply React-specific aliases to SVG elements', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
const node = container.firstChild;
// Test attribute initialization.
expect(node.getAttribute('arabic-form')).toBe('foo');
expect(node.hasAttribute('arabicForm')).toBe(false);
// Test attribute update.
await act(() => {
root.render();
});
expect(node.getAttribute('arabic-form')).toBe('boo');
expect(node.hasAttribute('arabicForm')).toBe(false);
// Test attribute removal by setting to null.
await act(() => {
root.render();
});
expect(node.hasAttribute('arabic-form')).toBe(false);
expect(node.hasAttribute('arabicForm')).toBe(false);
// Restore.
await act(() => {
root.render();
});
expect(node.getAttribute('arabic-form')).toBe('foo');
expect(node.hasAttribute('arabicForm')).toBe(false);
// Test attribute removal by setting to undefined.
await act(() => {
root.render();
});
expect(node.hasAttribute('arabic-form')).toBe(false);
expect(node.hasAttribute('arabicForm')).toBe(false);
// Restore.
await act(() => {
root.render();
});
expect(node.getAttribute('arabic-form')).toBe('foo');
expect(node.hasAttribute('arabicForm')).toBe(false);
// Test attribute removal.
await act(() => {
root.render();
});
expect(node.hasAttribute('arabic-form')).toBe(false);
expect(node.hasAttribute('arabicForm')).toBe(false);
});
it('should properly update custom attributes on custom elements', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
expect(container.firstChild.getAttribute('foo')).toBe('bar');
await act(() => {
root.render();
});
expect(container.firstChild.hasAttribute('foo')).toBe(false);
expect(container.firstChild.getAttribute('bar')).toBe('buzz');
const node = container.firstChild;
expect(node.hasAttribute('foo')).toBe(false);
expect(node.getAttribute('bar')).toBe('buzz');
});
it('should not apply React-specific aliases to custom elements', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
const node = container.firstChild;
// Should not get transformed to arabic-form as SVG would be.
expect(node.getAttribute('arabicForm')).toBe('foo');
expect(node.hasAttribute('arabic-form')).toBe(false);
// Test attribute update.
await act(() => {
root.render();
});
expect(node.getAttribute('arabicForm')).toBe('boo');
// Test attribute removal and addition.
await act(() => {
root.render();
});
// Verify the previous attribute was removed.
expect(node.hasAttribute('arabicForm')).toBe(false);
// Should not get transformed to accept-charset as HTML would be.
expect(node.getAttribute('acceptCharset')).toBe('buzz');
expect(node.hasAttribute('accept-charset')).toBe(false);
});
it('should clear a single style prop when changing `style`', async () => {
let styles = {display: 'none', color: 'red'};
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
const stubStyle = container.firstChild.style;
styles = {color: 'green'};
await act(() => {
root.render();
});
expect(stubStyle.display).toEqual('');
expect(stubStyle.color).toEqual('green');
});
it('should reject attribute key injection attack on markup for regular DOM (SSR)', () => {
for (let i = 0; i < 3; i++) {
const element1 = React.createElement(
'div',
{'blah" onclick="beevil" noise="hi': 'selected'},
null,
);
const element2 = React.createElement(
'div',
{'>
,
);
});
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();
});
const node = container.firstChild;
let nodeValue = true;
const nodeValueSetter = jest.fn();
Object.defineProperty(node, 'muted', {
get: function () {
return nodeValue;
},
set: nodeValueSetter.mockImplementation(function (newValue) {
nodeValue = newValue;
}),
});
await act(() => {
root.render();
});
expect(nodeValueSetter).toHaveBeenCalledTimes(0);
await act(() => {
root.render();
});
expect(nodeValueSetter).toHaveBeenCalledTimes(1);
});
it('should ignore attribute list for elements with the "is" attribute', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
expect(container.firstChild.hasAttribute('cowabunga')).toBe(true);
});
it('should warn about non-string "is" attribute', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
assertConsoleErrorDev([
'Received a `function` for a string attribute `is`. If this is expected, cast ' +
'the value to a string.\n' +
' in button (at **)',
]);
});
it('should not update when switching between null/undefined', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
const setter = jest.fn();
container.firstChild.setAttribute = setter;
await act(() => {
root.render();
});
await act(() => {
root.render();
});
await act(() => {
root.render();
});
expect(setter).toHaveBeenCalledTimes(0);
await act(() => {
root.render();
});
expect(setter).toHaveBeenCalledTimes(1);
});
it('handles multiple child updates without interference', async () => {
// This test might look like it's just testing ReactMultiChild but the
// last bug in this was actually in DOMChildrenOperations so this test
// needs to be in some DOM-specific test file.
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
// ABCD
await act(() => {
root.render(
A
B
C
D
,
);
});
// BADC
await act(() => {
root.render(
B
A
D
C
,
);
});
expect(container.textContent).toBe('BADC');
});
});
describe('createOpenTagMarkup', () => {
function quoteRegexp(str) {
return String(str).replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1');
}
function expectToHaveAttribute(actual, expected) {
const [attr, value] = expected;
let re = '(?:^|\\s)' + attr + '=[\\\'"]';
if (typeof value !== 'undefined') {
re += quoteRegexp(value) + '[\\\'"]';
}
expect(actual).toMatch(new RegExp(re));
}
function genMarkup(props) {
return ReactDOMServer.renderToString();
}
it('should generate the correct markup with className', () => {
expectToHaveAttribute(genMarkup({className: 'a'}), ['class', 'a']);
expectToHaveAttribute(genMarkup({className: 'a b'}), ['class', 'a b']);
expectToHaveAttribute(genMarkup({className: ''}), ['class', '']);
});
it('should escape style names and values', () => {
expectToHaveAttribute(
genMarkup({
style: {'b&ckground': '<3'},
}),
['style', 'b&ckground:<3'],
);
});
});
describe('createContentMarkup', () => {
function quoteRegexp(str) {
return String(str).replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1');
}
function genMarkup(props) {
return ReactDOMServer.renderToString();
}
function toHaveInnerhtml(actual, expected) {
const re = quoteRegexp(expected);
return new RegExp(re).test(actual);
}
it('should handle dangerouslySetInnerHTML', () => {
const innerHTML = {__html: 'testContent'};
expect(
toHaveInnerhtml(
genMarkup({dangerouslySetInnerHTML: innerHTML}),
'testContent',
),
).toBe(true);
});
});
describe('mountComponent', () => {
let mountComponent;
beforeEach(() => {
mountComponent = async props => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
};
});
it('should work error event on element', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
,
);
});
const errorEvent = document.createEvent('Event');
errorEvent.initEvent('error', false, false);
container.getElementsByTagName('source')[0].dispatchEvent(errorEvent);
if (__DEV__) {
assertLog(['onError called']);
}
});
it('should warn for uppercased selfclosing tags', () => {
class Container extends React.Component {
render() {
return React.createElement('BR', null);
}
}
const returnedValue = ReactDOMServer.renderToString();
assertConsoleErrorDev([
' is using incorrect casing. ' +
'Use PascalCase for React components, ' +
'or lowercase for HTML elements.\n' +
' in BR (at **)\n' +
' in Container (at **)',
]);
// This includes a duplicate tag because we didn't treat this as self-closing.
expect(returnedValue).toContain('');
});
it('should warn on upper case HTML tags, not SVG nor custom tags', async () => {
let container = document.createElement('div');
let root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
React.createElement('svg', null, React.createElement('PATH')),
);
});
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(React.createElement('CUSTOM-TAG'));
});
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(React.createElement('IMG'));
});
assertConsoleErrorDev([
' is using incorrect casing. ' +
'Use PascalCase for React components, ' +
'or lowercase for HTML elements.\n' +
' in IMG (at **)',
]);
});
it('should warn on props reserved for future use', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
assertConsoleErrorDev([
'The `aria` attribute is reserved for future use in React. ' +
'Pass individual `aria-` attributes instead.\n' +
' in div (at **)',
]);
});
it('should warn if the tag is unrecognized', async () => {
let realToString;
try {
realToString = Object.prototype.toString;
const wrappedToString = function () {
// Emulate browser behavior which is missing in jsdom
if (this instanceof window.HTMLUnknownElement) {
return '[object HTMLUnknownElement]';
}
return realToString.apply(this, arguments);
};
Object.prototype.toString = wrappedToString; // eslint-disable-line no-extend-native
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render();
});
assertConsoleErrorDev([
'The tag is unrecognized in this browser. ' +
'If you meant to render a React component, start its name with an uppercase letter.\n' +
' in bar (at **)',
]);
// Test deduplication
await act(() => {
root.render();
});
assertConsoleErrorDev([
'The tag is unrecognized in this browser. ' +
'If you meant to render a React component, start its name with an uppercase letter.\n' +
' in foo (at **)',
]);
await act(() => {
root.render();
});
await act(() => {
root.render();
});
// Corner case. Make sure out deduplication logic doesn't break with weird tag.
await act(() => {
root.render();
});
assertConsoleErrorDev([
' is using incorrect casing. ' +
'Use PascalCase for React components, or lowercase for HTML elements.\n' +
' in hasOwnProperty (at **)',
'The tag is unrecognized in this browser. ' +
'If you meant to render a React component, start its name with an uppercase letter.\n' +
' in hasOwnProperty (at **)',
]);
} finally {
Object.prototype.toString = realToString; // eslint-disable-line no-extend-native
}
});
it('should throw on children for void elements', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(children);
});
}).rejects.toThrowError(
'input is a void element tag and must neither have `children` nor ' +
'use `dangerouslySetInnerHTML`.',
);
});
it('should throw on dangerouslySetInnerHTML for void elements', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render();
});
}).rejects.toThrowError(
'input is a void element tag and must neither have `children` nor ' +
'use `dangerouslySetInnerHTML`.',
);
});
it('should treat menuitem as a void element but still create the closing tag', async () => {
// menuitem is not implemented in jsdom, so this triggers the unknown warning error
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const returnedValue = ReactDOMServer.renderToString(
,
);
expect(returnedValue).toContain('');
await expect(async () => {
await act(() => {
root.render(
,
);
});
}).rejects.toThrowError(
'menuitem is a void element tag and must neither have `children` nor use ' +
'`dangerouslySetInnerHTML`.',
);
assertConsoleErrorDev([
'The tag