| 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 | }); |