| 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 | // TODO: All these warnings should become static errors using Flow instead |
| 13 | // of dynamic errors when using JSX with Flow. |
| 14 | let act; |
| 15 | let React; |
| 16 | let ReactDOMClient; |
| 17 | let assertConsoleErrorDev; |
| 18 | |
| 19 | describe('ReactJSXElementValidator', () => { |
| 20 | let Component; |
| 21 | let RequiredPropComponent; |
| 22 | |
| 23 | beforeEach(() => { |
| 24 | jest.resetModules(); |
| 25 | |
| 26 | ({act, assertConsoleErrorDev} = require('internal-test-utils')); |
| 27 | React = require('react'); |
| 28 | ReactDOMClient = require('react-dom/client'); |
| 29 | |
| 30 | Component = class extends React.Component { |
| 31 | render() { |
| 32 | return <div>{this.props.children}</div>; |
| 33 | } |
| 34 | }; |
| 35 | |
| 36 | RequiredPropComponent = class extends React.Component { |
| 37 | render() { |
| 38 | return <span>{this.props.prop}</span>; |
| 39 | } |
| 40 | }; |
| 41 | RequiredPropComponent.displayName = 'RequiredPropComponent'; |
| 42 | }); |
| 43 | |
| 44 | it('warns for keys for arrays of elements in children position', async () => { |
| 45 | const container = document.createElement('div'); |
| 46 | const root = ReactDOMClient.createRoot(container); |
| 47 | |
| 48 | await act(() => { |
| 49 | root.render(<Component>{[<Component />, <Component />]}</Component>); |
| 50 | }); |
| 51 | assertConsoleErrorDev([ |
| 52 | 'Each child in a list should have a unique "key" prop.\n\n' + |
| 53 | 'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' + |
| 54 | ' in Component (at **)', |
| 55 | ]); |
| 56 | }); |
| 57 | |
| 58 | it('warns for keys for arrays of elements with owner info', async () => { |
| 59 | class InnerComponent extends React.Component { |
| 60 | render() { |
| 61 | return <Component>{this.props.childSet}</Component>; |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | class ComponentWrapper extends React.Component { |
| 66 | render() { |
| 67 | return <InnerComponent childSet={[<Component />, <Component />]} />; |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | const container = document.createElement('div'); |
| 72 | const root = ReactDOMClient.createRoot(container); |
| 73 | await act(() => { |
| 74 | root.render(<ComponentWrapper />); |
| 75 | }); |
| 76 | assertConsoleErrorDev([ |
| 77 | 'Each child in a list should have a unique "key" prop.' + |
| 78 | '\n\nCheck the render method of `Component`. ' + |
| 79 | 'It was passed a child from ComponentWrapper. See https://react.dev/link/warning-keys for more information.\n' + |
| 80 | ' in ComponentWrapper (at **)', |
| 81 | ]); |
| 82 | }); |
| 83 | |
| 84 | it('warns for keys for iterables of elements in rest args', async () => { |
| 85 | const iterable = { |
| 86 | '@@iterator': function () { |
| 87 | let i = 0; |
| 88 | return { |
| 89 | next: function () { |
| 90 | const done = ++i > 2; |
| 91 | return {value: done ? undefined : <Component />, done: done}; |
| 92 | }, |
| 93 | }; |
| 94 | }, |
| 95 | }; |
| 96 | |
| 97 | const container = document.createElement('div'); |
| 98 | const root = ReactDOMClient.createRoot(container); |
| 99 | |
| 100 | await act(() => { |
| 101 | root.render(<Component>{iterable}</Component>); |
| 102 | }); |
| 103 | assertConsoleErrorDev([ |
| 104 | 'Each child in a list should have a unique "key" prop.\n\n' + |
| 105 | 'Check the render method of `Component`. It was passed a child from div. ' + |
| 106 | 'See https://react.dev/link/warning-keys for more information.\n' + |
| 107 | ' in Component (at **)', |
| 108 | ]); |
| 109 | }); |
| 110 | |
| 111 | it('does not warn for arrays of elements with keys', async () => { |
| 112 | const container = document.createElement('div'); |
| 113 | const root = ReactDOMClient.createRoot(container); |
| 114 | |
| 115 | await act(() => { |
| 116 | root.render( |
| 117 | <Component> |
| 118 | {[<Component key="#1" />, <Component key="#2" />]} |
| 119 | </Component>, |
| 120 | ); |
| 121 | }); |
| 122 | }); |
| 123 | |
| 124 | it('does not warn for iterable elements with keys', async () => { |
| 125 | const iterable = { |
| 126 | '@@iterator': function () { |
| 127 | let i = 0; |
| 128 | return { |
| 129 | next: function () { |
| 130 | const done = ++i > 2; |
| 131 | return { |
| 132 | value: done ? undefined : <Component key={'#' + i} />, |
| 133 | done: done, |
| 134 | }; |
| 135 | }, |
| 136 | }; |
| 137 | }, |
| 138 | }; |
| 139 | |
| 140 | const container = document.createElement('div'); |
| 141 | const root = ReactDOMClient.createRoot(container); |
| 142 | |
| 143 | await act(() => { |
| 144 | root.render(<Component>{iterable}</Component>); |
| 145 | }); |
| 146 | }); |
| 147 | |
| 148 | it('does not warn for numeric keys in entry iterable as a child', async () => { |
| 149 | const iterable = { |
| 150 | '@@iterator': function () { |
| 151 | let i = 0; |
| 152 | return { |
| 153 | next: function () { |
| 154 | const done = ++i > 2; |
| 155 | return {value: done ? undefined : [i, <Component />], done: done}; |
| 156 | }, |
| 157 | }; |
| 158 | }, |
| 159 | }; |
| 160 | iterable.entries = iterable['@@iterator']; |
| 161 | |
| 162 | // This only applies to the warning during construction. |
| 163 | // We do warn if it's actually rendered. |
| 164 | <Component>{iterable}</Component>; |
| 165 | }); |
| 166 | |
| 167 | it('does not warn when the element is directly as children', async () => { |
| 168 | const container = document.createElement('div'); |
| 169 | const root = ReactDOMClient.createRoot(container); |
| 170 | await act(() => { |
| 171 | root.render( |
| 172 | <Component> |
| 173 | <Component /> |
| 174 | <Component /> |
| 175 | </Component>, |
| 176 | ); |
| 177 | }); |
| 178 | }); |
| 179 | |
| 180 | it('does not warn when the child array contains non-elements', () => { |
| 181 | void (<Component>{[{}, {}]}</Component>); |
| 182 | }); |
| 183 | |
| 184 | it('should give context for errors in nested components.', async () => { |
| 185 | class MyComp extends React.Component { |
| 186 | render() { |
| 187 | return [<div />]; |
| 188 | } |
| 189 | } |
| 190 | class ParentComp extends React.Component { |
| 191 | render() { |
| 192 | return <MyComp />; |
| 193 | } |
| 194 | } |
| 195 | const container = document.createElement('div'); |
| 196 | const root = ReactDOMClient.createRoot(container); |
| 197 | |
| 198 | await act(() => { |
| 199 | root.render(<ParentComp />); |
| 200 | }); |
| 201 | assertConsoleErrorDev([ |
| 202 | 'Each child in a list should have a unique "key" prop.' + |
| 203 | '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' + |
| 204 | 'See https://react.dev/link/warning-keys for more information.\n' + |
| 205 | ' in div (at **)\n' + |
| 206 | ' in MyComp (at **)\n' + |
| 207 | ' in ParentComp (at **)', |
| 208 | ]); |
| 209 | }); |
| 210 | |
| 211 | it('warns for fragments with illegal attributes', async () => { |
| 212 | class Foo extends React.Component { |
| 213 | render() { |
| 214 | return <React.Fragment a={1}>hello</React.Fragment>; |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | const container = document.createElement('div'); |
| 219 | const root = ReactDOMClient.createRoot(container); |
| 220 | await act(() => { |
| 221 | root.render(<Foo />); |
| 222 | }); |
| 223 | assertConsoleErrorDev([ |
| 224 | gate('enableFragmentRefs') |
| 225 | ? 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + |
| 226 | 'can only have `key`, `ref`, and `children` props.\n' + |
| 227 | ' in Foo (at **)' |
| 228 | : 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + |
| 229 | 'can only have `key` and `children` props.\n' + |
| 230 | ' in Foo (at **)', |
| 231 | ]); |
| 232 | }); |
| 233 | |
| 234 | it('warns for fragments with refs', async () => { |
| 235 | class Foo extends React.Component { |
| 236 | render() { |
| 237 | return ( |
| 238 | <React.Fragment |
| 239 | ref={bar => { |
| 240 | this.foo = bar; |
| 241 | }}> |
| 242 | hello |
| 243 | </React.Fragment> |
| 244 | ); |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | const container = document.createElement('div'); |
| 249 | const root = ReactDOMClient.createRoot(container); |
| 250 | await act(() => { |
| 251 | root.render(<Foo />); |
| 252 | }); |
| 253 | assertConsoleErrorDev( |
| 254 | gate('enableFragmentRefs') |
| 255 | ? [] |
| 256 | : [ |
| 257 | 'Invalid prop `ref` supplied to `React.Fragment`.' + |
| 258 | ' React.Fragment can only have `key` and `children` props.\n' + |
| 259 | ' in Foo (at **)', |
| 260 | ], |
| 261 | ); |
| 262 | }); |
| 263 | |
| 264 | it('does not warn for fragments of multiple elements without keys', async () => { |
| 265 | const container = document.createElement('div'); |
| 266 | const root = ReactDOMClient.createRoot(container); |
| 267 | await act(() => { |
| 268 | root.render( |
| 269 | <> |
| 270 | <span>1</span> |
| 271 | <span>2</span> |
| 272 | </>, |
| 273 | ); |
| 274 | }); |
| 275 | }); |
| 276 | |
| 277 | it('warns for fragments of multiple elements with same key', async () => { |
| 278 | const container = document.createElement('div'); |
| 279 | const root = ReactDOMClient.createRoot(container); |
| 280 | await act(() => { |
| 281 | root.render( |
| 282 | <> |
| 283 | <span key="a">1</span> |
| 284 | <span key="a">2</span> |
| 285 | <span key="b">3</span> |
| 286 | </>, |
| 287 | ); |
| 288 | }); |
| 289 | assertConsoleErrorDev([ |
| 290 | 'Encountered two children with the same key, `a`. ' + |
| 291 | 'Keys should be unique so that components maintain their identity across updates. ' + |
| 292 | 'Non-unique keys may cause children to be duplicated and/or omitted — ' + |
| 293 | 'the behavior is unsupported and could change in a future version.\n' + |
| 294 | ' in span (at **)', |
| 295 | ]); |
| 296 | }); |
| 297 | |
| 298 | it('does not call lazy initializers eagerly', () => { |
| 299 | let didCall = false; |
| 300 | const Lazy = React.lazy(() => { |
| 301 | didCall = true; |
| 302 | return {then() {}}; |
| 303 | }); |
| 304 | <Lazy />; |
| 305 | expect(didCall).toBe(false); |
| 306 | }); |
| 307 | }); |