'use strict';
let React;
let ReactDOMClient;
let JSXRuntime;
let JSXDEVRuntime;
let act;
describe('ReactJSXRuntime', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
JSXRuntime = require('react/jsx-runtime');
JSXDEVRuntime = require('react/jsx-dev-runtime');
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;
});
it('allows static methods to be called using the type property', () => {
class StaticMethodComponentClass extends React.Component {
render() {
return JSXRuntime.jsx('div', {});
}
}
StaticMethodComponentClass.someStaticMethod = () => 'someReturnValue';
const element = JSXRuntime.jsx(StaticMethodComponentClass, {});
expect(element.type.someStaticMethod()).toBe('someReturnValue');
});
it('is indistinguishable from a plain object', () => {
const element = JSXRuntime.jsx('div', {className: 'foo'});
const object = {};
expect(element.constructor).toBe(object.constructor);
});
it('should use default prop value when removing a prop', async () => {
class Component extends React.Component {
render() {
return JSXRuntime.jsx('span', {children: [this.props.fruit]});
}
}
Component.defaultProps = {fruit: 'persimmon'};
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(JSXRuntime.jsx(Component, {fruit: 'mango'}));
});
expect(container.firstChild.textContent).toBe('mango');
await act(() => {
root.render(JSXRuntime.jsx(Component, {}));
});
expect(container.firstChild.textContent).toBe('persimmon');
});
it('should normalize props with default values', async () => {
class Component extends React.Component {
render() {
return JSXRuntime.jsx('span', {children: this.props.prop});
}
}
Component.defaultProps = {prop: 'testKey'};
let container = document.createElement('div');
let root = ReactDOMClient.createRoot(container);
let instance;
await act(() => {
root.render(
JSXRuntime.jsx(Component, {ref: current => (instance = current)}),
);
});
expect(instance.props.prop).toBe('testKey');
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
let inst2;
await act(() => {
root.render(
JSXRuntime.jsx(Component, {
prop: null,
ref: current => (inst2 = current),
}),
);
});
expect(inst2.props.prop).toBe(null);
});
it('throws when changing a prop (in dev) after element creation', async () => {
class Outer extends React.Component {
render() {
const el = JSXRuntime.jsx('div', {className: 'moo'});
if (__DEV__) {
expect(function () {
el.props.className = 'quack';
}).toThrow();
expect(el.props.className).toBe('moo');
} else {
el.props.className = 'quack';
expect(el.props.className).toBe('quack');
}
return el;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(JSXRuntime.jsx(Outer, {color: 'orange'}));
});
const outer = container.firstChild;
if (__DEV__) {
expect(outer.className).toBe('moo');
} else {
expect(outer.className).toBe('quack');
}
});
it('throws when adding a prop (in dev) after element creation', async () => {
const container = document.createElement('div');
class Outer extends React.Component {
render() {
const el = JSXRuntime.jsx('div', {children: this.props.sound});
if (__DEV__) {
expect(function () {
el.props.className = 'quack';
}).toThrow();
expect(el.props.className).toBe(undefined);
} else {
el.props.className = 'quack';
expect(el.props.className).toBe('quack');
}
return el;
}
}
Outer.defaultProps = {sound: 'meow'};
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(JSXRuntime.jsx(Outer, {}));
});
expect(container.firstChild.textContent).toBe('meow');
if (__DEV__) {
expect(container.firstChild.className).toBe('');
} else {
expect(container.firstChild.className).toBe('quack');
}
});
it('does not warn for NaN props', async () => {
class Test extends React.Component {
render() {
return JSXRuntime.jsx('div', {});
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let test;
await act(() => {
root.render(
JSXRuntime.jsx(Test, {
value: +undefined,
ref: current => (test = current),
}),
);
});
expect(test.props.value).toBeNaN();
});
it('should warn when `key` is being accessed on composite element', async () => {
const container = document.createElement('div');
class Child extends React.Component {
render() {
return JSXRuntime.jsx('div', {children: this.props.key});
}
}
class Parent extends React.Component {
render() {
return JSXRuntime.jsxs('div', {
children: [
JSXRuntime.jsx(Child, {}, '0'),
JSXRuntime.jsx(Child, {}, '1'),
JSXRuntime.jsx(Child, {}, '2'),
],
});
}
}
await expect(async () => {
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(JSXRuntime.jsx(Parent, {}));
});
}).toErrorDev(
'Child: `key` is not a prop. Trying to access it will result ' +
'in `undefined` being returned. If you need to access the same ' +
'value within the child component, you should pass it as a different ' +
'prop. (https://react.dev/link/special-props)',
);
});
it('warns when a jsxs is passed something that is not an array', async () => {
const container = document.createElement('div');
await expect(async () => {
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(JSXRuntime.jsxs('div', {children: 'foo'}, null));
});
}).toErrorDev(
'React.jsx: Static children should always be an array. ' +
'You are likely explicitly calling React.jsxs or React.jsxDEV. ' +
'Use the Babel transform instead.',
{withoutStack: true},
);
});
it('should warn when `key` is being accessed on a host element', () => {
const element = JSXRuntime.jsxs('div', {}, '3');
expect(() => void element.props.key).toErrorDev(
'div: `key` is not a prop. Trying to access it will result ' +
'in `undefined` being returned. If you need to access the same ' +
'value within the child component, you should pass it as a different ' +
'prop. (https://react.dev/link/special-props)',
{withoutStack: true},
);
});
it('should warn when `ref` is being accessed', async () => {
const container = document.createElement('div');
class Child extends React.Component {
render() {
return JSXRuntime.jsx('div', {children: this.props.ref});
}
}
class Parent extends React.Component {
render() {
return JSXRuntime.jsx('div', {
children: JSXRuntime.jsx(Child, {ref: React.createRef()}),
});
}
}
await expect(async () => {
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(JSXRuntime.jsx(Parent, {}));
});
}).toErrorDev(
'Child: `ref` is not a prop. Trying to access it will result ' +
'in `undefined` being returned. If you need to access the same ' +
'value within the child component, you should pass it as a different ' +
'prop. (https://react.dev/link/special-props)',
);
});
it('should warn when unkeyed children are passed to jsx', async () => {
const container = document.createElement('div');
class Child extends React.Component {
render() {
return JSXRuntime.jsx('div', {});
}
}
class Parent extends React.Component {
render() {
return JSXRuntime.jsx('div', {
children: [
JSXRuntime.jsx(Child, {}),
JSXRuntime.jsx(Child, {}),
JSXRuntime.jsx(Child, {}),
],
});
}
}
await expect(async () => {
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(JSXRuntime.jsx(Parent, {}));
});
}).toErrorDev(
'Each child in a list should have a unique "key" prop.\n\n' +
'Check the render method of `Parent`. See https://react.dev/link/warning-keys for more information.\n' +
(gate(flags => flags.enableOwnerStacks)
? ''
: ' in Child (at **)\n') +
' in Parent (at **)',
);
});
it('should warn when keys are passed as part of props', async () => {
const container = document.createElement('div');
class Child extends React.Component {
render() {
return JSXRuntime.jsx('div', {});
}
}
class Parent extends React.Component {
render() {
return JSXRuntime.jsx('div', {
children: [JSXRuntime.jsx(Child, {key: '0', prop: 'hi'})],
});
}
}
await expect(async () => {
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(JSXRuntime.jsx(Parent, {}));
});
}).toErrorDev(
'A props object containing a "key" prop is being spread into JSX:\n' +
' let props = {key: someKey, prop: ...};\n' +
' <Child {...props} />\n' +
'React keys must be passed directly to JSX without using spread:\n' +
' let props = {prop: ...};\n' +
' <Child key={someKey} {...props} />',
);
});
it('should not warn when unkeyed children are passed to jsxs', async () => {
const container = document.createElement('div');
class Child extends React.Component {
render() {
return JSXRuntime.jsx('div', {});
}
}
class Parent extends React.Component {
render() {
return JSXRuntime.jsxs('div', {
children: [
JSXRuntime.jsx(Child, {}),
JSXRuntime.jsx(Child, {}),
JSXRuntime.jsx(Child, {}),
],
});
}
}
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(JSXRuntime.jsx(Parent, {}));
});
expect(true).toBe(true);
});
it('does not call lazy initializers eagerly', () => {
let didCall = false;
const Lazy = React.lazy(() => {
didCall = true;
return {then() {}};
});
if (__DEV__) {
JSXDEVRuntime.jsxDEV(Lazy, {});
} else {
JSXRuntime.jsx(Lazy, {});
}
expect(didCall).toBe(false);
});
it('does not clone props object if key and ref is not spread', async () => {
const config = {
foo: 'foo',
bar: 'bar',
};
const element = __DEV__
? JSXDEVRuntime.jsxDEV('div', config)
: JSXRuntime.jsx('div', config);
expect(Object.is(element.props, config)).toBe(true);
const configWithKey = {
foo: 'foo',
bar: 'bar',
key: 'key',
};
let elementWithSpreadKey;
expect(() => {
elementWithSpreadKey = __DEV__
? JSXDEVRuntime.jsxDEV('div', configWithKey)
: JSXRuntime.jsx('div', configWithKey);
}).toErrorDev(
'A props object containing a "key" prop is being spread into JSX',
{withoutStack: true},
);
expect(elementWithSpreadKey.props).not.toBe(configWithKey);
});
});