main
coffee 574 lines 18.3 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
8 PropTypes = null
9 React = null
10 ReactDOM = null
11 ReactDOMClient = null
12 assertConsoleErrorDev = null
13 assertConsoleWarnDev = null
14
15 featureFlags = require 'shared/ReactFeatureFlags'
16
17 describe 'ReactCoffeeScriptClass', ->
18 container = null
19 root = null
20 InnerComponent = null
21 attachedListener = null;
22 renderedName = null;
23
24 beforeEach ->
25 React = require 'react'
26 ReactDOM = require 'react-dom'
27 ReactDOMClient = require 'react-dom/client'
28 PropTypes = require 'prop-types'
29 container = document.createElement 'div'
30 root = ReactDOMClient.createRoot container
31 attachedListener = null
32 renderedName = null
33 TestUtils = require 'internal-test-utils'
34 assertConsoleErrorDev = TestUtils.assertConsoleErrorDev
35 assertConsoleWarnDev = TestUtils.assertConsoleWarnDev
36 InnerComponent = class extends React.Component
37 getName: -> this.props.name
38 render: ->
39 attachedListener = this.props.onClick
40 renderedName = this.props.name
41 return React.createElement('div', className: this.props.name)
42
43 test = (element, expectedTag, expectedClassName) ->
44 ReactDOM.flushSync ->
45 root.render(element)
46 expect(container.firstChild).not.toBeNull()
47 expect(container.firstChild.tagName).toBe(expectedTag)
48 expect(container.firstChild.className).toBe(expectedClassName)
49
50 it 'preserves the name of the class for use in error messages', ->
51 class Foo extends React.Component
52 expect(Foo.name).toBe 'Foo'
53
54 it 'throws if no render function is defined', ->
55 class Foo extends React.Component
56 caughtErrors = []
57 errorHandler = (event) ->
58 event.preventDefault()
59 caughtErrors.push(event.error)
60 window.addEventListener 'error', errorHandler;
61 ReactDOM.flushSync ->
62 root.render React.createElement(Foo)
63 assertConsoleErrorDev [
64 # A failed component renders twice in DEV in concurrent mode
65 'No `render` method found on the Foo instance: you may have forgotten to define `render`.\n' +
66 ' in Foo (at **)',
67 'No `render` method found on the Foo instance: you may have forgotten to define `render`.\n' +
68 ' in Foo (at **)',
69 ]
70 window.removeEventListener 'error', errorHandler;
71 expect(caughtErrors).toEqual([
72 expect.objectContaining(
73 message: expect.stringContaining('is not a function')
74 )
75 ])
76
77 it 'renders a simple stateless component with prop', ->
78 class Foo extends React.Component
79 render: ->
80 React.createElement(InnerComponent,
81 name: @props.bar
82 )
83
84 test React.createElement(Foo, bar: 'foo'), 'DIV', 'foo'
85 test React.createElement(Foo, bar: 'bar'), 'DIV', 'bar'
86
87 it 'renders based on state using initial values in this.props', ->
88 class Foo extends React.Component
89 constructor: (props) ->
90 super props
91 @state = bar: @props.initialValue
92
93 render: ->
94 React.createElement('span',
95 className: @state.bar
96 )
97
98 test React.createElement(Foo, initialValue: 'foo'), 'SPAN', 'foo'
99
100 it 'renders based on state using props in the constructor', ->
101 class Foo extends React.Component
102 constructor: (props) ->
103 @state = bar: props.initialValue
104
105 changeState: ->
106 @setState bar: 'bar'
107
108 render: ->
109 if @state.bar is 'foo'
110 return React.createElement('div',
111 className: 'foo'
112 )
113 React.createElement('span',
114 className: @state.bar
115 )
116
117 ref = React.createRef()
118 test React.createElement(Foo, initialValue: 'foo', ref: ref), 'DIV', 'foo'
119 ReactDOM.flushSync ->
120 ref.current.changeState()
121 test React.createElement(Foo), 'SPAN', 'bar'
122
123 it 'sets initial state with value returned by static getDerivedStateFromProps', ->
124 class Foo extends React.Component
125 constructor: (props) ->
126 super props
127 @state = foo: null
128 render: ->
129 React.createElement('div',
130 className: "#{@state.foo} #{@state.bar}"
131 )
132 Foo.getDerivedStateFromProps = (nextProps, prevState) ->
133 {
134 foo: nextProps.foo
135 bar: 'bar'
136 }
137 test React.createElement(Foo, foo: 'foo'), 'DIV', 'foo bar'
138
139 it 'warns if getDerivedStateFromProps is not static', ->
140 class Foo extends React.Component
141 render: ->
142 React.createElement('div')
143 getDerivedStateFromProps: ->
144 {}
145 ReactDOM.flushSync ->
146 root.render React.createElement(Foo, foo: 'foo')
147 assertConsoleErrorDev [
148 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.\n' +
149 ' in Foo (at **)']
150
151 it 'warns if getDerivedStateFromError is not static', ->
152 class Foo extends React.Component
153 render: ->
154 React.createElement('div')
155 getDerivedStateFromError: ->
156 {}
157 ReactDOM.flushSync ->
158 root.render React.createElement(Foo, foo: 'foo')
159
160 assertConsoleErrorDev [
161 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.\n' +
162 ' in Foo (at **)'
163 ]
164
165 it 'warns if getSnapshotBeforeUpdate is static', ->
166 class Foo extends React.Component
167 render: ->
168 React.createElement('div')
169 Foo.getSnapshotBeforeUpdate = () ->
170 {}
171 ReactDOM.flushSync ->
172 root.render React.createElement(Foo, foo: 'foo')
173
174 assertConsoleErrorDev [
175 'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.\n' +
176 ' in Foo (at **)'
177 ]
178
179 it 'warns if state not initialized before static getDerivedStateFromProps', ->
180 class Foo extends React.Component
181 render: ->
182 React.createElement('div',
183 className: "#{@state.foo} #{@state.bar}"
184 )
185 Foo.getDerivedStateFromProps = (nextProps, prevState) ->
186 {
187 foo: nextProps.foo
188 bar: 'bar'
189 }
190 ReactDOM.flushSync ->
191 root.render React.createElement(Foo, foo: 'foo')
192
193 assertConsoleErrorDev [
194 '`Foo` uses `getDerivedStateFromProps` but its initial state is
195 undefined. This is not recommended. Instead, define the initial state by
196 assigning an object to `this.state` in the constructor of `Foo`.
197 This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' +
198 ' in Foo (at **)'
199 ]
200
201 it 'updates initial state with values returned by static getDerivedStateFromProps', ->
202 class Foo extends React.Component
203 constructor: (props, context) ->
204 super props, context
205 @state =
206 foo: 'foo'
207 bar: 'bar'
208 render: ->
209 React.createElement('div',
210 className: "#{@state.foo} #{@state.bar}"
211 )
212 Foo.getDerivedStateFromProps = (nextProps, prevState) ->
213 {
214 foo: "not-#{prevState.foo}"
215 }
216 test React.createElement(Foo), 'DIV', 'not-foo bar'
217
218 it 'renders updated state with values returned by static getDerivedStateFromProps', ->
219 class Foo extends React.Component
220 constructor: (props, context) ->
221 super props, context
222 @state =
223 value: 'initial'
224 render: ->
225 React.createElement('div',
226 className: @state.value
227 )
228 Foo.getDerivedStateFromProps = (nextProps, prevState) ->
229 if nextProps.update
230 return {
231 value: 'updated'
232 }
233 return null
234 test React.createElement(Foo, update: false), 'DIV', 'initial'
235 test React.createElement(Foo, update: true), 'DIV', 'updated'
236
237 if !featureFlags.disableLegacyContext
238 it 'renders based on context in the constructor', ->
239 class Foo extends React.Component
240 @contextTypes:
241 tag: PropTypes.string
242 className: PropTypes.string
243
244 constructor: (props, context) ->
245 super props, context
246 @state =
247 tag: context.tag
248 className: @context.className
249
250 render: ->
251 Tag = @state.tag
252 React.createElement Tag,
253 className: @state.className
254
255 class Outer extends React.Component
256 @childContextTypes:
257 tag: PropTypes.string
258 className: PropTypes.string
259
260 getChildContext: ->
261 tag: 'span'
262 className: 'foo'
263
264 render: ->
265 React.createElement Foo
266
267 test React.createElement(Outer), 'SPAN', 'foo'
268
269 assertConsoleErrorDev([
270 'Outer uses the legacy childContextTypes API which will soon be removed.
271 Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
272 ' in Outer (at **)',
273 'Foo uses the legacy contextTypes API which will soon be removed.
274 Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
275 ' in Outer (at **)',
276 ]);
277
278 it 'renders only once when setting state in componentWillMount', ->
279 renderCount = 0
280 class Foo extends React.Component
281 constructor: (props) ->
282 @state = bar: props.initialValue
283
284 UNSAFE_componentWillMount: ->
285 @setState bar: 'bar'
286
287 render: ->
288 renderCount++
289 React.createElement('span', className: @state.bar)
290
291 test React.createElement(Foo, initialValue: 'foo'), 'SPAN', 'bar'
292 expect(renderCount).toBe(1)
293
294 it 'should warn with non-object in the initial state property', ->
295 [['an array'], 'a string', 1234].forEach (state) ->
296 class Foo extends React.Component
297 constructor: ->
298 @state = state
299
300 render: ->
301 React.createElement('span')
302
303 test React.createElement(Foo), 'SPAN', ''
304 assertConsoleErrorDev [
305 'Foo.state: must be set to an object or null\n' +
306 ' in Foo (at **)'
307 ]
308
309 it 'should render with null in the initial state property', ->
310 class Foo extends React.Component
311 constructor: ->
312 @state = null
313
314 render: ->
315 React.createElement('span')
316
317 test React.createElement(Foo), 'SPAN', ''
318
319 it 'setState through an event handler', ->
320 class Foo extends React.Component
321 constructor: (props) ->
322 @state = bar: props.initialValue
323
324 handleClick: =>
325 @setState bar: 'bar'
326
327 render: ->
328 React.createElement(InnerComponent,
329 name: @state.bar
330 onClick: @handleClick
331 )
332
333 test React.createElement(Foo, initialValue: 'foo'), 'DIV', 'foo'
334 ReactDOM.flushSync ->
335 attachedListener()
336 expect(renderedName).toBe 'bar'
337
338 it 'should not implicitly bind event handlers', ->
339 class Foo extends React.Component
340 constructor: (props) ->
341 @state = bar: props.initialValue
342
343 handleClick: -> # needs double arrow
344 @setState bar: 'bar'
345
346 render: ->
347 React.createElement(InnerComponent,
348 name: @state.bar
349 onClick: @handleClick
350 )
351
352 test React.createElement(Foo, initialValue: 'foo'), 'DIV', 'foo'
353 expect(attachedListener).toThrow()
354
355 it 'renders using forceUpdate even when there is no state', ->
356 class Foo extends React.Component
357 constructor: (props) ->
358 @mutativeValue = props.initialValue
359
360 handleClick: =>
361 @mutativeValue = 'bar'
362 @forceUpdate()
363
364 render: ->
365 React.createElement(InnerComponent,
366 name: @mutativeValue
367 onClick: @handleClick
368 )
369
370 test React.createElement(Foo, initialValue: 'foo'), 'DIV', 'foo'
371 ReactDOM.flushSync ->
372 attachedListener()
373 expect(renderedName).toBe 'bar'
374
375 it 'will call all the normal life cycle methods', ->
376 lifeCycles = []
377 class Foo extends React.Component
378 constructor: ->
379 @state = {}
380
381 UNSAFE_componentWillMount: ->
382 lifeCycles.push 'will-mount'
383
384 componentDidMount: ->
385 lifeCycles.push 'did-mount'
386
387 UNSAFE_componentWillReceiveProps: (nextProps) ->
388 lifeCycles.push 'receive-props', nextProps
389
390 shouldComponentUpdate: (nextProps, nextState) ->
391 lifeCycles.push 'should-update', nextProps, nextState
392 true
393
394 UNSAFE_componentWillUpdate: (nextProps, nextState) ->
395 lifeCycles.push 'will-update', nextProps, nextState
396
397 componentDidUpdate: (prevProps, prevState) ->
398 lifeCycles.push 'did-update', prevProps, prevState
399
400 componentWillUnmount: ->
401 lifeCycles.push 'will-unmount'
402
403 render: ->
404 React.createElement('span',
405 className: @props.value
406 )
407
408 test React.createElement(Foo, value: 'foo'), 'SPAN', 'foo'
409 expect(lifeCycles).toEqual [
410 'will-mount'
411 'did-mount'
412 ]
413 lifeCycles = [] # reset
414 test React.createElement(Foo, value: 'bar'), 'SPAN', 'bar'
415 expect(lifeCycles).toEqual [
416 'receive-props', { value: 'bar' }
417 'should-update', { value: 'bar' }, {}
418 'will-update', { value: 'bar' }, {}
419 'did-update', { value: 'foo' }, {}
420 ]
421 lifeCycles = [] # reset
422 ReactDOM.flushSync ->
423 root.unmount()
424 expect(lifeCycles).toEqual ['will-unmount']
425
426 if !featureFlags.disableLegacyContext
427 it 'warns when classic properties are defined on the instance,
428 but does not invoke them.', ->
429 getInitialStateWasCalled = false
430 getDefaultPropsWasCalled = false
431 class Foo extends React.Component
432 constructor: ->
433 @contextTypes = {}
434 @contextType = {}
435
436 getInitialState: ->
437 getInitialStateWasCalled = true
438 {}
439
440 getDefaultProps: ->
441 getDefaultPropsWasCalled = true
442 {}
443
444 render: ->
445 React.createElement('span',
446 className: 'foo'
447 )
448
449 test React.createElement(Foo), 'SPAN', 'foo'
450 assertConsoleErrorDev [
451 'getInitialState was defined on Foo, a plain JavaScript class.
452 This is only supported for classes created using React.createClass.
453 Did you mean to define a state property instead?\n' +
454 ' in Foo (at **)',
455 'getDefaultProps was defined on Foo, a plain JavaScript class.
456 This is only supported for classes created using React.createClass.
457 Use a static property to define defaultProps instead.\n' +
458 ' in Foo (at **)',
459 'contextType was defined as an instance property on Foo. Use a static property to define contextType instead.\n' +
460 ' in Foo (at **)',
461 'contextTypes was defined as an instance property on Foo. Use a static property to define contextTypes instead.\n' +
462 ' in Foo (at **)',
463 ]
464 expect(getInitialStateWasCalled).toBe false
465 expect(getDefaultPropsWasCalled).toBe false
466
467 it 'does not warn about getInitialState() on class components
468 if state is also defined.', ->
469 class Foo extends React.Component
470 constructor: (props) ->
471 super props
472 @state = bar: @props.initialValue
473
474 getInitialState: ->
475 {}
476
477 render: ->
478 React.createElement('span',
479 className: 'foo'
480 )
481
482 test React.createElement(Foo), 'SPAN', 'foo'
483
484 it 'should warn when misspelling shouldComponentUpdate', ->
485 class NamedComponent extends React.Component
486 componentShouldUpdate: ->
487 false
488
489 render: ->
490 React.createElement('span',
491 className: 'foo'
492 )
493
494 test React.createElement(NamedComponent), 'SPAN', 'foo'
495 assertConsoleErrorDev [
496 'NamedComponent has a method called componentShouldUpdate().
497 Did you mean shouldComponentUpdate()? The name is phrased as a
498 question because the function is expected to return a value.\n' +
499 ' in NamedComponent (at **)'
500 ]
501
502 it 'should warn when misspelling componentWillReceiveProps', ->
503 class NamedComponent extends React.Component
504 componentWillRecieveProps: ->
505 false
506
507 render: ->
508 React.createElement('span',
509 className: 'foo'
510 )
511
512 test React.createElement(NamedComponent), 'SPAN', 'foo'
513 assertConsoleErrorDev [
514 'NamedComponent has a method called componentWillRecieveProps().
515 Did you mean componentWillReceiveProps()?\n' +
516 ' in NamedComponent (at **)'
517 ]
518
519 it 'should warn when misspelling UNSAFE_componentWillReceiveProps', ->
520 class NamedComponent extends React.Component
521 UNSAFE_componentWillRecieveProps: ->
522 false
523
524 render: ->
525 React.createElement('span',
526 className: 'foo'
527 )
528
529 test React.createElement(NamedComponent), 'SPAN', 'foo'
530 assertConsoleErrorDev [
531 'NamedComponent has a method called UNSAFE_componentWillRecieveProps().
532 Did you mean UNSAFE_componentWillReceiveProps()?\n' +
533 ' in NamedComponent (at **)'
534 ]
535
536 it 'should throw AND warn when trying to access classic APIs', ->
537 ref = React.createRef()
538 test React.createElement(InnerComponent, name: 'foo', ref: ref), 'DIV', 'foo'
539
540 expect(-> ref.current.replaceState {}).toThrow()
541 assertConsoleWarnDev([
542 'replaceState(...) is deprecated in plain JavaScript React classes. Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).'])
543
544 expect(-> ref.current.isMounted()).toThrow()
545 assertConsoleWarnDev([
546 'isMounted(...) is deprecated in plain JavaScript React classes. Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks.'])
547
548 if !featureFlags.disableLegacyContext
549 it 'supports this.context passed via getChildContext', ->
550 class Bar extends React.Component
551 @contextTypes:
552 bar: PropTypes.string
553 render: ->
554 React.createElement('div', className: @context.bar)
555
556 class Foo extends React.Component
557 @childContextTypes:
558 bar: PropTypes.string
559 getChildContext: ->
560 bar: 'bar-through-context'
561 render: ->
562 React.createElement Bar
563
564 test React.createElement(Foo), 'DIV', 'bar-through-context'
565 assertConsoleErrorDev [
566 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.
567 (https://react.dev/link/legacy-context)\n' +
568 ' in Foo (at **)',
569 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.
570 (https://react.dev/link/legacy-context)\n' +
571 ' in Foo (at **)'
572 ]
573
574 undefined