--[[*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
]]
-- ROBLOX FIXME Luau: this doesn't play nicely with Object.assign
--!strict
-- ROBLOX deviation: prompted by selene to add a type
local Packages = script.Parent.Parent.Parent
local LuauPolyfill = require(Packages.LuauPolyfill)
local Object = LuauPolyfill.Object
type ComparisonTestsProps = {
a: number?,
b: number?,
c: number?,
}
local JestGlobals = require(Packages.Dev.JestGlobals)
local jestExpect = JestGlobals.expect
local jest = JestGlobals.jest
local beforeEach = JestGlobals.beforeEach
local describe = JestGlobals.describe
local it = JestGlobals.it
local xit = JestGlobals.xit
-- local PropTypes
local React
local ReactNoop
local Scheduler
describe("forwardRef", function()
beforeEach(function()
jest.resetModules()
-- PropTypes = require('prop-types')
React = require(Packages.React)
ReactNoop = require(Packages.Dev.ReactNoopRenderer)
Scheduler = require(Packages.Dev.Scheduler)
end)
it("should update refs when switching between children", function()
local function FunctionComponent(props)
local forwardedRef = props.forwardedRef
local setRefOnDiv = props.setRefOnDiv
-- deviation: clearer to express this way, since we don't have real ternaries
local firstRef, secondRef
if setRefOnDiv then
firstRef = forwardedRef
else
secondRef = forwardedRef
end
return React.createElement(
"section",
nil,
React.createElement(
"div",
{ ref = firstRef },
React.createElement("TextLabel", { Text = "First" })
),
React.createElement(
"span",
{ ref = secondRef },
React.createElement("TextLabel", { Text = "Second" })
)
)
end
local RefForwardingComponent = React.forwardRef(function(props, ref)
return React.createElement(
FunctionComponent,
Object.assign({}, props, { forwardedRef = ref })
)
end)
local ref = React.createRef()
ReactNoop.render(
React.createElement(RefForwardingComponent, { ref = ref, setRefOnDiv = true })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(ref.current.type).toBe("div")
ReactNoop.render(
React.createElement(
RefForwardingComponent,
{ ref = ref, setRefOnDiv = false }
)
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(ref.current.type).toBe("span")
end)
it("should support rendering nil", function()
local RefForwardingComponent = React.forwardRef(function(props, ref)
return nil
end)
local ref = React.createRef()
ReactNoop.render(React.createElement(RefForwardingComponent, { ref = ref }))
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(ref.current).toBe(nil)
end)
it("should support rendering nil for multiple children", function()
local RefForwardingComponent = React.forwardRef(function(props, ref)
return nil
end)
local ref = React.createRef()
ReactNoop.render(
React.createElement(
"div",
nil,
React.createElement("div"),
React.createElement(RefForwardingComponent, { ref = ref }),
React.createElement("div")
)
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(ref.current).toBe(nil)
end)
-- ROBLOX TODO: PropTypes not yet supported
xit("should support propTypes and defaultProps", function()
-- local function FunctionComponent(props)
-- local forwardedRef = props.forwardedRef
-- local optional = props.optional
-- local required = props.required
-- return React.createElement("div", {ref=forwardedRef}, {
-- optional,
-- required,
-- })
-- end
-- local RefForwardingComponent = React.forwardRef(function NamedFunction(
-- props,
-- ref,
-- )
-- return
-- })
-- RefForwardingComponent.propTypes = {
-- optional: PropTypes.string,
-- required: PropTypes.string.isRequired,
-- end
-- RefForwardingComponent.defaultProps = {
-- optional: 'default',
-- end
-- local ref = React.createRef()
-- ReactNoop.render(
-- ,
-- )
-- jestExpect(Scheduler).toFlushWithoutYielding()
-- jestExpect(ref.current.children).toEqual([
-- {text: 'foo', hidden: false},
-- {text: 'bar', hidden: false},
-- ])
-- ReactNoop.render()
-- jestExpect(Scheduler).toFlushWithoutYielding()
-- jestExpect(ref.current.children).toEqual([
-- {text: 'default', hidden: false},
-- {text: 'foo', hidden: false},
-- ])
-- jestExpect(() =>
-- ReactNoop.render(),
-- ).toErrorDev(
-- 'Warning: Failed prop type: The prop `required` is marked as required in ' +
-- '`ForwardRef(NamedFunction)`, but its value is `undefined`.\n' +
-- ' in NamedFunction (at **)',
-- )
end)
it("should warn if not provided a callback during creation", function()
-- deviation: no `undefined` in Lua
-- jestExpect(function()
-- React.forwardRef(nil)
-- end).toErrorDev(
-- "forwardRef requires a render function but was given undefined.",
-- {withoutStack: true},
-- )
jestExpect(function()
-- ROBLOX deviation: requires casting away type safety to run and see the warning
(React.forwardRef :: any)(nil)
end).toErrorDev(
"forwardRef requires a render function but was given nil.",
{ withoutStack = true }
)
jestExpect(function()
-- ROBLOX deviation: requires casting away type safety to run and see the warning
(React.forwardRef :: any)("foo")
end).toErrorDev(
"forwardRef requires a render function but was given string.",
{ withoutStack = true }
)
end)
it("should warn if no render function is provided", function()
jestExpect(React.forwardRef).toErrorDev(
"forwardRef requires a render function but was given nil.",
{
withoutStack = true,
}
)
end)
-- ROBLOX deviation: Functions can't have attributes; this test isn't needed
xit(
"should warn if the render function provided has propTypes or defaultProps attributes",
function()
-- function renderWithPropTypes(props, ref)
-- return nil
-- end
-- renderWithPropTypes.propTypes = {}
-- function renderWithDefaultProps(props, ref)
-- return nil
-- end
-- renderWithDefaultProps.defaultProps = {}
-- jestExpect(function()
-- React.forwardRef(renderWithPropTypes)
-- ).toErrorDev(
-- "forwardRef render functions do not support propTypes or defaultProps. " ..
-- "Did you accidentally pass a React component?",
-- {withoutStack: true}
-- )
-- jestExpect(function()
-- React.forwardRef(renderWithDefaultProps)
-- ).toErrorDev(
-- "forwardRef render functions do not support propTypes or defaultProps. " ..
-- "Did you accidentally pass a React component?",
-- {withoutStack: true}
-- )
end
)
it(
"should not warn if the render function provided does not use any parameter",
function()
-- ROBLOX deviation: remove the function name, and don't try to access arguments that don't exist
React.forwardRef(function()
return React.createElement("div", { ref = "" })
end)
end
)
it(
"should warn if the render function provided does not use the forwarded ref parameter",
function()
local function arityOfOne(props)
return React.createElement("div", props)
end
jestExpect(function()
React.forwardRef(arityOfOne)
end).toErrorDev(
"forwardRef render functions accept exactly two parameters: props and ref. "
.. "Did you forget to use the ref parameter?",
{ withoutStack = true }
)
end
)
it(
"should not warn if the render function provided use exactly two parameters",
function()
local function arityOfTwo(_props, ref)
React.createElement("div", { ref = ref })
end
React.forwardRef(arityOfTwo)
end
)
it(
"should warn if the render function provided expects to use more than two parameters",
function()
local function arityOfThree(_props, ref, _x)
return React.createElement("div", { ref = ref })
end
jestExpect(function()
-- ROBLOX deviation: requires casting away type safety to run and see the warning
(React.forwardRef :: any)(arityOfThree)
end).toErrorDev(
"forwardRef render functions accept exactly two parameters: props and ref. "
.. "Any additional parameter will be undefined.",
{ withoutStack = true }
)
end
)
-- ROBLOX TODO: Support PropTypes
xit(
"should honor a displayName if set on the forwardRef wrapper in warnings",
function()
-- local Component = function(props)
-- return React.createElement("div", props)
-- end
-- local RefForwardingComponent = React.forwardRef(function(props, ref)
-- return React.createElement(Component, Object.assign({}, props, {forwardedRef=ref}))
-- end)
-- RefForwardingComponent.displayName = 'Foo'
-- RefForwardingComponent.propTypes = {
-- optional: PropTypes.string,
-- required: PropTypes.string.isRequired,
-- end
-- RefForwardingComponent.defaultProps = {
-- optional: 'default',
-- end
-- local ref = React.createRef()
-- jestExpect(() =>
-- ReactNoop.render(),
-- ).toErrorDev(
-- 'Warning: Failed prop type: The prop `required` is marked as required in ' +
-- '`Foo`, but its value is `undefined`.\n' +
-- ' in Foo (at **)',
-- )
end
)
-- ROBLOX deviation: Cannot add displayName attribute to function
xit("should honor a displayName in stacks if set on the inner function", function()
-- local Component = props =>
-- local inner = (props, ref) =>
-- inner.displayName = 'Foo'
-- local RefForwardingComponent = React.forwardRef(inner)
-- RefForwardingComponent.propTypes = {
-- optional: PropTypes.string,
-- required: PropTypes.string.isRequired,
-- end
-- RefForwardingComponent.defaultProps = {
-- optional: 'default',
-- end
-- local ref = React.createRef()
-- jestExpect(() =>
-- ReactNoop.render(),
-- ).toErrorDev(
-- 'Warning: Failed prop type: The prop `required` is marked as required in ' +
-- '`ForwardRef(Foo)`, but its value is `undefined`.\n' +
-- ' in Foo (at **)',
-- )
end)
it("should not bailout if forwardRef is not wrapped in memo", function()
local Component = function(props)
return React.createElement("div", props)
end
local renderCount = 0
local RefForwardingComponent = React.forwardRef(function(props, ref)
renderCount += 1
return React.createElement(
Component,
Object.assign({}, props, { forwardedRef = ref })
)
end)
local ref = React.createRef()
ReactNoop.render(
React.createElement(RefForwardingComponent, { ref = ref, optional = "foo" })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 2 or 1)
ReactNoop.render(
React.createElement(RefForwardingComponent, { ref = ref, optional = "foo" })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 4 or 2)
end)
it("should bailout if forwardRef is wrapped in memo", function()
local Component = function(props)
return React.createElement("div", { ref = props.forwardedRef })
end
local renderCount = 0
local RefForwardingComponent = React.memo(React.forwardRef(function(props, ref)
renderCount += 1
return React.createElement(
Component,
Object.assign({}, props, { forwardedRef = ref })
)
end))
local ref = React.createRef()
ReactNoop.render(
React.createElement(RefForwardingComponent, { ref = ref, optional = "foo" })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 2 or 1)
jestExpect(ref.current.type).toBe("div")
ReactNoop.render(
React.createElement(RefForwardingComponent, { ref = ref, optional = "foo" })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 2 or 1)
local differentRef = React.createRef()
ReactNoop.render(
React.createElement(
RefForwardingComponent,
{ ref = differentRef, optional = "foo" }
)
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 4 or 2)
jestExpect(ref.current).toBe(nil)
jestExpect(differentRef.current.type).toBe("div")
ReactNoop.render(
React.createElement(RefForwardingComponent, { ref = ref, optional = "bar" })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 6 or 3)
end)
it("should custom memo comparisons to compose", function()
local Component = function(props)
return React.createElement("div", { ref = props.forwardedRef })
end
local renderCount = 0
local RefForwardingComponent = React.memo(
React.forwardRef(function(props: ComparisonTestsProps, ref)
renderCount += 1
return React.createElement(
Component,
Object.assign({}, props, { forwardedRef = ref })
)
end),
-- ROBLOX FIXME Luau: needs normalization:
function(o: ComparisonTestsProps, p: ComparisonTestsProps)
return o.a == p.a and o.b == p.b
end :: any
)
local ref = React.createRef()
ReactNoop.render(
React.createElement(
RefForwardingComponent,
{ ref = ref, a = 0, b = 0, c = 1 }
)
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 2 or 1)
jestExpect(ref.current.type).toBe("div")
-- Changing either a or b rerenders
ReactNoop.render(
React.createElement(
RefForwardingComponent,
{ ref = ref, a = 0, b = 1, c = 1 }
)
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 4 or 2)
-- Changing c doesn't rerender
ReactNoop.render(
React.createElement(
RefForwardingComponent,
{ ref = ref, a = 0, b = 1, c = 2 }
)
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 4 or 2)
local ComposedMemo = React.memo(
RefForwardingComponent,
function(o: ComparisonTestsProps, p: ComparisonTestsProps)
return o.a == p.a and o.c == p.c
end
)
ReactNoop.render(
React.createElement(ComposedMemo, { ref = ref, a = 0, b = 0, c = 0 })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 6 or 3)
-- Changing just b no longer updates
ReactNoop.render(
React.createElement(ComposedMemo, { ref = ref, a = 0, b = 1, c = 0 })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 6 or 3)
-- Changing just a and c updates
ReactNoop.render(
React.createElement(ComposedMemo, { ref = ref, a = 2, b = 2, c = 2 })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 8 or 4)
-- Changing just c does not update
ReactNoop.render(
React.createElement(ComposedMemo, { ref = ref, a = 2, b = 2, c = 3 })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 8 or 4)
-- Changing ref still rerenders
local differentRef = React.createRef()
ReactNoop.render(
React.createElement(ComposedMemo, { ref = differentRef, a = 2, b = 2, c = 3 })
)
jestExpect(Scheduler).toFlushWithoutYielding()
jestExpect(renderCount).toBe(_G.__DEV__ and 10 or 5)
jestExpect(ref.current).toBe(nil)
jestExpect(differentRef.current.type).toBe("div")
end)
it("warns on forwardRef(memo(...))", function()
jestExpect(function()
-- ROBLOX deviation: this vioaltes strong types, so we have to cast away safety to test runtime check
(React.forwardRef :: any)(React.memo(function(props, ref)
return nil
end))
end).toErrorDev({
"Warning: forwardRef requires a render function but received a `memo` "
.. "component. Instead of forwardRef(memo(...)), use "
.. "memo(forwardRef(...)).",
}, {
withoutStack = true,
})
end)
-- ROBLOX deviation: Regression test for bad metatable logic
it("allows new fields to be assigned on it", function()
local MyComponent = React.forwardRef(function(_props, _ref)
return nil
end)
MyComponent.SomeEnum = { ValueA = 1, ValueB = 2 }
jestExpect(MyComponent.SomeEnum).never.toBeNil()
jestExpect(MyComponent.SomeEnum.ValueA).toBe(1)
jestExpect(MyComponent.SomeEnum.ValueB).toBe(2)
end)
end)