main
js 379 lines 11.4 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 let React;
13 let ReactDOMClient;
14 let JSXRuntime;
15 let JSXDEVRuntime;
16 let act;
17 let assertConsoleErrorDev;
18
19 // NOTE: Prefer to call the JSXRuntime directly in these tests so we can be
20 // certain that we are testing the runtime behavior, as opposed to the Babel
21 // transform that we use in our tests configuration.
22 describe('ReactJSXRuntime', () => {
23 beforeEach(() => {
24 jest.resetModules();
25
26 React = require('react');
27 JSXRuntime = require('react/jsx-runtime');
28 JSXDEVRuntime = require('react/jsx-dev-runtime');
29 ReactDOMClient = require('react-dom/client');
30 ({act, assertConsoleErrorDev} = require('internal-test-utils'));
31 });
32
33 it('allows static methods to be called using the type property', () => {
34 class StaticMethodComponentClass extends React.Component {
35 render() {
36 return JSXRuntime.jsx('div', {});
37 }
38 }
39 StaticMethodComponentClass.someStaticMethod = () => 'someReturnValue';
40
41 const element = JSXRuntime.jsx(StaticMethodComponentClass, {});
42 expect(element.type.someStaticMethod()).toBe('someReturnValue');
43 });
44
45 it('is indistinguishable from a plain object', () => {
46 const element = JSXRuntime.jsx('div', {className: 'foo'});
47 const object = {};
48 expect(element.constructor).toBe(object.constructor);
49 });
50
51 it('should use default prop value when removing a prop', async () => {
52 class Component extends React.Component {
53 render() {
54 return JSXRuntime.jsx('span', {children: [this.props.fruit]});
55 }
56 }
57 Component.defaultProps = {fruit: 'persimmon'};
58
59 const container = document.createElement('div');
60 const root = ReactDOMClient.createRoot(container);
61 await act(() => {
62 root.render(JSXRuntime.jsx(Component, {fruit: 'mango'}));
63 });
64 expect(container.firstChild.textContent).toBe('mango');
65
66 await act(() => {
67 root.render(JSXRuntime.jsx(Component, {}));
68 });
69 expect(container.firstChild.textContent).toBe('persimmon');
70 });
71
72 it('should normalize props with default values', async () => {
73 class Component extends React.Component {
74 render() {
75 return JSXRuntime.jsx('span', {children: this.props.prop});
76 }
77 }
78 Component.defaultProps = {prop: 'testKey'};
79
80 let container = document.createElement('div');
81 let root = ReactDOMClient.createRoot(container);
82 let instance;
83 await act(() => {
84 root.render(
85 JSXRuntime.jsx(Component, {ref: current => (instance = current)}),
86 );
87 });
88
89 expect(instance.props.prop).toBe('testKey');
90
91 container = document.createElement('div');
92 root = ReactDOMClient.createRoot(container);
93 let inst2;
94 await act(() => {
95 root.render(
96 JSXRuntime.jsx(Component, {
97 prop: null,
98 ref: current => (inst2 = current),
99 }),
100 );
101 });
102
103 expect(inst2.props.prop).toBe(null);
104 });
105
106 it('throws when changing a prop (in dev) after element creation', async () => {
107 class Outer extends React.Component {
108 render() {
109 const el = JSXRuntime.jsx('div', {className: 'moo'});
110
111 if (__DEV__) {
112 expect(function () {
113 el.props.className = 'quack';
114 }).toThrow();
115 expect(el.props.className).toBe('moo');
116 } else {
117 el.props.className = 'quack';
118 expect(el.props.className).toBe('quack');
119 }
120
121 return el;
122 }
123 }
124 const container = document.createElement('div');
125 const root = ReactDOMClient.createRoot(container);
126 await act(() => {
127 root.render(JSXRuntime.jsx(Outer, {color: 'orange'}));
128 });
129
130 const outer = container.firstChild;
131 if (__DEV__) {
132 expect(outer.className).toBe('moo');
133 } else {
134 expect(outer.className).toBe('quack');
135 }
136 });
137
138 it('throws when adding a prop (in dev) after element creation', async () => {
139 const container = document.createElement('div');
140 class Outer extends React.Component {
141 render() {
142 const el = JSXRuntime.jsx('div', {children: this.props.sound});
143
144 if (__DEV__) {
145 expect(function () {
146 el.props.className = 'quack';
147 }).toThrow();
148 expect(el.props.className).toBe(undefined);
149 } else {
150 el.props.className = 'quack';
151 expect(el.props.className).toBe('quack');
152 }
153
154 return el;
155 }
156 }
157 Outer.defaultProps = {sound: 'meow'};
158 const root = ReactDOMClient.createRoot(container);
159 await act(() => {
160 root.render(JSXRuntime.jsx(Outer, {}));
161 });
162 expect(container.firstChild.textContent).toBe('meow');
163 if (__DEV__) {
164 expect(container.firstChild.className).toBe('');
165 } else {
166 expect(container.firstChild.className).toBe('quack');
167 }
168 });
169
170 it('does not warn for NaN props', async () => {
171 class Test extends React.Component {
172 render() {
173 return JSXRuntime.jsx('div', {});
174 }
175 }
176 const container = document.createElement('div');
177 const root = ReactDOMClient.createRoot(container);
178 let test;
179 await act(() => {
180 root.render(
181 JSXRuntime.jsx(Test, {
182 value: +undefined,
183 ref: current => (test = current),
184 }),
185 );
186 });
187
188 expect(test.props.value).toBeNaN();
189 });
190
191 it('should warn when `key` is being accessed on composite element', async () => {
192 const container = document.createElement('div');
193 class Child extends React.Component {
194 render() {
195 return JSXRuntime.jsx('div', {children: this.props.key});
196 }
197 }
198 class Parent extends React.Component {
199 render() {
200 return JSXRuntime.jsxs('div', {
201 children: [
202 JSXRuntime.jsx(Child, {}, '0'),
203 JSXRuntime.jsx(Child, {}, '1'),
204 JSXRuntime.jsx(Child, {}, '2'),
205 ],
206 });
207 }
208 }
209 const root = ReactDOMClient.createRoot(container);
210 await act(() => {
211 root.render(JSXRuntime.jsx(Parent, {}));
212 });
213 assertConsoleErrorDev([
214 'Child: `key` is not a prop. Trying to access it will result ' +
215 'in `undefined` being returned. If you need to access the same ' +
216 'value within the child component, you should pass it as a different ' +
217 'prop. (https://react.dev/link/special-props)\n' +
218 ' in Parent (at **)',
219 ]);
220 });
221
222 it('warns when a jsxs is passed something that is not an array', async () => {
223 const container = document.createElement('div');
224 const root = ReactDOMClient.createRoot(container);
225 await act(() => {
226 root.render(JSXRuntime.jsxs('div', {children: 'foo'}, null));
227 });
228 assertConsoleErrorDev([
229 'React.jsx: Static children should always be an array. ' +
230 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' +
231 'Use the Babel transform instead.',
232 ]);
233 });
234
235 it('should warn when `key` is being accessed on a host element', () => {
236 const element = JSXRuntime.jsxs('div', {}, '3');
237 void element.props.key;
238 assertConsoleErrorDev([
239 'div: `key` is not a prop. Trying to access it will result ' +
240 'in `undefined` being returned. If you need to access the same ' +
241 'value within the child component, you should pass it as a different ' +
242 'prop. (https://react.dev/link/special-props)',
243 ]);
244 });
245
246 it('should warn when unkeyed children are passed to jsx', async () => {
247 const container = document.createElement('div');
248
249 class Child extends React.Component {
250 render() {
251 return JSXRuntime.jsx('div', {});
252 }
253 }
254 class Parent extends React.Component {
255 render() {
256 return JSXRuntime.jsx('div', {
257 children: [
258 JSXRuntime.jsx(Child, {}),
259 JSXRuntime.jsx(Child, {}),
260 JSXRuntime.jsx(Child, {}),
261 ],
262 });
263 }
264 }
265 const root = ReactDOMClient.createRoot(container);
266 await act(() => {
267 root.render(JSXRuntime.jsx(Parent, {}));
268 });
269 assertConsoleErrorDev([
270 'Each child in a list should have a unique "key" prop.\n\n' +
271 'Check the render method of `Parent`. See https://react.dev/link/warning-keys for more information.\n' +
272 ' in Parent (at **)',
273 ]);
274 });
275
276 it('should warn when keys are passed as part of props', async () => {
277 const container = document.createElement('div');
278 class Child extends React.Component {
279 render() {
280 return JSXRuntime.jsx('div', {});
281 }
282 }
283 class Parent extends React.Component {
284 render() {
285 return JSXRuntime.jsx('div', {
286 children: [JSXRuntime.jsx(Child, {key: '0', prop: 'hi'})],
287 });
288 }
289 }
290 const root = ReactDOMClient.createRoot(container);
291 await act(() => {
292 root.render(JSXRuntime.jsx(Parent, {}));
293 });
294 assertConsoleErrorDev([
295 'A props object containing a "key" prop is being spread into JSX:\n' +
296 ' let props = {key: someKey, prop: ...};\n' +
297 ' <Child {...props} />\n' +
298 'React keys must be passed directly to JSX without using spread:\n' +
299 ' let props = {prop: ...};\n' +
300 ' <Child key={someKey} {...props} />\n' +
301 ' in Parent (at **)',
302 ]);
303 });
304
305 it('should not warn when unkeyed children are passed to jsxs', async () => {
306 const container = document.createElement('div');
307 class Child extends React.Component {
308 render() {
309 return JSXRuntime.jsx('div', {});
310 }
311 }
312 class Parent extends React.Component {
313 render() {
314 return JSXRuntime.jsxs('div', {
315 children: [
316 JSXRuntime.jsx(Child, {}),
317 JSXRuntime.jsx(Child, {}),
318 JSXRuntime.jsx(Child, {}),
319 ],
320 });
321 }
322 }
323
324 const root = ReactDOMClient.createRoot(container);
325 await act(() => {
326 root.render(JSXRuntime.jsx(Parent, {}));
327 });
328
329 // Test shouldn't throw any errors.
330 expect(true).toBe(true);
331 });
332
333 it('does not call lazy initializers eagerly', () => {
334 let didCall = false;
335 const Lazy = React.lazy(() => {
336 didCall = true;
337 return {then() {}};
338 });
339 if (__DEV__) {
340 JSXDEVRuntime.jsxDEV(Lazy, {});
341 } else {
342 JSXRuntime.jsx(Lazy, {});
343 }
344 expect(didCall).toBe(false);
345 });
346
347 it('does not clone props object if key and ref is not spread', async () => {
348 const config = {
349 foo: 'foo',
350 bar: 'bar',
351 };
352
353 const element = __DEV__
354 ? JSXDEVRuntime.jsxDEV('div', config)
355 : JSXRuntime.jsx('div', config);
356 expect(Object.is(element.props, config)).toBe(true);
357
358 const configWithKey = {
359 foo: 'foo',
360 bar: 'bar',
361 // This only happens when the key is spread onto the element. A statically
362 // defined key is passed as a separate argument to the jsx() runtime.
363 key: 'key',
364 };
365
366 const elementWithSpreadKey = __DEV__
367 ? JSXDEVRuntime.jsxDEV('div', configWithKey)
368 : JSXRuntime.jsx('div', configWithKey);
369 assertConsoleErrorDev([
370 'A props object containing a "key" prop is being spread into JSX:\n' +
371 ' let props = {key: someKey, foo: ..., bar: ...};\n' +
372 ' <div {...props} />\n' +
373 'React keys must be passed directly to JSX without using spread:\n' +
374 ' let props = {foo: ..., bar: ...};\n' +
375 ' <div key={someKey} {...props} />',
376 ]);
377 expect(elementWithSpreadKey.props).not.toBe(configWithKey);
378 });
379 });