PropTypes = null
React = null
ReactDOM = null
ReactDOMClient = null
assertConsoleErrorDev = null
assertConsoleWarnDev = null
featureFlags = require 'shared/ReactFeatureFlags'
describe 'ReactCoffeeScriptClass', ->
container = null
root = null
InnerComponent = null
attachedListener = null;
renderedName = null;
beforeEach ->
React = require 'react'
ReactDOM = require 'react-dom'
ReactDOMClient = require 'react-dom/client'
PropTypes = require 'prop-types'
container = document.createElement 'div'
root = ReactDOMClient.createRoot container
attachedListener = null
renderedName = null
TestUtils = require 'internal-test-utils'
assertConsoleErrorDev = TestUtils.assertConsoleErrorDev
assertConsoleWarnDev = TestUtils.assertConsoleWarnDev
InnerComponent = class extends React.Component
getName: -> this.props.name
render: ->
attachedListener = this.props.onClick
renderedName = this.props.name
return React.createElement('div', className: this.props.name)
test = (element, expectedTag, expectedClassName) ->
ReactDOM.flushSync ->
root.render(element)
expect(container.firstChild).not.toBeNull()
expect(container.firstChild.tagName).toBe(expectedTag)
expect(container.firstChild.className).toBe(expectedClassName)
it 'preserves the name of the class for use in error messages', ->
class Foo extends React.Component
expect(Foo.name).toBe 'Foo'
it 'throws if no render function is defined', ->
class Foo extends React.Component
caughtErrors = []
errorHandler = (event) ->
event.preventDefault()
caughtErrors.push(event.error)
window.addEventListener 'error', errorHandler;
ReactDOM.flushSync ->
root.render React.createElement(Foo)
assertConsoleErrorDev [
'No `render` method found on the Foo instance: you may have forgotten to define `render`.\n' +
' in Foo (at **)',
'No `render` method found on the Foo instance: you may have forgotten to define `render`.\n' +
' in Foo (at **)',
]
window.removeEventListener 'error', errorHandler;
expect(caughtErrors).toEqual([
expect.objectContaining(
message: expect.stringContaining('is not a function')
)
])
it 'renders a simple stateless component with prop', ->
class Foo extends React.Component
render: ->
React.createElement(InnerComponent,
name: @props.bar
)
test React.createElement(Foo, bar: 'foo'), 'DIV', 'foo'
test React.createElement(Foo, bar: 'bar'), 'DIV', 'bar'
it 'renders based on state using initial values in this.props', ->
class Foo extends React.Component
constructor: (props) ->
super props
@state = bar: @props.initialValue
render: ->
React.createElement('span',
className: @state.bar
)
test React.createElement(Foo, initialValue: 'foo'), 'SPAN', 'foo'
it 'renders based on state using props in the constructor', ->
class Foo extends React.Component
constructor: (props) ->
@state = bar: props.initialValue
changeState: ->
@setState bar: 'bar'
render: ->
if @state.bar is 'foo'
return React.createElement('div',
className: 'foo'
)
React.createElement('span',
className: @state.bar
)
ref = React.createRef()
test React.createElement(Foo, initialValue: 'foo', ref: ref), 'DIV', 'foo'
ReactDOM.flushSync ->
ref.current.changeState()
test React.createElement(Foo), 'SPAN', 'bar'
it 'sets initial state with value returned by static getDerivedStateFromProps', ->
class Foo extends React.Component
constructor: (props) ->
super props
@state = foo: null
render: ->
React.createElement('div',
className: "#{@state.foo} #{@state.bar}"
)
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
{
foo: nextProps.foo
bar: 'bar'
}
test React.createElement(Foo, foo: 'foo'), 'DIV', 'foo bar'
it 'warns if getDerivedStateFromProps is not static', ->
class Foo extends React.Component
render: ->
React.createElement('div')
getDerivedStateFromProps: ->
{}
ReactDOM.flushSync ->
root.render React.createElement(Foo, foo: 'foo')
assertConsoleErrorDev [
'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.\n' +
' in Foo (at **)']
it 'warns if getDerivedStateFromError is not static', ->
class Foo extends React.Component
render: ->
React.createElement('div')
getDerivedStateFromError: ->
{}
ReactDOM.flushSync ->
root.render React.createElement(Foo, foo: 'foo')
assertConsoleErrorDev [
'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.\n' +
' in Foo (at **)'
]
it 'warns if getSnapshotBeforeUpdate is static', ->
class Foo extends React.Component
render: ->
React.createElement('div')
Foo.getSnapshotBeforeUpdate = () ->
{}
ReactDOM.flushSync ->
root.render React.createElement(Foo, foo: 'foo')
assertConsoleErrorDev [
'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.\n' +
' in Foo (at **)'
]
it 'warns if state not initialized before static getDerivedStateFromProps', ->
class Foo extends React.Component
render: ->
React.createElement('div',
className: "#{@state.foo} #{@state.bar}"
)
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
{
foo: nextProps.foo
bar: 'bar'
}
ReactDOM.flushSync ->
root.render React.createElement(Foo, foo: 'foo')
assertConsoleErrorDev [
'`Foo` uses `getDerivedStateFromProps` but its initial state is
undefined. This is not recommended. Instead, define the initial state by
assigning an object to `this.state` in the constructor of `Foo`.
This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' +
' in Foo (at **)'
]
it 'updates initial state with values returned by static getDerivedStateFromProps', ->
class Foo extends React.Component
constructor: (props, context) ->
super props, context
@state =
foo: 'foo'
bar: 'bar'
render: ->
React.createElement('div',
className: "#{@state.foo} #{@state.bar}"
)
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
{
foo: "not-#{prevState.foo}"
}
test React.createElement(Foo), 'DIV', 'not-foo bar'
it 'renders updated state with values returned by static getDerivedStateFromProps', ->
class Foo extends React.Component
constructor: (props, context) ->
super props, context
@state =
value: 'initial'
render: ->
React.createElement('div',
className: @state.value
)
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
if nextProps.update
return {
value: 'updated'
}
return null
test React.createElement(Foo, update: false), 'DIV', 'initial'
test React.createElement(Foo, update: true), 'DIV', 'updated'
if !featureFlags.disableLegacyContext
it 'renders based on context in the constructor', ->
class Foo extends React.Component
@contextTypes:
tag: PropTypes.string
className: PropTypes.string
constructor: (props, context) ->
super props, context
@state =
tag: context.tag
className: @context.className
render: ->
Tag = @state.tag
React.createElement Tag,
className: @state.className
class Outer extends React.Component
@childContextTypes:
tag: PropTypes.string
className: PropTypes.string
getChildContext: ->
tag: 'span'
className: 'foo'
render: ->
React.createElement Foo
test React.createElement(Outer), 'SPAN', 'foo'
assertConsoleErrorDev([
'Outer uses the legacy childContextTypes API which will soon be removed.
Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Outer (at **)',
'Foo uses the legacy contextTypes API which will soon be removed.
Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Outer (at **)',
]);
it 'renders only once when setting state in componentWillMount', ->
renderCount = 0
class Foo extends React.Component
constructor: (props) ->
@state = bar: props.initialValue
UNSAFE_componentWillMount: ->
@setState bar: 'bar'
render: ->
renderCount++
React.createElement('span', className: @state.bar)
test React.createElement(Foo, initialValue: 'foo'), 'SPAN', 'bar'
expect(renderCount).toBe(1)
it 'should warn with non-object in the initial state property', ->
[['an array'], 'a string', 1234].forEach (state) ->
class Foo extends React.Component
constructor: ->
@state = state
render: ->
React.createElement('span')
test React.createElement(Foo), 'SPAN', ''
assertConsoleErrorDev [
'Foo.state: must be set to an object or null\n' +
' in Foo (at **)'
]
it 'should render with null in the initial state property', ->
class Foo extends React.Component
constructor: ->
@state = null
render: ->
React.createElement('span')
test React.createElement(Foo), 'SPAN', ''
it 'setState through an event handler', ->
class Foo extends React.Component
constructor: (props) ->
@state = bar: props.initialValue
handleClick: =>
@setState bar: 'bar'
render: ->
React.createElement(InnerComponent,
name: @state.bar
onClick: @handleClick
)
test React.createElement(Foo, initialValue: 'foo'), 'DIV', 'foo'
ReactDOM.flushSync ->
attachedListener()
expect(renderedName).toBe 'bar'
it 'should not implicitly bind event handlers', ->
class Foo extends React.Component
constructor: (props) ->
@state = bar: props.initialValue
handleClick: ->
@setState bar: 'bar'
render: ->
React.createElement(InnerComponent,
name: @state.bar
onClick: @handleClick
)
test React.createElement(Foo, initialValue: 'foo'), 'DIV', 'foo'
expect(attachedListener).toThrow()
it 'renders using forceUpdate even when there is no state', ->
class Foo extends React.Component
constructor: (props) ->
@mutativeValue = props.initialValue
handleClick: =>
@mutativeValue = 'bar'
@forceUpdate()
render: ->
React.createElement(InnerComponent,
name: @mutativeValue
onClick: @handleClick
)
test React.createElement(Foo, initialValue: 'foo'), 'DIV', 'foo'
ReactDOM.flushSync ->
attachedListener()
expect(renderedName).toBe 'bar'
it 'will call all the normal life cycle methods', ->
lifeCycles = []
class Foo extends React.Component
constructor: ->
@state = {}
UNSAFE_componentWillMount: ->
lifeCycles.push 'will-mount'
componentDidMount: ->
lifeCycles.push 'did-mount'
UNSAFE_componentWillReceiveProps: (nextProps) ->
lifeCycles.push 'receive-props', nextProps
shouldComponentUpdate: (nextProps, nextState) ->
lifeCycles.push 'should-update', nextProps, nextState
true
UNSAFE_componentWillUpdate: (nextProps, nextState) ->
lifeCycles.push 'will-update', nextProps, nextState
componentDidUpdate: (prevProps, prevState) ->
lifeCycles.push 'did-update', prevProps, prevState
componentWillUnmount: ->
lifeCycles.push 'will-unmount'
render: ->
React.createElement('span',
className: @props.value
)
test React.createElement(Foo, value: 'foo'), 'SPAN', 'foo'
expect(lifeCycles).toEqual [
'will-mount'
'did-mount'
]
lifeCycles = []
test React.createElement(Foo, value: 'bar'), 'SPAN', 'bar'
expect(lifeCycles).toEqual [
'receive-props', { value: 'bar' }
'should-update', { value: 'bar' }, {}
'will-update', { value: 'bar' }, {}
'did-update', { value: 'foo' }, {}
]
lifeCycles = []
ReactDOM.flushSync ->
root.unmount()
expect(lifeCycles).toEqual ['will-unmount']
if !featureFlags.disableLegacyContext
it 'warns when classic properties are defined on the instance,
but does not invoke them.', ->
getInitialStateWasCalled = false
getDefaultPropsWasCalled = false
class Foo extends React.Component
constructor: ->
@contextTypes = {}
@contextType = {}
getInitialState: ->
getInitialStateWasCalled = true
{}
getDefaultProps: ->
getDefaultPropsWasCalled = true
{}
render: ->
React.createElement('span',
className: 'foo'
)
test React.createElement(Foo), 'SPAN', 'foo'
assertConsoleErrorDev [
'getInitialState was defined on Foo, a plain JavaScript class.
This is only supported for classes created using React.createClass.
Did you mean to define a state property instead?\n' +
' in Foo (at **)',
'getDefaultProps was defined on Foo, a plain JavaScript class.
This is only supported for classes created using React.createClass.
Use a static property to define defaultProps instead.\n' +
' in Foo (at **)',
'contextType was defined as an instance property on Foo. Use a static property to define contextType instead.\n' +
' in Foo (at **)',
'contextTypes was defined as an instance property on Foo. Use a static property to define contextTypes instead.\n' +
' in Foo (at **)',
]
expect(getInitialStateWasCalled).toBe false
expect(getDefaultPropsWasCalled).toBe false
it 'does not warn about getInitialState() on class components
if state is also defined.', ->
class Foo extends React.Component
constructor: (props) ->
super props
@state = bar: @props.initialValue
getInitialState: ->
{}
render: ->
React.createElement('span',
className: 'foo'
)
test React.createElement(Foo), 'SPAN', 'foo'
it 'should warn when misspelling shouldComponentUpdate', ->
class NamedComponent extends React.Component
componentShouldUpdate: ->
false
render: ->
React.createElement('span',
className: 'foo'
)
test React.createElement(NamedComponent), 'SPAN', 'foo'
assertConsoleErrorDev [
'NamedComponent has a method called componentShouldUpdate().
Did you mean shouldComponentUpdate()? The name is phrased as a
question because the function is expected to return a value.\n' +
' in NamedComponent (at **)'
]
it 'should warn when misspelling componentWillReceiveProps', ->
class NamedComponent extends React.Component
componentWillRecieveProps: ->
false
render: ->
React.createElement('span',
className: 'foo'
)
test React.createElement(NamedComponent), 'SPAN', 'foo'
assertConsoleErrorDev [
'NamedComponent has a method called componentWillRecieveProps().
Did you mean componentWillReceiveProps()?\n' +
' in NamedComponent (at **)'
]
it 'should warn when misspelling UNSAFE_componentWillReceiveProps', ->
class NamedComponent extends React.Component
UNSAFE_componentWillRecieveProps: ->
false
render: ->
React.createElement('span',
className: 'foo'
)
test React.createElement(NamedComponent), 'SPAN', 'foo'
assertConsoleErrorDev [
'NamedComponent has a method called UNSAFE_componentWillRecieveProps().
Did you mean UNSAFE_componentWillReceiveProps()?\n' +
' in NamedComponent (at **)'
]
it 'should throw AND warn when trying to access classic APIs', ->
ref = React.createRef()
test React.createElement(InnerComponent, name: 'foo', ref: ref), 'DIV', 'foo'
expect(-> ref.current.replaceState {}).toThrow()
assertConsoleWarnDev([
'replaceState(...) is deprecated in plain JavaScript React classes.
Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).'
], {withoutStack: true})
expect(-> ref.current.isMounted()).toThrow()
assertConsoleWarnDev([
'isMounted(...) is deprecated in plain JavaScript React classes.
Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks.',
], {withoutStack: true})
if !featureFlags.disableLegacyContext
it 'supports this.context passed via getChildContext', ->
class Bar extends React.Component
@contextTypes:
bar: PropTypes.string
render: ->
React.createElement('div', className: @context.bar)
class Foo extends React.Component
@childContextTypes:
bar: PropTypes.string
getChildContext: ->
bar: 'bar-through-context'
render: ->
React.createElement Bar
test React.createElement(Foo), 'DIV', 'bar-through-context'
assertConsoleErrorDev [
'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.
(https://react.dev/link/legacy-context)\n' +
' in Foo (at **)',
'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.
(https://react.dev/link/legacy-context)\n' +
' in Foo (at **)'
]
undefined