-- Upstream: https://github.com/facebook/react/blob/16654436039dd8f16a63928e71081c7745872e8f/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js
--  * 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
--  * @jest-environment node
--  */

-- !strict
local Packages = script.Parent.Parent.Parent
local ReactFeatureFlags

local React
local ReactTestRenderer
-- local prettyFormat = require('pretty-format')

-- Isolate noop renderer
-- local ReactNoop = require(Packages.ReactNoopRenderer)
-- local Scheduler = require(Packages.Scheduler)

local LuauPolyfill = require(Packages.LuauPolyfill)
local Array = LuauPolyfill.Array
local Symbol = LuauPolyfill.Symbol

-- Kind of hacky, but we nullify all the instances to test the tree structure
-- with jasmine's deep equality function, and test the instances separate. We
-- also delete children props because testing them is more annoying and not
-- really important to verify.
local function cleanNodeOrArray(node)
	if not node then
		return
	end
	if Array.isArray(node) then
		-- ROBLOX deviation: for loop in place of forEach()
		for _, v in node do
			cleanNodeOrArray(v)
		end
		return
	end
	if node and node.instance then
		node.instance = nil
	end
	if node and node.props and node.props.children then
		-- eslint-disable-next-line no-unused-vars
		node.props["children"] = nil
	end
	if Array.isArray(node.rendered) then
		-- ROBLOX deviation: for loop in place of forEach()
		for _, v in node.rendered do
			cleanNodeOrArray(v)
		end
	elseif typeof(node.rendered) == "table" then
		cleanNodeOrArray(node.rendered)
	end
end

local JestGlobals = require(Packages.Dev.JestGlobals)
local beforeEach = JestGlobals.beforeEach
local jestExpect = JestGlobals.expect
local describe = JestGlobals.describe
local it = JestGlobals.it
local jest = JestGlobals.jest

describe("ReactTestRenderer", function()
	beforeEach(function()
		jest.resetModules()

		ReactFeatureFlags = require(Packages.Shared).ReactFeatureFlags
		ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false

		React = require(Packages.React)
		ReactTestRenderer = require(Packages.ReactTestRenderer)
		-- local prettyFormat = require('pretty-format')
	end)
	it("renders a simple component", function()
		local function Link()
			return React.createElement("a", {
				role = "link",
			})
		end

		local renderer = ReactTestRenderer.create(React.createElement(Link))

		jestExpect(renderer.toJSON()).toEqual({
			type = "a",
			props = {
				role = "link",
			},
			children = nil,
		})
	end)
	it("renders a top-level empty component", function()
		local function Empty()
			return nil
		end

		local renderer = ReactTestRenderer.create(React.createElement(Empty))

		jestExpect(renderer.toJSON()).toEqual(nil)
	end)
	it("exposes a type flag", function()
		local function Link()
			return React.createElement("a", {
				role = "link",
			})
		end

		local renderer = ReactTestRenderer.create(React.createElement(Link))
		local object = renderer.toJSON()
		-- ROBLOX FIXME: needs to stringify $$typeof because Symbol module is reset. Un-stringify once we've found a solution.
		jestExpect(tostring(object["$$typeof"])).toEqual(
			tostring(Symbol.for_("react.test.json"))
		)

		-- $$typeof should not be enumerable.
		for key, _ in object do
			jestExpect(key).never.toEqual("$$typeof")
		end
	end)
	it("can render a composite component", function()
		local Component = React.Component:extend("Component")

		local Child = function()
			return React.createElement("moo")
		end

		function Component:render()
			return React.createElement("div", {
				className = "purple",
			}, React.createElement(Child, nil))
		end

		local renderer = ReactTestRenderer.create(React.createElement(Component))

		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			props = {
				className = "purple",
			},
			children = {
				{
					type = "moo",
					props = {},
					children = nil,
				},
			},
		})
	end)
	it("renders some basics with an update", function()
		local renders = 0
		local Component = React.Component:extend("Component")

		local Child = function()
			renders = renders + 1

			return React.createElement("moo")
		end
		local Null = function()
			renders = renders + 1
			return nil
		end

		function Component:init()
			self.state = { x = 3 }
			return
		end
		function Component:render()
			renders = renders + 1

			return React.createElement("div", {
				className = "purple",
			}, self.state.x, React.createElement(Child), React.createElement(Null))
		end
		function Component:componentDidMount()
			self:setState({ x = 7 })
		end

		local renderer = ReactTestRenderer.create(React.createElement(Component))

		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			props = {
				className = "purple",
			},
			children = {
				"7",
				{
					type = "moo",
					props = {},
					children = nil,
				},
			},
		})
		jestExpect(renders).toEqual(6)
	end)
	it("exposes the instance", function()
		local Mouse = React.Component:extend("Mouse")

		function Mouse:init()
			self.state = {
				mouse = "mouse",
			}
		end
		function Mouse:handleMoose()
			self:setState({
				mouse = "moose",
			})
		end
		function Mouse:render()
			return React.createElement("div", nil, self.state.mouse)
		end

		local renderer = ReactTestRenderer.create(React.createElement(Mouse))

		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			props = {},
			children = {
				"mouse",
			},
		})

		local mouse = renderer.getInstance()

		mouse:handleMoose()
		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			children = {
				"moose",
			},
			props = {},
		})
	end)
	it("updates types", function()
		local renderer =
			ReactTestRenderer.create(React.createElement("div", nil, "mouse"))

		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			props = {},
			children = {
				"mouse",
			},
		})
		renderer.update(React.createElement("span", nil, "mice"))
		jestExpect(renderer.toJSON()).toEqual({
			type = "span",
			props = {},
			children = {
				"mice",
			},
		})
	end)
	it("updates children", function()
		local renderer = ReactTestRenderer.create(
			React.createElement(
				"div",
				nil,
				React.createElement("span", {
					key = "a",
				}, "A"),
				React.createElement("span", {
					key = "b",
				}, "B"),
				React.createElement("span", {
					key = "c",
				}, "C")
			)
		)

		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			props = {},
			children = {
				{
					type = "span",
					props = {},
					children = {
						"A",
					},
				},
				{
					type = "span",
					props = {},
					children = {
						"B",
					},
				},
				{
					type = "span",
					props = {},
					children = {
						"C",
					},
				},
			},
		})
		renderer.update(React.createElement(
			"div",
			nil,
			React.createElement("span", {
				key = "d",
			}, "D"),
			React.createElement("span", {
				key = "c",
			}, "C"),
			React.createElement("span", {
				key = "b",
			}, "B")
		))
		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			props = {},
			children = {
				{
					type = "span",
					props = {},
					children = {
						"D",
					},
				},
				{
					type = "span",
					props = {},
					children = {
						"C",
					},
				},
				{
					type = "span",
					props = {},
					children = {
						"B",
					},
				},
			},
		})
	end)
	it("does the full lifecycle", function()
		local log = {}
		local Log = React.Component:extend("Log")

		function Log:render()
			table.insert(log, "render " .. self.props.name)
			return React.createElement("div")
		end
		function Log:componentDidMount()
			table.insert(log, "mount " .. self.props.name)
		end
		function Log:componentWillUnmount()
			table.insert(log, "unmount " .. self.props.name)
		end

		local renderer = ReactTestRenderer.create(React.createElement(Log, {
			key = "foo",
			name = "Foo",
		}))

		renderer.update(React.createElement(Log, {
			key = "bar",
			name = "Bar",
		}))
		renderer.unmount()
		jestExpect(log).toEqual({
			"render Foo",
			"mount Foo",
			"render Bar",
			"unmount Foo",
			"mount Bar",
			"unmount Bar",
		})
	end)
	it("gives a ref to native components", function()
		local log = {}

		ReactTestRenderer.create(React.createElement("div", {
			ref = function(r)
				return table.insert(log, r)
			end,
		}))
		jestExpect(log).toEqual({ nil })
	end)
	it("warns correctly for refs on SFCs", function()
		local function Bar()
			return React.createElement("div", nil, "Hello, world")
		end

		local Foo = React.Component:extend("Foo")

		function Foo:render()
			return React.createElement(Bar, {
				ref = React.createRef(),
			})
		end

		local Baz = React.Component:extend("Baz")

		function Baz:render()
			return React.createElement("div", {
				ref = React.createRef(),
			})
		end

		ReactTestRenderer.create(React.createElement(Baz))
		jestExpect(function()
			return ReactTestRenderer.create(React.createElement(Foo))
		end).toErrorDev(
			"Warning: Function components cannot be given refs. Attempts "
				.. "to access this ref will fail. "
				.. "Did you mean to use React.forwardRef()?\n\n"
				.. "Check the render method of `Foo`.\n"
				.. "    in Bar (at **)\n"
				.. "    in Foo (at **)"
		)
	end)
	it("allows an optional createNodeMock function", function()
		local mockDivInstance = {
			appendChild = function() end,
		}
		local mockInputInstance = {
			focus = function() end,
		}
		local mockListItemInstance = {
			click = function() end,
		}
		local mockAnchorInstance = {
			hover = function() end,
		}
		local log = {}

		-- ROBLOX deviation: using createRef in place of string refs
		local bar = React.createRef()
		local Foo = React.Component:extend("Foo")

		function Foo:componentDidMount()
			table.insert(log, bar.current)
		end
		function Foo:render()
			return React.createElement("a", {
				ref = bar,
			}, "Hello, world")
		end

		local function createNodeMock(element)
			if element.type == "div" then
				return mockDivInstance
			elseif element.type == "input" then
				return mockInputInstance
			elseif element.type == "li" then
				return mockListItemInstance
			elseif element.type == "a" then
				return mockAnchorInstance
			else
				return {}
			end
		end

		ReactTestRenderer.create(
			React.createElement("div", {
				ref = function(r)
					return table.insert(log, r)
				end,
			}),
			{ createNodeMock = createNodeMock }
		)
		ReactTestRenderer.create(
			React.createElement("input", {
				ref = function(r)
					return table.insert(log, r)
				end,
			}),
			{ createNodeMock = createNodeMock }
		)
		ReactTestRenderer.create(
			React.createElement(
				"div",
				nil,
				React.createElement(
					"span",
					nil,
					React.createElement(
						"ul",
						nil,
						React.createElement("li", {
							ref = function(r)
								return table.insert(log, r)
							end,
						})
					),
					React.createElement(
						"ul",
						nil,
						React.createElement("li", {
							ref = function(r)
								return table.insert(log, r)
							end,
						}),
						React.createElement("li", {
							ref = function(r)
								return table.insert(log, r)
							end,
						})
					)
				)
			),
			{
				createNodeMock = createNodeMock,
				foobar = true,
			}
		)
		ReactTestRenderer.create(
			React.createElement(Foo),
			{ createNodeMock = createNodeMock }
		)
		ReactTestRenderer.create(React.createElement("div", {
			ref = function(r)
				return table.insert(log, r)
			end,
		}))
		ReactTestRenderer.create(
			React.createElement("div", {
				ref = function(r)
					return table.insert(log, r)
				end,
			}),
			{}
		)
		jestExpect(log).toEqual({
			mockDivInstance,
			mockInputInstance,
			mockListItemInstance,
			mockListItemInstance,
			mockListItemInstance,
			mockAnchorInstance,
		})
	end)
	it("supports unmounting when using refs", function()
		local Foo = React.Component:extend("Foo")

		-- ROBLOX deviation: using createRef in place of string refs
		local foo = React.createRef()

		function Foo:render()
			return React.createElement("div", {
				ref = foo,
			})
		end

		local inst = ReactTestRenderer.create(React.createElement(Foo), {
			createNodeMock = function()
				return foo.current
			end,
		})

		jestExpect(function()
			return inst.unmount()
		end).never.toThrow()
	end)
	it("supports unmounting inner instances", function()
		local count = 0
		local Foo = React.Component:extend("Foo")

		function Foo:componentWillUnmount()
			count = count + 1
		end
		function Foo:render()
			return React.createElement("div")
		end

		local inst = ReactTestRenderer.create(
			React.createElement("div", nil, React.createElement(Foo)),
			{
				createNodeMock = function()
					return "foo"
				end,
			}
		)

		jestExpect(function()
			return inst.unmount()
		end).never.toThrow()
		jestExpect(count).toEqual(1)
	end)
	it("supports updates when using refs", function()
		local log = {}
		local createNodeMock = function(element)
			table.insert(log, element.type)
			return element.type
		end
		local Foo = React.Component:extend("Foo")

		function Foo:render()
			return (function()
				if self.props.useDiv then
					return React.createElement("div", {
						ref = React.createRef(),
					})
				end

				return React.createElement("span", {
					ref = React.createRef(),
				})
			end)()
		end

		local inst = ReactTestRenderer.create(
			React.createElement(Foo, { useDiv = true }),
			{ createNodeMock = createNodeMock }
		)

		inst.update(React.createElement(Foo, { useDiv = false }))
		jestExpect(log).toEqual({
			"div",
			"span",
		})
	end)
	it("supports error boundaries", function()
		local log = {}
		local Angry = React.Component:extend("Angry")

		function Angry:render()
			table.insert(log, "Angry render")
			error("Please, do not render me.")
		end
		function Angry:componentDidMount()
			table.insert(log, "Angry componentDidMount")
		end
		function Angry:componentWillUnmount()
			table.insert(log, "Angry componentWillUnmount")
		end

		local Boundary = React.Component:extend("Boundary")

		function Boundary:init(props)
			self.state = { error = false }
		end
		function Boundary:render()
			table.insert(log, "Boundary render")

			if not self.state.error then
				return React.createElement(
					"div",
					nil,
					React.createElement("button", {
						onClick = self.onClick,
					}, "ClickMe"),
					React.createElement(Angry)
				)
			else
				return React.createElement("div", nil, "Happy Birthday!")
			end
		end
		function Boundary:componentDidMount()
			table.insert(log, "Boundary componentDidMount")
		end
		function Boundary:componentWillUnmount()
			table.insert(log, "Boundary componentWillUnmount")
		end
		function Boundary:onClick()
			-- do nothing
			return
		end
		function Boundary:componentDidCatch()
			table.insert(log, "Boundary componentDidCatch")
			self:setState({ error = true })
		end

		-- ROBLOX Test Noise: jest setup config makes this hide error
		-- boundary warnings in upstream (scripts/jest/setupTests.js:72)
		local renderer = ReactTestRenderer.create(React.createElement(Boundary))

		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			props = {},
			children = {
				"Happy Birthday!",
			},
		})
		jestExpect(log).toEqual({
			"Boundary render",
			"Angry render",
			"Boundary componentDidMount",
			"Boundary componentDidCatch",
			"Boundary render",
		})
	end)
	it("can update text nodes", function()
		local Component = React.Component:extend("Component")

		function Component:render()
			return React.createElement("div", nil, self.props.children)
		end

		local renderer =
			ReactTestRenderer.create(React.createElement(Component, nil, "Hi"))

		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			children = {
				"Hi",
			},
			props = {},
		})
		renderer.update(React.createElement(Component, nil, {
			"Hi",
			"Bye",
		}))
		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			children = {
				"Hi",
				"Bye",
			},
			props = {},
		})
		renderer.update(React.createElement(Component, nil, "Bye"))
		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			children = {
				"Bye",
			},
			props = {},
		})
		renderer.update(React.createElement(Component, nil, 42))
		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			children = {
				"42",
			},
			props = {},
		})
		renderer.update(React.createElement(Component, nil, React.createElement("div")))
		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			children = {
				{
					type = "div",
					children = nil,
					props = {},
				},
			},
			props = {},
		})
	end)
	it("toTree() renders simple components returning host components", function()
		local Qoo = function()
			return React.createElement("span", {
				className = "Qoo",
			}, "Hello World!")
		end
		local renderer = ReactTestRenderer.create(React.createElement(Qoo))
		local tree = renderer.toTree()

		cleanNodeOrArray(tree)

		-- ROBLOX deviation: no need to pretty format
		jestExpect(tree).toEqual({
			nodeType = "component",
			type = Qoo,
			props = {},
			instance = nil,
			rendered = {
				nodeType = "host",
				type = "span",
				props = {
					className = "Qoo",
				},
				instance = nil,
				rendered = {
					"Hello World!",
				},
			},
		})
	end)
	it("toTree() handles nested Fragments", function()
		local Foo = function()
			return React.createElement(
				React.Fragment,
				nil,
				React.createElement(React.Fragment, nil, "foo")
			)
		end
		local renderer = ReactTestRenderer.create(React.createElement(Foo))
		local tree = renderer.toTree()

		cleanNodeOrArray(tree)

		-- ROBLOX deviation: no need to pretty format
		jestExpect(tree).toEqual({
			nodeType = "component",
			type = Foo,
			instance = nil,
			props = {},
			rendered = "foo",
		})
	end)
	it("toTree() handles null rendering components", function()
		local Foo = React.Component:extend("Foo")

		function Foo:render()
			return nil
		end

		local renderer = ReactTestRenderer.create(React.createElement(Foo))
		local tree = renderer.toTree()

		-- ROBLOX deviation: toBeInstanceOf not yet implemented, workaround by checking elementType
		jestExpect(tree.instance._reactInternals.elementType.__componentName).toEqual(
			"Foo"
		)
		cleanNodeOrArray(tree)

		jestExpect(tree).toEqual({
			type = Foo,
			nodeType = "component",
			props = {},
			instance = nil,
			rendered = nil,
		})
	end)
	it("toTree() handles simple components that return arrays", function()
		local Foo = function(_ref)
			local children = _ref.children

			return children
		end
		local renderer = ReactTestRenderer.create(
			React.createElement(
				Foo,
				nil,
				React.createElement("div", nil, "One"),
				React.createElement("div", nil, "Two")
			)
		)
		local tree = renderer.toTree()

		cleanNodeOrArray(tree)

		-- ROBLOX deviation: no need to pretty format
		jestExpect(tree).toEqual({
			type = Foo,
			nodeType = "component",
			props = {},
			instance = nil,
			rendered = {
				{
					instance = nil,
					nodeType = "host",
					props = {},
					rendered = {
						"One",
					},
					type = "div",
				},
				{
					instance = nil,
					nodeType = "host",
					props = {},
					rendered = {
						"Two",
					},
					type = "div",
				},
			},
		})
	end)
	it("toTree() handles complicated tree of arrays", function()
		local Foo = React.Component:extend("Foo")

		function Foo:render()
			return self.props.children
		end

		local renderer = ReactTestRenderer.create(
			React.createElement(
				"div",
				nil,
				React.createElement(
					Foo,
					nil,
					React.createElement("div", nil, "One"),
					React.createElement("div", nil, "Two"),
					React.createElement(
						Foo,
						nil,
						React.createElement("div", nil, "Three")
					)
				),
				React.createElement("div", nil, "Four")
			)
		)
		local tree = renderer.toTree()

		cleanNodeOrArray(tree)

		-- ROBLOX deviation: no need to pretty format
		jestExpect(tree).toEqual({
			type = "div",
			instance = nil,
			nodeType = "host",
			props = {},
			rendered = {
				{
					type = Foo,
					nodeType = "component",
					props = {},
					instance = nil,
					rendered = {
						{
							type = "div",
							nodeType = "host",
							props = {},
							instance = nil,
							rendered = {
								"One",
							},
						},
						{
							type = "div",
							nodeType = "host",
							props = {},
							instance = nil,
							rendered = {
								"Two",
							},
						},
						{
							type = Foo,
							nodeType = "component",
							props = {},
							instance = nil,
							rendered = {
								type = "div",
								nodeType = "host",
								props = {},
								instance = nil,
								rendered = {
									"Three",
								},
							},
						},
					},
				},
				{
					type = "div",
					nodeType = "host",
					props = {},
					instance = nil,
					rendered = {
						"Four",
					},
				},
			},
		})
	end)
	it("toTree() handles complicated tree of fragments", function()
		local renderer = ReactTestRenderer.create(
			React.createElement(
				React.Fragment,
				nil,
				React.createElement(
					React.Fragment,
					nil,
					React.createElement("div", nil, "One"),
					React.createElement("div", nil, "Two"),
					React.createElement(
						React.Fragment,
						nil,
						React.createElement("div", nil, "Three")
					)
				),
				React.createElement("div", nil, "Four")
			)
		)
		local tree = renderer.toTree()

		cleanNodeOrArray(tree)
		-- ROBLOX deviation: no need to pretty format
		jestExpect(tree).toEqual({
			{
				type = "div",
				nodeType = "host",
				props = {},
				instance = nil,
				rendered = {
					"One",
				},
			},
			{
				type = "div",
				nodeType = "host",
				props = {},
				instance = nil,
				rendered = {
					"Two",
				},
			},
			{
				type = "div",
				nodeType = "host",
				props = {},
				instance = nil,
				rendered = {
					"Three",
				},
			},
			{
				type = "div",
				nodeType = "host",
				props = {},
				instance = nil,
				rendered = {
					"Four",
				},
			},
		})
	end)
	it("root instance and createNodeMock ref return the same value", function()
		local createNodeMock = function(ref)
			return { node = ref }
		end
		local refInst = nil
		local renderer = ReactTestRenderer.create(
			React.createElement("div", {
				ref = function(ref)
					refInst = ref
					return
				end,
			}),
			{ createNodeMock = createNodeMock }
		)
		local root = renderer.getInstance()

		jestExpect(root).toEqual(refInst)
	end)
	it("toTree() renders complicated trees of composites and hosts", function()
		-- SFC returning host. no children props.
		local Qoo = function()
			return React.createElement("span", {
				className = "Qoo",
			}, "Hello World!")
		end

		-- SFC returning host. passes through children.
		local Foo = function(props)
			local className, children = props.className, props.children

			return React.createElement(
				"div",
				{
					className = "Foo " .. className,
				},
				React.createElement("span", {
					className = "Foo2",
				}, "Literal"),
				children
			)
		end

		-- class composite returning composite. passes through children.
		local Bar = React.Component:extend("Bar")
		function Bar:render()
			local children = self.props.children
			local special = self.props.special

			return React.createElement(Foo, {
				className = (function()
					if special then
						return "special"
					end

					return "normal"
				end)(),
			}, children)
		end

		-- class composite return composite. no children props.
		local Bam = React.Component:extend("Bam")

		function Bam:render()
			return React.createElement(Bar, { special = true }, React.createElement(Qoo))
		end

		local renderer = ReactTestRenderer.create(React.createElement(Bam))
		local tree = renderer.toTree()

		-- we test for the presence of instances before nulling them out
		-- ROBLOX deviation: toBeInstanceOf not yet implemented, workaround by checking elementType
		jestExpect(tree.instance._reactInternals.elementType.__componentName).toEqual(
			"Bam"
		)
		jestExpect(tree.rendered.instance._reactInternals.elementType.__componentName).toEqual(
			"Bar"
		)

		cleanNodeOrArray(tree)

		jestExpect(tree).toEqual({
			type = Bam,
			nodeType = "component",
			props = {},
			instance = nil,
			rendered = {
				type = Bar,
				nodeType = "component",
				props = { special = true },
				instance = nil,
				rendered = {
					type = Foo,
					nodeType = "component",
					props = {
						className = "special",
					},
					instance = nil,
					rendered = {
						type = "div",
						nodeType = "host",
						props = {
							className = "Foo special",
						},
						instance = nil,
						rendered = {
							{
								type = "span",
								nodeType = "host",
								props = {
									className = "Foo2",
								},
								instance = nil,
								rendered = {
									"Literal",
								},
							},
							{
								type = Qoo,
								nodeType = "component",
								props = {},
								instance = nil,
								rendered = {
									type = "span",
									nodeType = "host",
									props = {
										className = "Qoo",
									},
									instance = nil,
									rendered = {
										"Hello World!",
									},
								},
							},
						},
					},
				},
			},
		})
	end)
	it("can update text nodes when rendered as root", function()
		local renderer = ReactTestRenderer.create({
			"Hello",
			"world",
		})

		jestExpect(renderer.toJSON()).toEqual({
			"Hello",
			"world",
		})
		renderer.update(42)
		jestExpect(renderer.toJSON()).toEqual("42")
		renderer.update({
			42,
			"world",
		})
		jestExpect(renderer.toJSON()).toEqual({
			"42",
			"world",
		})
	end)
	it("can render and update root fragments", function()
		local Component = function(props)
			return props.children
		end
		local renderer = ReactTestRenderer.create({
			React.createElement(Component, {
				key = "a",
			}, "Hi"),
			React.createElement(Component, {
				key = "b",
			}, "Bye"),
		})

		jestExpect(renderer.toJSON()).toEqual({
			"Hi",
			"Bye",
		})
		renderer.update(React.createElement("div"))
		jestExpect(renderer.toJSON()).toEqual({
			type = "div",
			children = nil,
			props = {},
		})
		renderer.update({
			React.createElement("div", {
				key = "a",
			}, "goodbye"),
			"world",
		})
		jestExpect(renderer.toJSON()).toEqual({
			{
				type = "div",
				children = {
					"goodbye",
				},
				props = {},
			},
			"world",
		})
	end)
	it("supports context providers and consumers", function()
		local context = React.createContext("a")
		local Consumer = context.Consumer
		local Provider = context.Provider

		local function Child(props)
			return props.value
		end
		local function App()
			return React.createElement(
				Provider,
				{
					value = "b",
				},
				React.createElement(Consumer, nil, function(value)
					return React.createElement(Child, { value = value })
				end)
			)
		end

		local renderer = ReactTestRenderer.create(React.createElement(App))
		local child = renderer.root:findByType(Child)

		jestExpect(child.children).toEqual({
			"b",
		})
		-- ROBLOX deviation: no need to pretty format
		jestExpect(renderer.toTree()).toEqual({
			instance = nil,
			nodeType = "component",
			props = {},
			rendered = {
				instance = nil,
				nodeType = "component",
				props = {
					value = "b",
				},
				rendered = "b",
				type = Child,
			},
			type = App,
		})
	end)
	it("supports modes", function()
		local function Child(props)
			return props.value
		end
		local function App(props)
			return React.createElement(
				React.StrictMode,
				nil,
				React.createElement(Child, {
					value = props.value,
				})
			)
		end

		local renderer = ReactTestRenderer.create(React.createElement(App, {
			value = "a",
		}))
		local child = renderer.root:findByType(Child)

		jestExpect(child.children).toEqual({
			"a",
		})
		-- ROBLOX deviation: no need to pretty format
		jestExpect(renderer.toTree()).toEqual({
			instance = nil,
			nodeType = "component",
			props = {
				value = "a",
			},
			rendered = {
				instance = nil,
				nodeType = "component",
				props = {
					value = "a",
				},
				rendered = "a",
				type = Child,
			},
			type = App,
		})
	end)
	it("supports forwardRef", function()
		local InnerRefed = React.forwardRef(function(props, ref)
			return React.createElement(
				"div",
				nil,
				React.createElement("span", { ref = ref })
			)
		end)
		local App = React.Component:extend("App")

		function App:render()
			return React.createElement(InnerRefed, {
				ref = function(r)
					self.ref = r
					return
				end,
			})
		end

		local renderer = ReactTestRenderer.create(React.createElement(App))
		local tree = renderer.toTree()

		cleanNodeOrArray(tree)

		-- ROBLOX deviation: no need to pretty format
		jestExpect(tree).toEqual({
			instance = nil,
			nodeType = "component",
			props = {},
			rendered = {
				instance = nil,
				nodeType = "host",
				props = {},
				rendered = {
					{
						instance = nil,
						nodeType = "host",
						props = {},
						rendered = {},
						type = "span",
					},
				},
				type = "div",
			},
			type = App,
		})
	end)
	-- ROBLOX TODO: set up React Noop in this file
	-- xit('can concurrently render context with a "primary" renderer', function()
	--     local Context = React.createContext(nil)
	--     local Indirection = React.Fragment
	--     local App = function()
	--         return React.createElement(Context.Provider, {value = nil}, React.createElement(Indirection, nil, React.createElement(Context.Consumer, nil, function(
	--         )
	--             return nil
	--         end)))
	--     end

	--     ReactNoop.render(React.createElement(App))
	--     jestExpect(Scheduler).toFlushWithoutYielding()
	--     ReactTestRenderer.create(React.createElement(App))
	-- end)
	it(
		'calling findByType() with an invalid component will fall back to "Unknown" for component name',
		function()
			local App = function()
				return nil
			end
			local renderer = ReactTestRenderer.create(React.createElement(App))
			local NonComponent = {}
			jestExpect(function()
				renderer.root:findByType(NonComponent)
			end).toThrow('No instances found with node type: "Unknown"')
		end
	)
end)
