| 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 | // NOTE: We're explicitly not using JSX in this file. This is intended to test |
| 13 | // classic React.createElement without JSX. |
| 14 | // TODO: ^ the above note is a bit stale because there are tests in this file |
| 15 | // that do use JSX syntax. We should port them to React.createElement, and also |
| 16 | // confirm there's a corresponding test that uses JSX syntax. |
| 17 | |
| 18 | let React; |
| 19 | let ReactDOMClient; |
| 20 | let act; |
| 21 | let assertConsoleErrorDev; |
| 22 | |
| 23 | describe('ReactElementValidator', () => { |
| 24 | let ComponentClass; |
| 25 | |
| 26 | beforeEach(() => { |
| 27 | jest.resetModules(); |
| 28 | |
| 29 | React = require('react'); |
| 30 | ReactDOMClient = require('react-dom/client'); |
| 31 | ({act, assertConsoleErrorDev} = require('internal-test-utils')); |
| 32 | ComponentClass = class extends React.Component { |
| 33 | render() { |
| 34 | return React.createElement('div', null, this.props.children); |
| 35 | } |
| 36 | }; |
| 37 | }); |
| 38 | |
| 39 | it('warns for keys for arrays of elements in rest args', async () => { |
| 40 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 41 | await act(() => |
| 42 | root.render( |
| 43 | React.createElement(ComponentClass, null, [ |
| 44 | React.createElement(ComponentClass), |
| 45 | React.createElement(ComponentClass), |
| 46 | ]), |
| 47 | ), |
| 48 | ); |
| 49 | assertConsoleErrorDev([ |
| 50 | 'Each child in a list should have a unique "key" prop.\n\n' + |
| 51 | 'Check the render method of `ComponentClass`. See https://react.dev/link/warning-keys for more information.\n' + |
| 52 | ' in ComponentClass (at **)', |
| 53 | ]); |
| 54 | }); |
| 55 | |
| 56 | it('warns for keys for arrays of elements with owner info', async () => { |
| 57 | class InnerClass extends React.Component { |
| 58 | render() { |
| 59 | return React.createElement(ComponentClass, null, this.props.childSet); |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | class ComponentWrapper extends React.Component { |
| 64 | render() { |
| 65 | return React.createElement(InnerClass, { |
| 66 | childSet: [ |
| 67 | React.createElement(ComponentClass), |
| 68 | React.createElement(ComponentClass), |
| 69 | ], |
| 70 | }); |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 75 | await act(() => root.render(React.createElement(ComponentWrapper))); |
| 76 | assertConsoleErrorDev([ |
| 77 | 'Each child in a list should have a unique "key" prop.' + |
| 78 | '\n\nCheck the render method of `ComponentClass`. ' + |
| 79 | 'It was passed a child from ComponentWrapper. ' + |
| 80 | 'See https://react.dev/link/warning-keys for more information.\n' + |
| 81 | ' in ComponentWrapper (at **)', |
| 82 | ]); |
| 83 | }); |
| 84 | |
| 85 | it('warns for keys for arrays with no owner or parent info', async () => { |
| 86 | function Anonymous({children}) { |
| 87 | return <div>{children}</div>; |
| 88 | } |
| 89 | Object.defineProperty(Anonymous, 'name', {value: undefined}); |
| 90 | |
| 91 | const divs = [<div />, <div />]; |
| 92 | |
| 93 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 94 | await act(() => root.render(<Anonymous>{divs}</Anonymous>)); |
| 95 | assertConsoleErrorDev([ |
| 96 | // For owner stacks the parent being validated is the div. |
| 97 | 'Each child in a list should have a unique ' + |
| 98 | '"key" prop.' + |
| 99 | '\n\nCheck the top-level render call using <div>. ' + |
| 100 | 'See https://react.dev/link/warning-keys for more information.\n' + |
| 101 | ' in div (at **)', |
| 102 | ]); |
| 103 | }); |
| 104 | |
| 105 | it('warns for keys for arrays of elements with no owner info', async () => { |
| 106 | const divs = [<div />, <div />]; |
| 107 | |
| 108 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 109 | |
| 110 | await act(() => root.render(<div>{divs}</div>)); |
| 111 | assertConsoleErrorDev([ |
| 112 | 'Each child in a list should have a unique ' + |
| 113 | '"key" prop.' + |
| 114 | '\n\nCheck the top-level render call using <div>. ' + |
| 115 | 'See https://react.dev/link/warning-keys for more information.\n' + |
| 116 | ' in div (at **)', |
| 117 | ]); |
| 118 | }); |
| 119 | |
| 120 | it('warns for keys with component stack info', async () => { |
| 121 | function Component() { |
| 122 | return <div>{[<div />, <div />]}</div>; |
| 123 | } |
| 124 | |
| 125 | function Parent(props) { |
| 126 | return React.cloneElement(props.child); |
| 127 | } |
| 128 | |
| 129 | function GrandParent() { |
| 130 | return <Parent child={<Component />} />; |
| 131 | } |
| 132 | |
| 133 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 134 | await act(() => root.render(<GrandParent />)); |
| 135 | assertConsoleErrorDev([ |
| 136 | 'Each child in a list should have a unique ' + |
| 137 | '"key" prop.\n\nCheck the render method of `Component`. See ' + |
| 138 | 'https://react.dev/link/warning-keys for more information.\n' + |
| 139 | ' in div (at **)\n' + |
| 140 | ' in Component (at **)\n' + |
| 141 | ' in GrandParent (at **)', |
| 142 | ]); |
| 143 | }); |
| 144 | |
| 145 | it('does not warn for keys when passing children down', async () => { |
| 146 | function Wrapper(props) { |
| 147 | return ( |
| 148 | <div> |
| 149 | {props.children} |
| 150 | <footer /> |
| 151 | </div> |
| 152 | ); |
| 153 | } |
| 154 | |
| 155 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 156 | await act(() => |
| 157 | root.render( |
| 158 | <Wrapper> |
| 159 | <span /> |
| 160 | <span /> |
| 161 | </Wrapper>, |
| 162 | ), |
| 163 | ); |
| 164 | }); |
| 165 | |
| 166 | it('warns for keys for iterables of elements in rest args', async () => { |
| 167 | const iterable = { |
| 168 | '@@iterator': function () { |
| 169 | let i = 0; |
| 170 | return { |
| 171 | next: function () { |
| 172 | const done = ++i > 2; |
| 173 | return { |
| 174 | value: done ? undefined : React.createElement(ComponentClass), |
| 175 | done: done, |
| 176 | }; |
| 177 | }, |
| 178 | }; |
| 179 | }, |
| 180 | }; |
| 181 | |
| 182 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 183 | await act(() => |
| 184 | root.render(React.createElement(ComponentClass, null, iterable)), |
| 185 | ); |
| 186 | assertConsoleErrorDev([ |
| 187 | 'Each child in a list should have a unique "key" prop.\n\n' + |
| 188 | 'Check the render method of `ComponentClass`. It was passed a child from div. ' + |
| 189 | 'See https://react.dev/link/warning-keys for more information.\n' + |
| 190 | ' in ComponentClass (at **)', |
| 191 | ]); |
| 192 | }); |
| 193 | |
| 194 | it('does not warns for arrays of elements with keys', () => { |
| 195 | React.createElement(ComponentClass, null, [ |
| 196 | React.createElement(ComponentClass, {key: '#1'}), |
| 197 | React.createElement(ComponentClass, {key: '#2'}), |
| 198 | ]); |
| 199 | }); |
| 200 | |
| 201 | it('does not warns for iterable elements with keys', () => { |
| 202 | const iterable = { |
| 203 | '@@iterator': function () { |
| 204 | let i = 0; |
| 205 | return { |
| 206 | next: function () { |
| 207 | const done = ++i > 2; |
| 208 | return { |
| 209 | value: done |
| 210 | ? undefined |
| 211 | : React.createElement(ComponentClass, {key: '#' + i}), |
| 212 | done: done, |
| 213 | }; |
| 214 | }, |
| 215 | }; |
| 216 | }, |
| 217 | }; |
| 218 | |
| 219 | React.createElement(ComponentClass, null, iterable); |
| 220 | }); |
| 221 | |
| 222 | it('does not warn when the element is directly in rest args', () => { |
| 223 | React.createElement( |
| 224 | ComponentClass, |
| 225 | null, |
| 226 | React.createElement(ComponentClass), |
| 227 | React.createElement(ComponentClass), |
| 228 | ); |
| 229 | }); |
| 230 | |
| 231 | it('does not warn when the array contains a non-element', () => { |
| 232 | React.createElement(ComponentClass, null, [{}, {}]); |
| 233 | }); |
| 234 | |
| 235 | it('should give context for errors in nested components.', async () => { |
| 236 | function MyComp() { |
| 237 | return [React.createElement('div')]; |
| 238 | } |
| 239 | function ParentComp() { |
| 240 | return React.createElement(MyComp); |
| 241 | } |
| 242 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 243 | await act(() => root.render(React.createElement(ParentComp))); |
| 244 | assertConsoleErrorDev([ |
| 245 | 'Each child in a list should have a unique "key" prop.' + |
| 246 | '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' + |
| 247 | 'See https://react.dev/link/warning-keys for more information.\n' + |
| 248 | ' in div (at **)\n' + |
| 249 | ' in MyComp (at **)\n' + |
| 250 | ' in ParentComp (at **)', |
| 251 | ]); |
| 252 | }); |
| 253 | |
| 254 | it('gives a helpful error when passing invalid types', async () => { |
| 255 | function Foo() {} |
| 256 | const errors = []; |
| 257 | const root = ReactDOMClient.createRoot(document.createElement('div'), { |
| 258 | onUncaughtError(error) { |
| 259 | errors.push(error.message); |
| 260 | }, |
| 261 | }); |
| 262 | const cases = [ |
| 263 | [ |
| 264 | () => React.createElement(undefined), |
| 265 | 'React.createElement: type is invalid -- expected a string ' + |
| 266 | '(for built-in components) or a class/function (for composite ' + |
| 267 | 'components) but got: undefined. You likely forgot to export your ' + |
| 268 | "component from the file it's defined in, or you might have mixed up " + |
| 269 | 'default and named imports.', |
| 270 | ], |
| 271 | [ |
| 272 | () => React.createElement(null), |
| 273 | 'React.createElement: type is invalid -- expected a string ' + |
| 274 | '(for built-in components) or a class/function (for composite ' + |
| 275 | 'components) but got: null.', |
| 276 | ], |
| 277 | [ |
| 278 | () => React.createElement(true), |
| 279 | 'React.createElement: type is invalid -- expected a string ' + |
| 280 | '(for built-in components) or a class/function (for composite ' + |
| 281 | 'components) but got: boolean.', |
| 282 | ], |
| 283 | [ |
| 284 | () => React.createElement({x: 17}), |
| 285 | 'React.createElement: type is invalid -- expected a string ' + |
| 286 | '(for built-in components) or a class/function (for composite ' + |
| 287 | 'components) but got: object.', |
| 288 | ], |
| 289 | [ |
| 290 | () => React.createElement({}), |
| 291 | 'React.createElement: type is invalid -- expected a string ' + |
| 292 | '(for built-in components) or a class/function (for composite ' + |
| 293 | 'components) but got: object. You likely forgot to export your ' + |
| 294 | "component from the file it's defined in, or you might have mixed up " + |
| 295 | 'default and named imports.', |
| 296 | ], |
| 297 | [ |
| 298 | () => React.createElement(React.createElement('div')), |
| 299 | 'React.createElement: type is invalid -- expected a string ' + |
| 300 | '(for built-in components) or a class/function (for composite ' + |
| 301 | 'components) but got: <div />. Did you accidentally export a JSX literal ' + |
| 302 | 'instead of a component?', |
| 303 | ], |
| 304 | [ |
| 305 | () => React.createElement(React.createElement(Foo)), |
| 306 | 'React.createElement: type is invalid -- expected a string ' + |
| 307 | '(for built-in components) or a class/function (for composite ' + |
| 308 | 'components) but got: <Foo />. Did you accidentally export a JSX literal ' + |
| 309 | 'instead of a component?', |
| 310 | ], |
| 311 | [ |
| 312 | () => |
| 313 | React.createElement( |
| 314 | React.createElement(React.createContext().Consumer), |
| 315 | ), |
| 316 | 'React.createElement: type is invalid -- expected a string ' + |
| 317 | '(for built-in components) or a class/function (for composite ' + |
| 318 | 'components) but got: <Context.Consumer />. Did you accidentally ' + |
| 319 | 'export a JSX literal instead of a component?', |
| 320 | ], |
| 321 | [ |
| 322 | () => React.createElement({$$typeof: 'non-react-thing'}), |
| 323 | 'React.createElement: type is invalid -- expected a string ' + |
| 324 | '(for built-in components) or a class/function (for composite ' + |
| 325 | 'components) but got: object.', |
| 326 | ], |
| 327 | ]; |
| 328 | for (let i = 0; i < cases.length; i++) { |
| 329 | await act(async () => root.render(cases[i][0]())); |
| 330 | } |
| 331 | |
| 332 | expect(errors).toEqual( |
| 333 | __DEV__ |
| 334 | ? [ |
| 335 | 'Element type is invalid: expected a string ' + |
| 336 | '(for built-in components) or a class/function (for composite ' + |
| 337 | 'components) but got: undefined. You likely forgot to export your ' + |
| 338 | "component from the file it's defined in, or you might have mixed up " + |
| 339 | 'default and named imports.', |
| 340 | 'Element type is invalid: expected a string ' + |
| 341 | '(for built-in components) or a class/function (for composite ' + |
| 342 | 'components) but got: null.', |
| 343 | 'Element type is invalid: expected a string ' + |
| 344 | '(for built-in components) or a class/function (for composite ' + |
| 345 | 'components) but got: boolean.', |
| 346 | 'Element type is invalid: expected a string ' + |
| 347 | '(for built-in components) or a class/function (for composite ' + |
| 348 | 'components) but got: object.', |
| 349 | 'Element type is invalid: expected a string ' + |
| 350 | '(for built-in components) or a class/function (for composite ' + |
| 351 | 'components) but got: object. You likely forgot to export your ' + |
| 352 | "component from the file it's defined in, or you might have mixed up " + |
| 353 | 'default and named imports.', |
| 354 | 'Element type is invalid: expected a string ' + |
| 355 | '(for built-in components) or a class/function (for composite ' + |
| 356 | 'components) but got: <div />. Did you accidentally export a JSX literal ' + |
| 357 | 'instead of a component?', |
| 358 | 'Element type is invalid: expected a string ' + |
| 359 | '(for built-in components) or a class/function (for composite ' + |
| 360 | 'components) but got: <Foo />. Did you accidentally export a JSX literal ' + |
| 361 | 'instead of a component?', |
| 362 | 'Element type is invalid: expected a string ' + |
| 363 | '(for built-in components) or a class/function (for composite ' + |
| 364 | 'components) but got: <Context.Consumer />. Did you accidentally ' + |
| 365 | 'export a JSX literal instead of a component?', |
| 366 | 'Element type is invalid: expected a string ' + |
| 367 | '(for built-in components) or a class/function (for composite ' + |
| 368 | 'components) but got: object.', |
| 369 | ] |
| 370 | : [ |
| 371 | 'Element type is invalid: expected a string ' + |
| 372 | '(for built-in components) or a class/function (for composite ' + |
| 373 | 'components) but got: undefined.', |
| 374 | 'Element type is invalid: expected a string ' + |
| 375 | '(for built-in components) or a class/function (for composite ' + |
| 376 | 'components) but got: null.', |
| 377 | 'Element type is invalid: expected a string ' + |
| 378 | '(for built-in components) or a class/function (for composite ' + |
| 379 | 'components) but got: boolean.', |
| 380 | 'Element type is invalid: expected a string ' + |
| 381 | '(for built-in components) or a class/function (for composite ' + |
| 382 | 'components) but got: object.', |
| 383 | 'Element type is invalid: expected a string ' + |
| 384 | '(for built-in components) or a class/function (for composite ' + |
| 385 | 'components) but got: object.', |
| 386 | 'Element type is invalid: expected a string ' + |
| 387 | '(for built-in components) or a class/function (for composite ' + |
| 388 | 'components) but got: object.', |
| 389 | 'Element type is invalid: expected a string ' + |
| 390 | '(for built-in components) or a class/function (for composite ' + |
| 391 | 'components) but got: object.', |
| 392 | 'Element type is invalid: expected a string ' + |
| 393 | '(for built-in components) or a class/function (for composite ' + |
| 394 | 'components) but got: object.', |
| 395 | 'Element type is invalid: expected a string ' + |
| 396 | '(for built-in components) or a class/function (for composite ' + |
| 397 | 'components) but got: object.', |
| 398 | ], |
| 399 | ); |
| 400 | |
| 401 | // Should not log any additional warnings |
| 402 | React.createElement('div'); |
| 403 | }); |
| 404 | |
| 405 | it('includes the owner name when passing null, undefined, boolean, or number', async () => { |
| 406 | function ParentComp() { |
| 407 | return React.createElement(null); |
| 408 | } |
| 409 | |
| 410 | await expect(async () => { |
| 411 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 412 | await act(() => root.render(React.createElement(ParentComp))); |
| 413 | }).rejects.toThrow( |
| 414 | 'Element type is invalid: expected a string (for built-in components) ' + |
| 415 | 'or a class/function (for composite components) but got: null.' + |
| 416 | (__DEV__ ? '\n\nCheck the render method of `ParentComp`.' : ''), |
| 417 | ); |
| 418 | }); |
| 419 | |
| 420 | it('warns for fragments with illegal attributes', async () => { |
| 421 | class Foo extends React.Component { |
| 422 | render() { |
| 423 | return React.createElement(React.Fragment, {a: 1}, '123'); |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 428 | await act(() => root.render(React.createElement(Foo))); |
| 429 | assertConsoleErrorDev([ |
| 430 | gate('enableFragmentRefs') |
| 431 | ? 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + |
| 432 | 'can only have `key`, `ref`, and `children` props.\n' + |
| 433 | ' in Foo (at **)' |
| 434 | : 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + |
| 435 | 'can only have `key` and `children` props.\n' + |
| 436 | ' in Foo (at **)', |
| 437 | ]); |
| 438 | }); |
| 439 | |
| 440 | it('does not warn when using DOM node as children', async () => { |
| 441 | class DOMContainer extends React.Component { |
| 442 | ref; |
| 443 | render() { |
| 444 | return <div ref={n => (this.ref = n)} />; |
| 445 | } |
| 446 | componentDidMount() { |
| 447 | this.ref.appendChild(this.props.children); |
| 448 | } |
| 449 | } |
| 450 | |
| 451 | const node = document.createElement('div'); |
| 452 | const root = ReactDOMClient.createRoot(document.createElement('div')); |
| 453 | await act(() => { |
| 454 | // This shouldn't cause a stack overflow or any other problems (#3883) |
| 455 | root.render(<DOMContainer>{node}</DOMContainer>); |
| 456 | }); |
| 457 | }); |
| 458 | |
| 459 | it('should not enumerate enumerable numbers (#4776)', () => { |
| 460 | /*eslint-disable no-extend-native */ |
| 461 | Number.prototype['@@iterator'] = function () { |
| 462 | throw new Error('number iterator called'); |
| 463 | }; |
| 464 | /*eslint-enable no-extend-native */ |
| 465 | |
| 466 | try { |
| 467 | void ( |
| 468 | <div> |
| 469 | {5} |
| 470 | {12} |
| 471 | {13} |
| 472 | </div> |
| 473 | ); |
| 474 | } finally { |
| 475 | delete Number.prototype['@@iterator']; |
| 476 | } |
| 477 | }); |
| 478 | |
| 479 | it('does not blow up with inlined children', () => { |
| 480 | // We don't suggest this since it silences all sorts of warnings, but we |
| 481 | // shouldn't blow up either. |
| 482 | |
| 483 | const child = { |
| 484 | $$typeof: (<div />).$$typeof, |
| 485 | type: 'span', |
| 486 | key: null, |
| 487 | ref: null, |
| 488 | props: {}, |
| 489 | _owner: null, |
| 490 | }; |
| 491 | |
| 492 | void (<div>{[child]}</div>); |
| 493 | }); |
| 494 | |
| 495 | it('does not blow up on key warning with undefined type', () => { |
| 496 | const Foo = undefined; |
| 497 | void (<Foo>{[<div />]}</Foo>); |
| 498 | }); |
| 499 | |
| 500 | it('does not call lazy initializers eagerly', () => { |
| 501 | let didCall = false; |
| 502 | const Lazy = React.lazy(() => { |
| 503 | didCall = true; |
| 504 | return {then() {}}; |
| 505 | }); |
| 506 | React.createElement(Lazy); |
| 507 | expect(didCall).toBe(false); |
| 508 | }); |
| 509 | |
| 510 | it('__self and __source are treated as normal props', async () => { |
| 511 | // These used to be reserved props because the classic React.createElement |
| 512 | // runtime passed this data as props, whereas the jsxDEV() runtime passes |
| 513 | // them as separate arguments. |
| 514 | function Child({__self, __source}) { |
| 515 | return __self + __source; |
| 516 | } |
| 517 | |
| 518 | const container = document.createElement('div'); |
| 519 | const root = ReactDOMClient.createRoot(container); |
| 520 | // NOTE: The Babel transform treats the presence of these props as a syntax |
| 521 | // error but theoretically it doesn't have to. Using spread here to |
| 522 | // circumvent the syntax error and demonstrate that the runtime |
| 523 | // doesn't care. |
| 524 | const props = { |
| 525 | __self: 'Hello ', |
| 526 | __source: 'world!', |
| 527 | }; |
| 528 | await act(() => root.render(<Child {...props} />)); |
| 529 | expect(container.textContent).toBe('Hello world!'); |
| 530 | }); |
| 531 | }); |