--[[* * 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 ]] local Packages = script.Parent.Parent.Parent local JestGlobals = require(Packages.Dev.JestGlobals) local jestExpect = JestGlobals.expect local jest = JestGlobals.jest local describe = JestGlobals.describe local beforeEach = JestGlobals.beforeEach local it = JestGlobals.it local xit = JestGlobals.xit local React local ReactRoblox local ReactFeatureFlags -- ROBLOX deviation: the tests using these are currently SKIPped local PropTypes = nil -- ROBLOX deviation: This function is a misnomer even in upstream; here, we -- just render it into an orphaned root local ReactTestUtils = { renderIntoDocument = function(element) local instance = Instance.new("Folder") local root = ReactRoblox.createLegacyRoot(instance) root:render(element) return root end, } describe("ReactElementValidator", function() local ComponentClass beforeEach(function() jest.resetModules() -- PropTypes = require("prop-types") ReactFeatureFlags = require(Packages.Shared).ReactFeatureFlags ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false React = require(script.Parent.Parent) ReactRoblox = require(Packages.Dev.ReactRoblox) -- ReactTestUtils = require("react-dom/test-utils") ComponentClass = React.Component:extend("ComponentClass") function ComponentClass:render() return React.createElement("Frame") end end) it("warns for keys for arrays of elements in rest args", function() jestExpect(function() React.createElement(ComponentClass, nil, { React.createElement(ComponentClass), React.createElement(ComponentClass), }) end).toErrorDev('Each child in a list should have a unique "key" prop.') end) it("warns for keys for arrays of elements with owner info", function() local InnerClass = React.Component:extend("InnerClass") function InnerClass:render() return React.createElement(ComponentClass, nil, self.props.childSet) end local ComponentWrapper = React.Component:extend("ComponentWrapper") function ComponentWrapper:render() return React.createElement(InnerClass, { childSet = { React.createElement(ComponentClass), React.createElement(ComponentClass), }, }) end jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement(ComponentWrapper)) end).toErrorDev( 'Each child in a list should have a unique "key" prop.' .. "\n\nCheck the render method of `InnerClass`. " .. "It was passed a child from ComponentWrapper. " ) end) it("warns for keys for arrays with no owner or parent info", function() -- ROBLOX deviation: we can't nil out the function's name, so use a real anonymous function -- local function Anonymous() -- return React.createElement("Frame") -- end -- Object.defineProperty(Anonymous, "name", {value = nil}) local divs = { React.createElement("Frame"), React.createElement("Frame"), } jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement(function() return React.createElement("Frame") end, nil, divs)) end).toErrorDev( "Warning: Each child in a list should have a unique " .. '"key" prop. See https://reactjs.org/link/warning-keys for more information.\n' .. " in Frame (at **)" ) end) it("warns for keys for arrays of elements with no owner info", function() local divs = { React.createElement("Frame"), React.createElement("Frame"), } jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement("Frame", nil, divs)) end).toErrorDev( "Warning: Each child in a list should have a unique " .. '"key" prop.\n\nCheck the top-level render call using . See ' .. "https://reactjs.org/link/warning-keys for more information.\n" .. " in Frame (at **)" ) end) it("warns for keys with component stack info", function() local function Component() return React.createElement("Frame", nil, { React.createElement("Frame"), React.createElement("Frame"), }) end local function Parent(props) return React.cloneElement(props.child) end local function GrandParent() return React.createElement(Parent, { child = React.createElement(Component), }) end jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement(GrandParent)) end).toErrorDev( "Warning: Each child in a list should have a unique " .. '"key" prop.\n\nCheck the render method of `Component`. See ' .. "https://reactjs.org/link/warning-keys for more information.\n" .. " in Frame (at **)\n" .. " in Component (at **)\n" .. " in Parent (at **)\n" .. " in GrandParent (at **)" ) end) it("does not warn for keys when passing children down", function() local function Wrapper(props) return React.createElement( "Frame", nil, props.children, React.createElement("Frame") ) end -- ROBLOX deviation: Add expectation to make sure we get _no_ errors jestExpect(function() ReactTestUtils.renderIntoDocument( React.createElement( Wrapper, nil, React.createElement("Frame"), React.createElement("Frame") ) ) end).toErrorDev({}) end) -- ROBLOX deviation: This test is unique to roblox; we allow children to -- be passed as a table, and use the keys as stable keys for the -- equivalent children it("does not warn for keys when providing keys via children tables", function() -- ROBLOX FIXME: Expect coercion jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement("Frame", nil, { ChildA = React.createElement("Frame"), ChildB = React.createElement("Frame"), })) end).toErrorDev({}) end) -- ROBLOX deviation: no @@iterator in Lua xit("warns for keys for iterables of elements in rest args", function() local iterable = { ["@@iterator"] = function() local i = 0 return { next = function() i = i + 1 local done = i > 2 local value if not done then value = React.createElement(ComponentClass) end return { value = value, done = done, } end, } end, } jestExpect(function() return React.createElement(ComponentClass, nil, iterable) end).toErrorDev('Each child in a list should have a unique "key" prop.') end) it("does not warns for arrays of elements with keys", function() React.createElement(ComponentClass, nil, { React.createElement(ComponentClass, { key = "#1" }), React.createElement(ComponentClass, { key = "#2" }), }) end) -- ROBLOX deviation: no @@iterator in Lua xit("does not warns for iterable elements with keys", function() local iterable = { ["@@iterator"] = function() local i = 0 return { next = function() i = i + 1 local done = i > 2 return { value = not done and React.createElement( ComponentClass, { key = "#" .. i } ) or nil, done = done, } end, } end, } React.createElement(ComponentClass, nil, iterable) end) it("does not warn when the element is directly in rest args", function() React.createElement( ComponentClass, nil, React.createElement(ComponentClass), React.createElement(ComponentClass) ) end) it("does not warn when the array contains a non-element", function() React.createElement(ComponentClass, nil, { {}, {} }) end) -- ROBLOX TODO: implement PropTypes support xit("should give context for PropType errors in nested components.", function() -- // In this test, we're making sure that if a proptype error is found in a -- // component, we give a small hint as to which parent instantiated that -- // component as per warnings about key usage in ReactElementValidator. local MyComp = React.Component:extend("MyComp") function MyComp:render() return React.createElement("Frame", nil, "My color is " .. self.props.color) end MyComp.propTypes = { color = PropTypes.string, } local function ParentComp() return React.createElement(MyComp, { color = 123 }) end jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement(ParentComp)) end).toErrorDev( "Warning: Failed prop type: " .. "Invalid prop `color` of type `number` supplied to `MyComp`, " .. "expected `string`.\n" .. " in MyComp (at **)\n" .. " in ParentComp (at **)" ) end) it("gives a helpful error when passing invalid types", function() local function Foo() end jestExpect(function() React.createElement(nil) React.createElement(true) React.createElement({ x = 17 }) React.createElement({}) React.createElement(React.createElement("Frame")) React.createElement(React.createElement(Foo)) React.createElement(React.createElement(React.createContext().Consumer)) React.createElement({ ["$$typeof"] = "non-react-thing" }) end).toErrorDev({ "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: nil.", "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: boolean.", "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: table.", "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: array. You likely forgot to export your " .. "component from the file it's defined in, or you might have mixed up " .. "default and named imports.", "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: . Did you accidentally export a JSX literal " .. "or Element instead of a component?", "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: . Did you accidentally export a JSX literal " .. "or Element instead of a component?", "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: . Did you accidentally " .. "export a JSX literal or Element instead of a component?", "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: table.\n{", }, { withoutStack = true }) -- // Should not log any additional warnings React.createElement("Frame") end) it( "includes the owner name when passing null, undefined, boolean, or number", function() local function ParentComp() -- ROBLOX DEVIATION: The test says "null, undefined, boolean, or -- number", but uses `null`, which it treats differently from -- `undefined`. Here, we're passing a number, which should have -- behavior identical to upstream return React.createElement(1) end jestExpect(function() jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement(ParentComp)) end).toThrowError( "Element type is invalid: expected a string (for built-in components) " .. "or a class/function (for composite components) but got: number." .. ( _G.__DEV__ and "\n\nCheck the render method of `ParentComp`." or "" ) ) end).toErrorDev( "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: number." -- ROBLOX FIXME: Error output differs -- "\n\nCheck the render method of `ParentComp`.\n in ParentComp", ) end ) -- ROBLOX deviation: Regression test for error output issue it("includes the owner name of a PureComponent", function() local ParentPureComp = React.PureComponent:extend("ParentPureComp") function ParentPureComp:render() return React.createElement(1) end jestExpect(function() jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement(ParentPureComp)) end).toThrowError( "Element type is invalid: expected a string (for built-in components) " .. "or a class/function (for composite components) but got: number." .. ( _G.__DEV__ and "\n\nCheck the render method of `ParentPureComp`." or "" ) ) end).toErrorDev( "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: number." -- ROBLOX FIXME: Error output differs -- "\n\nCheck the render method of `ParentPureComp`.\n in ParentPureComp" ) end) -- ROBLOX TODO: implement PropTypes it.skip("should check default prop values", function() local Component = React.Component:extend("Component") function Component:render() return React.createElement("Frame", nil, self.props.prop) end Component.propTypes = { prop = PropTypes.string.isRequired, } Component.defaultProps = { prop = nil } jestExpect(function() return ReactTestUtils.renderIntoDocument(React.createElement(Component)) end).toErrorDev( "Warning: Failed prop type: The prop `prop` is marked as required in " .. "`Component`, but its value is `null`.\n" .. " in Component" ) end) -- ROBLOX TODO: implement PropTypes it.skip("should not check the default for explicit null", function() local Component = React.Component:extend("Component") function Component:render() return React.createElement("Frame", nil, self.props.prop) end Component.propTypes = { prop = PropTypes.string.isRequired, } Component.defaultProps = { prop = "text", } jestExpect(function() ReactTestUtils.renderIntoDocument( React.createElement(Component, { prop = nil }) ) end).toErrorDev( "Warning: Failed prop type: The prop `prop` is marked as required in " .. "`Component`, but its value is `null`.\n" .. " in Component" ) end) -- ROBLOX TODO: implement PropTypes it.skip("should check declared prop types", function() local Component = React.Component:extend("Component") function Component:render() return React.createElement("Frame", nil, self.props.prop) end Component.propTypes = { prop = PropTypes.string.isRequired, } jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement(Component)) ReactTestUtils.renderIntoDocument( React.createElement(Component, { prop = 42 }) ) end).toErrorDev({ "Warning: Failed prop type: " .. "The prop `prop` is marked as required in `Component`, but its value " .. "is `undefined`.\n" .. " in Component", "Warning: Failed prop type: " .. "Invalid prop `prop` of type `number` supplied to " .. "`Component`, expected `string`.\n" .. " in Component", }) -- // Should not error for strings ReactTestUtils.renderIntoDocument(React.createElement(Component, { prop = "string", })) end) -- ROBLOX TODO: implement PropTypes it.skip("should warn if a PropType creator is used as a PropType", function() local Component = React.Component:extend("Component") function Component:render() return React.createElement("Frame", nil, self.props.myProp.value) end Component.propTypes = { myProp = PropTypes.shape, } jestExpect(function() ReactTestUtils.renderIntoDocument( React.createElement(Component, { myProp = { value = "hi" } }) ) end).toErrorDev( "Warning: Component: type specification of prop `myProp` is invalid; " .. "the type checker function must return `null` or an `Error` but " .. "returned a function. You may have forgotten to pass an argument to " .. "the type checker creator (arrayOf, instanceOf, objectOf, oneOf, " .. "oneOfType, and shape all require an argument)." ) end) -- ROBLOX TODO: implement PropTypes it.skip("should warn if component declares PropTypes instead of propTypes", function() local MisspelledPropTypesComponent = React.Component:extend("MisspelledPropTypesComponent") function MisspelledPropTypesComponent:render() return React.createElement("Frame", nil, self.props.prop) end MisspelledPropTypesComponent.PropTypes = { prop = PropTypes.string, } jestExpect(function() ReactTestUtils.renderIntoDocument( React.createElement(MisspelledPropTypesComponent, { prop = "Hi" }) ) end).toErrorDev( "Warning: Component MisspelledPropTypesComponent declared `PropTypes` " .. "instead of `propTypes`. Did you misspell the property assignment?", { withoutStack = true } ) end) it("warns for fragments with illegal attributes", function() local Foo = React.Component:extend("Foo") function Foo:render() -- ROBLOX deviation: Use an actual child element instead of a -- text instance, which is unsupported in ReactRoblox return React.createElement( React.Fragment, { a = 1 }, React.createElement("Frame") ) end jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement(Foo)) end).toErrorDev( "Invalid prop `a` supplied to `React.Fragment`. React.Fragment " .. "can only have `key` and `children` props." ) end) if not _G.__EXPERIMENTAL__ then -- ROBLOX deviation: createFactory is deprecated in React so it is removed in -- the Lua version it.skip("should warn when accessing .type on an element factory", function() local function TestComponent() return React.createElement("Frame") end local TestFactory jestExpect(function() TestFactory = React.createFactory(TestComponent) end).toWarnDev( "Warning: React.createFactory() is deprecated and will be removed in a " .. "future major release. Consider using JSX or use React.createElement() " .. "directly instead.", { withoutStack = true } ) jestExpect(function() return TestFactory.type end).toWarnDev( "Warning: Factory.type is deprecated. Access the class directly before " .. "passing it to createFactory.", { withoutStack = true } ) -- // Warn once, not again jestExpect(TestFactory.type).toBe(TestComponent) end) end -- ROBLOX deviation: usage of web browser document global it.skip("does not warn when using DOM node as children", function() -- local DOMContainer = React.Component:extend("DOMContainer") -- function DOMContainer:render() -- return React.createElement("Frame") -- end -- function DOMContainer:componentDidMount() -- ReactDOM.findDOMNode(self).appendChild(self.props.children); -- end -- local node = document.createElement("Frame") -- -- // This shouldn't cause a stack overflow or any other problems (#3883) -- ReactTestUtils.renderIntoDocument( -- React.createElement(DOMContainer, nil, node) -- ) end) -- ROBLOX deviation: not applicable in Lua it.skip("should not enumerate enumerable numbers (#4776)", function() -- Number.prototype['@@iterator'] = function() -- error("number iterator called") -- end end) it("does not blow up with inlined children", function() -- // We don't suggest this since it silences all sorts of warnings, but we -- // shouldn't blow up either. local child = { ["$$typeof"] = React.createElement("Frame")["$$typeof"], type = "Frame", key = nil, ref = nil, props = {}, _owner = nil, } React.createElement("Frame", nil, { child }) end) it("does not blow up on key warning with undefined type", function() local Foo = nil jestExpect(function() React.createElement(Foo, { __source = { fileName = "fileName.lua", lineNumber = 100, }, }, { React.createElement("Frame") }) end).toErrorDev( "Warning: React.createElement: type is invalid -- expected a string " .. "(for built-in components) or a class/function (for composite " .. "components) but got: nil. You likely forgot to export your " .. "component from the file it's defined in, or you might have mixed up " .. "default and named imports.\n\nCheck your code at **.", { withoutStack = true } ) end) it("does not call lazy initializers eagerly", function() local didCall = false local Lazy = React.lazy(function() didCall = true return { andThen = function() end } end) React.createElement(Lazy) jestExpect(didCall).toBe(false) end) -- ROBLOX deviation: validate extra warning when using table keys as the -- keys provided to child elements it("warns when keys are provided via both the 'key' prop AND table keys", function() local Component = React.Component:extend("Component") function Component:render() return React.createElement("Frame", nil, { a1 = React.createElement("Frame", { key = "a2" }), b = React.createElement("Frame", { key = "b" }), }) end jestExpect(function() ReactTestUtils.renderIntoDocument(React.createElement(Component)) end).toErrorDev( 'Child element received a "key" prop ("a2") in addition to a key in ' .. 'the "children" table of its parent ("a1"). Please provide only ' .. 'one key definition. When both are present, the "key" prop ' .. "will take precedence.\n\nCheck the render method of `Component`. " .. "See https://reactjs.org/link/warning-keys for more information.\n" .. " in Frame (at **)\n" .. " in Component (at **)" ) end) end)