local j=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\106\101\115\116-\103\108\111\098\097\108\115")local d=j.expect local A=j.jest local f=d local T=j.beforeEach local Z=j.it local O=j.describe local I local g local G local J local S T(function()A.resetModules()A.useFakeTimers()I=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\114\101\097\099\116")g=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\114\101\097\099\116-\114\111\098\108\111\120")S=Instance.new("\070\111\108\100\101\114")G=g.createRoot(S)J=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\115\099\104\101\100\117\108\101\114")end)O("\109\111\117\110\116\105\110\103 \105\110\115\116\097\110\099\101\115",function()Z("\115\104\111\117\108\100 \099\114\101\097\116\101 \105\110\115\116\097\110\099\101\115 \119\105\116\104 \099\111\114\114\101\099\116 \112\114\111\112\115",function()local j="\072\101\108\108\111\033"local A="\083\111\109\101 \075\101\121"local f=I.createElement("\083\116\114\105\110\103\086\097\108\117\101",{Name=A;Value=j})G:render(f)J.unstable_flushAllWithoutAsserting();(d(#S:GetChildren())).toBe(1)local T=(S:GetChildren())[1];(d(T.ClassName)).toBe("\083\116\114\105\110\103\086\097\108\117\101");(d(T.Value)).toBe(j);(d(T.Name)).toBe(A)end)Z("\110\097\109\101\115 \105\110\115\116\097\110\099\101\115 \119\105\116\104 \116\104\101\105\114 \107\101\121 \118\097\108\117\101 \117\115\105\110\103 \108\101\103\097\099\121 \107\101\121 \115\121\110\116\097\120",function()local j="\083\111\109\101 \075\101\121"local A=I.createElement("\070\111\108\100\101\114",{},{[j]=I.createElement("\066\111\111\108\086\097\108\117\101")})G:render(A)J.unstable_flushAllWithoutAsserting();(d(#S:GetChildren())).toBe(1)local f=(S:GetChildren())[1];(d(f.ClassName)).toBe("\070\111\108\100\101\114")local T=f:FindFirstChildOfClass("\066\111\111\108\086\097\108\117\101");(d(T)).toBeDefined();(d(T.Name)).toEqual(j)end)Z("\110\097\109\101\115 \105\110\115\116\097\110\099\101\115 \119\105\116\104 \116\104\101\105\114 \107\101\121 \118\097\108\117\101 \040\117\115\105\110\103 \112\114\111\112\115\041",function()local j="\083\111\109\101 \075\101\121"local A=I.createElement("\070\111\108\100\101\114",{},I.createElement("\066\111\111\108\086\097\108\117\101",{key=j}))G:render(A)J.unstable_flushAllWithoutAsserting();(d(#S:GetChildren())).toBe(1)local f=(S:GetChildren())[1];(d(f.ClassName)).toBe("\070\111\108\100\101\114")local T=f:FindFirstChildOfClass("\066\111\111\108\086\097\108\117\101");(d(T)).toBeDefined();(d(T.Name)).toEqual(j)end)Z("\110\097\109\101\115 \105\110\115\116\097\110\099\101\115 \119\105\116\104 \116\104\101\105\114 \107\101\121 \118\097\108\117\101 \117\115\105\110\103 \108\101\103\097\099\121 \107\101\121 \115\121\110\116\097\120 \097\110\100 \117\112\100\097\116\101\115 \116\104\101\109",function()local j="\083\111\109\101 \075\101\121"local f=A.fn()local T=function(...)return f(...)end local Z=I.createElement("\070\111\108\100\101\114",{},{[j]=I.createElement("\066\111\111\108\086\097\108\117\101",{ref=T})})G:render(Z)J.unstable_flushAllWithoutAsserting();(d(f)).toHaveBeenCalledTimes(1)local O=f.mock.calls[1][1];(d(O.Name)).toEqual(j)local g="\083\111\109\101 \111\116\104\101\114 \107\101\121"local S=I.createElement("\070\111\108\100\101\114",{},{[g]=I.createElement("\066\111\111\108\086\097\108\117\101",{ref=T})})G:render(S)J.unstable_flushAllWithoutAsserting();(d(f)).toHaveBeenCalledTimes(3);(d(f)).toHaveBeenNthCalledWith(2)local v=f.mock.calls[3][1];(d(v.Name)).toEqual(g)end)Z("\115\104\111\117\108\100 \099\114\101\097\116\101 \099\104\105\108\100\114\101\110 \119\105\116\104 \099\111\114\114\101\099\116 \110\097\109\101\115 \097\110\100 \112\114\111\112\115",function()local j="\072\101\121 \116\104\101\114\101\033"local d=173 local A="\083\111\109\101 \075\101\121"local T=I.createElement("\083\116\114\105\110\103\086\097\108\117\101",{key=A,Value=j},{ChildA=I.createElement("\073\110\116\086\097\108\117\101",{Value=d});ChildB=I.createElement("\070\111\108\100\101\114")})G:render(T)J.unstable_flushAllWithoutAsserting();(f(#S:GetChildren())).toEqual(1)local Z=(S:GetChildren())[1];(f(Z.ClassName)).toEqual("\083\116\114\105\110\103\086\097\108\117\101");(f(Z.Value)).toEqual(j);(f(Z.Name)).toEqual(A);(f(#Z:GetChildren())).toEqual(2)local O=Z.ChildA local g=Z.ChildB;(f(O)).toBeTruthy();(f(g)).toBeTruthy();(f(O.ClassName)).toEqual("\073\110\116\086\097\108\117\101");(f(O.Value)).toEqual(d);(f(g.ClassName)).toEqual("\070\111\108\100\101\114")end)Z("\110\097\109\101\115 \105\110\115\116\097\110\099\101\115 \119\105\116\104 \116\104\101\105\114 \107\101\121 \118\097\108\117\101 \117\115\105\110\103 \108\101\103\097\099\121 \107\101\121 \115\121\110\116\097\120 \116\104\114\111\117\103\104 \102\117\110\099\116\105\111\110 \099\111\109\112\111\110\101\110\116",function()local j="\083\111\109\101 \075\101\121"local function A()return I.createElement("\066\111\111\108\086\097\108\117\101")end local f=I.createElement("\070\111\108\100\101\114",{},{[j]=I.createElement(A)})G:render(f)J.unstable_flushAllWithoutAsserting();(d(#S:GetChildren())).toBe(1)local T=(S:GetChildren())[1];(d(T.ClassName)).toBe("\070\111\108\100\101\114")local Z=T:FindFirstChildOfClass("\066\111\111\108\086\097\108\117\101");(d(Z)).toBeDefined();(d(Z.Name)).toEqual(j)end)end)O("\117\112\100\097\116\105\110\103 \105\110\115\116\097\110\099\101\115",function()Z("\115\104\111\117\108\100 \117\112\100\097\116\101 \110\111\100\101 \112\114\111\112\115 \097\110\100 \099\104\105\108\100\114\101\110",function()local j="\117\112\100\097\116\101\072\111\115\116\078\111\100\101\084\101\115\116"local A="\102\111\111"local f="\098\097\114"local T=(Instance.new("\083\116\114\105\110\103\086\097\108\117\101")).Value local Z=I.createElement("\083\116\114\105\110\103\086\097\108\117\101",{Name=j;Value=A},{ChildA=I.createElement("\073\110\116\086\097\108\117\101",{Name="\067\104\105\108\100\065";Value=1}),ChildB=I.createElement("\066\111\111\108\086\097\108\117\101",{Name="\067\104\105\108\100\066";Value=true});ChildC=I.createElement("\083\116\114\105\110\103\086\097\108\117\101",{Name="\067\104\105\108\100\067";Value="\116\101\115\116"}),ChildD=I.createElement("\083\116\114\105\110\103\086\097\108\117\101",{Name="\067\104\105\108\100\068";Value="\116\101\115\116"})})G:render(Z)J.unstable_flushAllWithoutAsserting()local O=I.createElement("\083\116\114\105\110\103\086\097\108\117\101",{Name=j,Value=f},{ChildA=I.createElement("\083\116\114\105\110\103\086\097\108\117\101",{Name="\067\104\105\108\100\065",Value="\116\101\115\116"});ChildB=I.createElement("\066\111\111\108\086\097\108\117\101",{Name="\067\104\105\108\100\066";Value=false}),ChildC=I.createElement("\083\116\114\105\110\103\086\097\108\117\101",{Name="\067\104\105\108\100\067"});ChildE=I.createElement("\070\111\108\100\101\114",{Name="\067\104\105\108\100\069"})})G:render(O)J.unstable_flushAllWithoutAsserting()local g=S[j];(d(g.ClassName)).toBe("\083\116\114\105\110\103\086\097\108\117\101");(d(g.Value)).toBe(f);(d(#g:GetChildren())).toBe(4)local v=g.ChildA;(d(v.ClassName)).toBe("\083\116\114\105\110\103\086\097\108\117\101");(d(v.Value)).toBe("\116\101\115\116")local s=g.ChildB;(d(s.ClassName)).toBe("\066\111\111\108\086\097\108\117\101");(d(s.Value)).toBe(false)local t=g.ChildC;(d(t.ClassName)).toBe("\083\116\114\105\110\103\086\097\108\117\101");(d(t.Value)).toBe(T)local h=g.ChildE;(d(h.ClassName)).toBe("\070\111\108\100\101\114")end)end)O("\080\111\114\116\097\108\115",function()Z("\115\104\111\117\108\100 \099\114\101\097\116\101 \097\110\100 \100\101\115\116\114\111\121 \105\110\115\116\097\110\099\101\115 \097\115 \099\104\105\108\100\114\101\110 \111\102 \096\116\097\114\103\101\116\096",function()local j=Instance.new("\070\111\108\100\101\114")local function A(j)return I.createElement("\073\110\116\086\097\108\117\101",{Name="\105\110\116\086\097\108\117\101\079\110\101",Value=j.value})end local f=g.createPortal({I.createElement("\070\111\108\100\101\114",{key="\049";Name="\102\111\108\100\101\114\079\110\101"});I.createElement("\070\111\108\100\101\114",{key="\050",Name="\102\111\108\100\101\114\084\119\111"});I.createElement(A,{key="\051";value=42})},j)G:render(f)J.unstable_flushAllWithoutAsserting();(d(#j:GetChildren())).toBe(3);(d(j:FindFirstChild("\102\111\108\100\101\114\079\110\101"))).toBeDefined();(d(j:FindFirstChild("\102\111\108\100\101\114\084\119\111"))).toBeDefined();(d(j:FindFirstChild("\105\110\116\086\097\108\117\101\079\110\101"))).toBeDefined();(d((j:FindFirstChild("\105\110\116\086\097\108\117\101\079\110\101")).Value)).toBe(42)G:unmount()J.unstable_flushAllWithoutAsserting();(d(#j:GetChildren())).toBe(0)end)Z("\115\104\111\117\108\100 \112\097\115\115 \112\114\111\112 \117\112\100\097\116\101\115 \116\104\114\111\117\103\104 \116\111 \099\104\105\108\100\114\101\110",function()local j=Instance.new("\070\111\108\100\101\114")local A=g.createPortal({ChildValue=I.createElement("\073\110\116\086\097\108\117\101",{Value=1})},j)local f=g.createPortal({ChildValue=I.createElement("\073\110\116\086\097\108\117\101",{Value=2})},j)G:render(A)J.unstable_flushAllWithoutAsserting();(d(#j:GetChildren())).toBe(1);(d((j:GetChildren())[1].Value)).toBe(1)G:render(f)J.unstable_flushAllWithoutAsserting();(d(#j:GetChildren())).toBe(1);(d((j:GetChildren())[1].Value)).toBe(2)G:unmount()J.unstable_flushAllWithoutAsserting();(d(#j:GetChildren())).toBe(0)end)Z("\115\104\111\117\108\100 \116\104\114\111\119 \105\102 \096\116\097\114\103\101\116\096 \105\115 \110\105\108",function()(d(function()g.createPortal(I.createElement("\073\110\116\086\097\108\117\101",{Value=1}))end)).toThrow()end)Z("\115\104\111\117\108\100 \114\101\099\114\101\097\116\101 \105\110\115\116\097\110\099\101\115 \105\102 \096\116\097\114\103\101\116\096 \099\104\097\110\103\101\115 \105\110 \097\110 \117\112\100\097\116\101",function()local j=Instance.new("\070\111\108\100\101\114")local A=Instance.new("\070\111\108\100\101\114")local f=g.createPortal(I.createElement("\073\110\116\086\097\108\117\101",{Value=1}),j)local T=g.createPortal(I.createElement("\073\110\116\086\097\108\117\101",{Value=2}),A)G:render(f)J.unstable_flushAllWithoutAsserting();(d(#j:GetChildren())).toBe(1);(d(#A:GetChildren())).toBe(0);(d((j:GetChildren())[1].Value)).toBe(1)G:render(T)J.unstable_flushAllWithoutAsserting();(d(#j:GetChildren())).toBe(0);(d(#A:GetChildren())).toBe(1);(d((A:GetChildren())[1].Value)).toBe(2)G:unmount()J.unstable_flushAllWithoutAsserting();(d(#j:GetChildren())).toBe(0);(d(#A:GetChildren())).toBe(0)end)end)O("\067\111\110\116\101\120\116",function()Z("\115\104\111\117\108\100 \112\097\115\115 \099\111\110\116\101\120\116 \118\097\108\117\101\115 \116\104\114\111\117\103\104 \112\111\114\116\097\108 \110\111\100\101\115",function()local j=Instance.new("\070\111\108\100\101\114")local A=I.createContext(1)local function f(d)return I.createElement(A.Provider,{value=d.value},{Portal=g.createPortal({Consumer=I.createElement(A.Consumer,nil,function(j)return I.createElement("\084\101\120\116\076\097\098\101\108",{Text="\082\101\115\117\108\116\058 "..tostring(j)})end)},j)})end G:render(I.createElement(f,{value=2}))J.unstable_flushAllWithoutAsserting();(d(#j:GetChildren())).toBe(1);(d((j:GetChildren())[1].Text)).toBe("\082\101\115\117\108\116\058 \050")G:render(I.createElement(f,{value=3}))J.unstable_flushAllWithoutAsserting();(d(#j:GetChildren())).toBe(1);(d((j:GetChildren())[1].Text)).toBe("\082\101\115\117\108\116\058 \051")end)end)
local JestGlobals = require("@pkg/@jsdotlua/jest-globals")
local jestExpect = JestGlobals.expect
local jest = JestGlobals.jest
local expect = jestExpect
local beforeEach = JestGlobals.beforeEach
local it = JestGlobals.it
local describe = JestGlobals.describe

local React
local ReactRoblox
local reactRobloxRoot
local Scheduler
local parent

beforeEach(function()
	jest.resetModules()
	jest.useFakeTimers()

	React = require("@pkg/@jsdotlua/react")
	ReactRoblox = require("@pkg/@jsdotlua/react-roblox")
	parent = Instance.new("Folder")
	reactRobloxRoot = ReactRoblox.createRoot(parent)
	Scheduler = require("@pkg/@jsdotlua/scheduler")
end)

describe("mounting instances", function()
	it("should create instances with correct props", function()
		local value = "Hello!"
		local key = "Some Key"

		local element = React.createElement("StringValue", {
			Name = key,
			Value = value,
		})

		reactRobloxRoot:render(element)
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#parent:GetChildren()).toBe(1)

		local rootInstance = parent:GetChildren()[1]

		jestExpect(rootInstance.ClassName).toBe("StringValue")
		jestExpect(rootInstance.Value).toBe(value)
		jestExpect(rootInstance.Name).toBe(key)
	end)

	it("names instances with their key value using legacy key syntax", function()
		local key = "Some Key"

		local element = React.createElement("Folder", {}, {
			[key] = React.createElement("BoolValue"),
		})

		reactRobloxRoot:render(element)
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#parent:GetChildren()).toBe(1)

		local rootInstance = parent:GetChildren()[1]
		jestExpect(rootInstance.ClassName).toBe("Folder")

		local boolValueInstance = rootInstance:FindFirstChildOfClass("BoolValue")
		jestExpect(boolValueInstance).toBeDefined()
		jestExpect(boolValueInstance.Name).toEqual(key)
	end)

	it("names instances with their key value (using props)", function()
		local key = "Some Key"

		local element = React.createElement(
			"Folder",
			{},
			React.createElement("BoolValue", {
				key = key,
			})
		)

		reactRobloxRoot:render(element)
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#parent:GetChildren()).toBe(1)

		local rootInstance = parent:GetChildren()[1]
		jestExpect(rootInstance.ClassName).toBe("Folder")

		local boolValueInstance = rootInstance:FindFirstChildOfClass("BoolValue")
		jestExpect(boolValueInstance).toBeDefined()
		jestExpect(boolValueInstance.Name).toEqual(key)
	end)

	it(
		"names instances with their key value using legacy key syntax and updates them",
		function()
			local key = "Some Key"
			local fnMock = jest.fn()
			local ref = function(...)
				return fnMock(...)
			end

			local element = React.createElement("Folder", {}, {
				[key] = React.createElement("BoolValue", {
					ref = ref,
				}),
			})

			reactRobloxRoot:render(element)
			Scheduler.unstable_flushAllWithoutAsserting()

			jestExpect(fnMock).toHaveBeenCalledTimes(1)
			local refValue = fnMock.mock.calls[1][1]
			jestExpect(refValue.Name).toEqual(key)

			local updatedKey = "Some other key"
			local updatedElement = React.createElement("Folder", {}, {
				[updatedKey] = React.createElement("BoolValue", {
					ref = ref,
				}),
			})
			reactRobloxRoot:render(updatedElement)
			Scheduler.unstable_flushAllWithoutAsserting()

			jestExpect(fnMock).toHaveBeenCalledTimes(3)
			-- ROBLOX FIXME: jest mocks should be able to handle explicit
			-- trailing nils, but do not appear to do so
			jestExpect(fnMock).toHaveBeenNthCalledWith(2 --[[, nil ]])

			local lastRefValue = fnMock.mock.calls[3][1]
			jestExpect(lastRefValue.Name).toEqual(updatedKey)
		end
	)

	it("should create children with correct names and props", function()
		local rootValue = "Hey there!"
		local childValue = 173
		local key = "Some Key"

		local element = React.createElement("StringValue", {
			key = key,
			Value = rootValue,
		}, {
			ChildA = React.createElement("IntValue", {
				Value = childValue,
			}),

			ChildB = React.createElement("Folder"),
		})

		reactRobloxRoot:render(element)
		Scheduler.unstable_flushAllWithoutAsserting()

		expect(#parent:GetChildren()).toEqual(1)

		local rootInstance = parent:GetChildren()[1]

		expect(rootInstance.ClassName).toEqual("StringValue")
		expect(rootInstance.Value).toEqual(rootValue)
		expect(rootInstance.Name).toEqual(key)

		expect(#rootInstance:GetChildren()).toEqual(2)

		local childA = rootInstance.ChildA
		local childB = rootInstance.ChildB

		expect(childA).toBeTruthy()
		expect(childB).toBeTruthy()

		expect(childA.ClassName).toEqual("IntValue")
		expect(childA.Value).toEqual(childValue)

		expect(childB.ClassName).toEqual("Folder")
	end)

	it(
		"names instances with their key value using legacy key syntax through function component",
		function()
			local key = "Some Key"

			local function Foo()
				return React.createElement("BoolValue")
			end

			local element = React.createElement("Folder", {}, {
				[key] = React.createElement(Foo),
			})

			reactRobloxRoot:render(element)
			Scheduler.unstable_flushAllWithoutAsserting()

			jestExpect(#parent:GetChildren()).toBe(1)

			local rootInstance = parent:GetChildren()[1]
			jestExpect(rootInstance.ClassName).toBe("Folder")

			local boolValueInstance = rootInstance:FindFirstChildOfClass("BoolValue")
			jestExpect(boolValueInstance).toBeDefined()
			jestExpect(boolValueInstance.Name).toEqual(key)
		end
	)

	-- it("should attach Bindings to Roblox properties", function()
	-- 	local parent = Instance.new("Folder")
	-- 	local key = "Some Key"

	-- 	local binding, update = Binding.create(10)
	-- 	local element = createElement("IntValue", {
	-- 		Value = binding,
	-- 	})

	-- 	local node = reconciler.createVirtualNode(element, parent, key)

	-- 	RobloxRenderer.mountHostNode(reconciler, node)

	-- 	expect(#parent:GetChildren()).toEqual(1)

	-- 	local instance = parent:GetChildren()[1]

	-- 	expect(instance.ClassName).toEqual("IntValue")
	-- 	expect(instance.Value).toEqual(10)

	-- 	update(20)

	-- 	expect(instance.Value).toEqual(20)

	-- 	RobloxRenderer.unmountHostNode(reconciler, node)
	-- end)

	-- it("should connect Binding refs", function()
	-- 	local parent = Instance.new("Folder")
	-- 	local key = "Some Key"

	-- 	local ref = createRef()
	-- 	local element = createElement("Frame", {
	-- 		[Ref] = ref,
	-- 	})

	-- 	local node = reconciler.createVirtualNode(element, parent, key)

	-- 	RobloxRenderer.mountHostNode(reconciler, node)

	-- 	expect(#parent:GetChildren()).toEqual(1)

	-- 	local instance = parent:GetChildren()[1]

	-- 	expect(ref.current).toBeTruthy()
	-- 	expect(ref.current).toEqual(instance)

	-- 	RobloxRenderer.unmountHostNode(reconciler, node)
	-- end)

	-- it("should call function refs", function()
	-- 	local parent = Instance.new("Folder")
	-- 	local key = "Some Key"

	-- 	local spyRef = createSpy()
	-- 	local element = createElement("Frame", {
	-- 		[Ref] = spyRef.value,
	-- 	})

	-- 	local node = reconciler.createVirtualNode(element, parent, key)

	-- 	RobloxRenderer.mountHostNode(reconciler, node)

	-- 	expect(#parent:GetChildren()).toEqual(1)

	-- 	local instance = parent:GetChildren()[1]

	-- 	expect(spyRef.callCount).toEqual(1)
	-- 	spyRef:assertCalledWith(instance)

	-- 	RobloxRenderer.unmountHostNode(reconciler, node)
	-- end)

	-- it("should throw if setting invalid instance properties", function()
	-- 	local configValues = {
	-- 		elementTracing = true,
	-- 	}

	-- 	GlobalConfig.scoped(configValues, function()
	-- 		local parent = Instance.new("Folder")
	-- 		local key = "Some Key"

	-- 		local element = createElement("Frame", {
	-- 			Frob = 6,
	-- 		})

	-- 		local node = reconciler.createVirtualNode(element, parent, key)

	-- 		local success, message = pcall(RobloxRenderer.mountHostNode, reconciler, node)
	-- 		assert(not success, "Expected call to fail")

	-- 		expect(string.find(message, "Frob")).toBeTruthy()
	-- 		expect(string.find(message, "Frame")).toBeTruthy()
	-- 		expect(string.find(message, "RobloxRenderer%.spec")).toBeTruthy()
	-- 	end)
	-- end)
end)

describe("updating instances", function()
	it("should update node props and children", function()
		local key = "updateHostNodeTest"
		local firstValue = "foo"
		local newValue = "bar"

		local defaultStringValue = Instance.new("StringValue").Value

		local element = React.createElement("StringValue", {
			Name = key,
			Value = firstValue,
		}, {
			ChildA = React.createElement("IntValue", {
				Name = "ChildA",
				Value = 1,
			}),
			ChildB = React.createElement("BoolValue", {
				Name = "ChildB",
				Value = true,
			}),
			ChildC = React.createElement("StringValue", {
				Name = "ChildC",
				Value = "test",
			}),
			ChildD = React.createElement("StringValue", {
				Name = "ChildD",
				Value = "test",
			}),
		})

		reactRobloxRoot:render(element)
		Scheduler.unstable_flushAllWithoutAsserting()

		-- Not testing mountHostNode's work here, only testing that the
		-- node is properly updated.

		local newElement = React.createElement("StringValue", {
			Name = key,
			Value = newValue,
		}, {
			-- ChildA changes element type.
			ChildA = React.createElement("StringValue", {
				Name = "ChildA",
				Value = "test",
			}),
			-- ChildB changes child properties.
			ChildB = React.createElement("BoolValue", {
				Name = "ChildB",
				Value = false,
			}),
			-- ChildC should reset its Value property back to the default.
			ChildC = React.createElement("StringValue", {
				Name = "ChildC",
			}),
			-- ChildD is deleted.
			-- ChildE is added.
			ChildE = React.createElement("Folder", {
				Name = "ChildE",
			}),
		})

		reactRobloxRoot:render(newElement)
		Scheduler.unstable_flushAllWithoutAsserting()

		local rootInstance = parent[key]
		jestExpect(rootInstance.ClassName).toBe("StringValue")
		jestExpect(rootInstance.Value).toBe(newValue)
		jestExpect(#rootInstance:GetChildren()).toBe(4)

		local childA = rootInstance.ChildA
		jestExpect(childA.ClassName).toBe("StringValue")
		jestExpect(childA.Value).toBe("test")

		local childB = rootInstance.ChildB
		jestExpect(childB.ClassName).toBe("BoolValue")
		jestExpect(childB.Value).toBe(false)

		local childC = rootInstance.ChildC
		jestExpect(childC.ClassName).toBe("StringValue")
		jestExpect(childC.Value).toBe(defaultStringValue)

		local childE = rootInstance.ChildE
		jestExpect(childE.ClassName).toBe("Folder")
	end)

	-- it("should update Bindings", function()
	-- 	local parent = Instance.new("Folder")
	-- 	local key = "Some Key"

	-- 	local bindingA, updateA = Binding.create(10)
	-- 	local element = createElement("IntValue", {
	-- 		Value = bindingA,
	-- 	})

	-- 	local node = reconciler.createVirtualNode(element, parent, key)

	-- 	RobloxRenderer.mountHostNode(reconciler, node)

	-- 	local instance = parent:GetChildren()[1]

	-- 	expect(instance.Value).toEqual(10)

	-- 	local bindingB, updateB = Binding.create(99)
	-- 	local newElement = createElement("IntValue", {
	-- 		Value = bindingB,
	-- 	})

	-- 	RobloxRenderer.updateHostNode(reconciler, node, newElement)

	-- 	expect(instance.Value).toEqual(99)

	-- 	updateA(123)

	-- 	expect(instance.Value).toEqual(99)

	-- 	updateB(123)

	-- 	expect(instance.Value).toEqual(123)

	-- 	RobloxRenderer.unmountHostNode(reconciler, node)
	-- end)

	-- it("should update Binding refs", function()
	-- 	local parent = Instance.new("Folder")
	-- 	local key = "Some Key"

	-- 	local refA = createRef()
	-- 	local refB = createRef()

	-- 	local element = createElement("Frame", {
	-- 		[Ref] = refA,
	-- 	})

	-- 	local node = reconciler.createVirtualNode(element, parent, key)

	-- 	RobloxRenderer.mountHostNode(reconciler, node)

	-- 	expect(#parent:GetChildren()).toEqual(1)

	-- 	local instance = parent:GetChildren()[1]

	-- 	expect(refA.current).toEqual(instance)
	-- 	expect(refB.current).never.toBeTruthy()

	-- 	local newElement = createElement("Frame", {
	-- 		[Ref] = refB,
	-- 	})

	-- 	RobloxRenderer.updateHostNode(reconciler, node, newElement)

	-- 	expect(refA.current).never.toBeTruthy()
	-- 	expect(refB.current).toEqual(instance)

	-- 	RobloxRenderer.unmountHostNode(reconciler, node)
	-- end)

	-- it("should call old function refs with nil and new function refs with a valid rbx", function()
	-- 	local parent = Instance.new("Folder")
	-- 	local key = "Some Key"

	-- 	local spyRefA = createSpy()
	-- 	local spyRefB = createSpy()

	-- 	local element = createElement("Frame", {
	-- 		[Ref] = spyRefA.value,
	-- 	})

	-- 	local node = reconciler.createVirtualNode(element, parent, key)

	-- 	RobloxRenderer.mountHostNode(reconciler, node)

	-- 	expect(#parent:GetChildren()).toEqual(1)

	-- 	local instance = parent:GetChildren()[1]

	-- 	expect(spyRefA.callCount).toEqual(1)
	-- 	spyRefA:assertCalledWith(instance)
	-- 	expect(spyRefB.callCount).toEqual(0)

	-- 	local newElement = createElement("Frame", {
	-- 		[Ref] = spyRefB.value,
	-- 	})

	-- 	RobloxRenderer.updateHostNode(reconciler, node, newElement)

	-- 	expect(spyRefA.callCount).toEqual(2)
	-- 	spyRefA:assertCalledWith(nil)
	-- 	expect(spyRefB.callCount).toEqual(1)
	-- 	spyRefB:assertCalledWith(instance)

	-- 	RobloxRenderer.unmountHostNode(reconciler, node)
	-- end)

	-- it("should not call function refs again if they didn't change", function()
	-- 	local parent = Instance.new("Folder")
	-- 	local key = "Some Key"

	-- 	local spyRef = createSpy()

	-- 	local element = createElement("Frame", {
	-- 		Size = UDim2.new(1, 0, 1, 0),
	-- 		[Ref] = spyRef.value,
	-- 	})

	-- 	local node = reconciler.createVirtualNode(element, parent, key)

	-- 	RobloxRenderer.mountHostNode(reconciler, node)

	-- 	expect(#parent:GetChildren()).toEqual(1)

	-- 	local instance = parent:GetChildren()[1]

	-- 	expect(spyRef.callCount).toEqual(1)
	-- 	spyRef:assertCalledWith(instance)

	-- 	local newElement = createElement("Frame", {
	-- 		Size = UDim2.new(0.5, 0, 0.5, 0),
	-- 		[Ref] = spyRef.value,
	-- 	})

	-- 	RobloxRenderer.updateHostNode(reconciler, node, newElement)

	-- 	-- Not called again
	-- 	expect(spyRef.callCount).toEqual(1)
	-- end)

	-- it("should throw if setting invalid instance properties", function()
	-- 	local configValues = {
	-- 		elementTracing = true,
	-- 	}

	-- 	GlobalConfig.scoped(configValues, function()
	-- 		local parent = Instance.new("Folder")
	-- 		local key = "Some Key"

	-- 		local firstElement = createElement("Frame")
	-- 		local secondElement = createElement("Frame", {
	-- 			Frob = 6,
	-- 		})

	-- 		local node = reconciler.createVirtualNode(firstElement, parent, key)
	-- 		RobloxRenderer.mountHostNode(reconciler, node)

	-- 		local success, message = pcall(RobloxRenderer.updateHostNode, reconciler, node, secondElement)
	-- 		assert(not success, "Expected call to fail")

	-- 		expect(string.find(message, "Frob")).toBeTruthy()
	-- 		expect(string.find(message, "Frame")).toBeTruthy()
	-- 		expect(string.find(message, "RobloxRenderer%.spec")).toBeTruthy()
	-- 	end)
	-- end)

	-- it("should delete instances when reconciling to nil children", function()
	-- 	local parent = Instance.new("Folder")
	-- 	local key = "Some Key"

	-- 	local element = createElement("Frame", {
	-- 		Size = UDim2.new(1, 0, 1, 0),
	-- 	}, {
	-- 		child = createElement("Frame"),
	-- 	})

	-- 	local node = reconciler.createVirtualNode(element, parent, key)

	-- 	RobloxRenderer.mountHostNode(reconciler, node)

	-- 	expect(#parent:GetChildren()).toEqual(1)

	-- 	local instance = parent:GetChildren()[1]
	-- 	expect(#instance:GetChildren()).toEqual(1)

	-- 	local newElement = createElement("Frame", {
	-- 		Size = UDim2.new(0.5, 0, 0.5, 0),
	-- 	})

	-- 	RobloxRenderer.updateHostNode(reconciler, node, newElement)
	-- 	expect(#instance:GetChildren()).toEqual(0)
	-- end)
end)

-- describe("removing instances", function()
-- 	it("should delete instances from the inside-out", function()
-- 		local parent = Instance.new("Folder")
-- 		local key = "Root"
-- 		local element = createElement("Folder", nil, {
-- 			Child = createElement("Folder", nil, {
-- 				Grandchild = createElement("Folder"),
-- 			}),
-- 		})

-- 		local node = reconciler.mountVirtualNode(element, parent, key)

-- 		expect(#parent:GetChildren()).toEqual(1)

-- 		local root = parent:GetChildren()[1]
-- 		expect(#root:GetChildren()).toEqual(1)

-- 		local child = root:GetChildren()[1]
-- 		expect(#child:GetChildren()).toEqual(1)

-- 		local grandchild = child:GetChildren()[1]

-- 		RobloxRenderer.unmountHostNode(reconciler, node)

-- 		expect(grandchild.Parent).toEqual(nil)
-- 		expect(child.Parent).toEqual(nil)
-- 		expect(root.Parent).toEqual(nil)
-- 	end)

-- 	it("should unsubscribe from any Bindings", function()
-- 		local parent = Instance.new("Folder")
-- 		local key = "Some Key"

-- 		local binding, update = Binding.create(10)
-- 		local element = createElement("IntValue", {
-- 			Value = binding,
-- 		})

-- 		local node = reconciler.createVirtualNode(element, parent, key)

-- 		RobloxRenderer.mountHostNode(reconciler, node)

-- 		local instance = parent:GetChildren()[1]

-- 		expect(instance.Value).toEqual(10)

-- 		RobloxRenderer.unmountHostNode(reconciler, node)
-- 		update(56)

-- 		expect(instance.Value).toEqual(10)
-- 	end)

-- 	it("should clear Binding refs", function()
-- 		local parent = Instance.new("Folder")
-- 		local key = "Some Key"

-- 		local ref = createRef()
-- 		local element = createElement("Frame", {
-- 			[Ref] = ref,
-- 		})

-- 		local node = reconciler.createVirtualNode(element, parent, key)

-- 		RobloxRenderer.mountHostNode(reconciler, node)

-- 		expect(ref.current).toBeTruthy()

-- 		RobloxRenderer.unmountHostNode(reconciler, node)

-- 		expect(ref.current).never.toBeTruthy()
-- 	end)

-- 	it("should call function refs with nil", function()
-- 		local parent = Instance.new("Folder")
-- 		local key = "Some Key"

-- 		local spyRef = createSpy()
-- 		local element = createElement("Frame", {
-- 			[Ref] = spyRef.value,
-- 		})

-- 		local node = reconciler.createVirtualNode(element, parent, key)

-- 		RobloxRenderer.mountHostNode(reconciler, node)

-- 		expect(spyRef.callCount).toEqual(1)

-- 		RobloxRenderer.unmountHostNode(reconciler, node)

-- 		expect(spyRef.callCount).toEqual(2)
-- 		spyRef:assertCalledWith(nil)
-- 	end)
-- end)

describe("Portals", function()
	it("should create and destroy instances as children of `target`", function()
		local target = Instance.new("Folder")

		local function FunctionComponent(props)
			return React.createElement("IntValue", {
				Name = "intValueOne",
				Value = props.value,
			})
		end

		local element = ReactRoblox.createPortal({
			React.createElement("Folder", { key = "1", Name = "folderOne" }),
			React.createElement("Folder", { key = "2", Name = "folderTwo" }),
			React.createElement(FunctionComponent, {
				key = "3",
				value = 42,
			}),
		}, target)

		reactRobloxRoot:render(element)
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#target:GetChildren()).toBe(3)
		jestExpect(target:FindFirstChild("folderOne")).toBeDefined()
		jestExpect(target:FindFirstChild("folderTwo")).toBeDefined()
		jestExpect(target:FindFirstChild("intValueOne")).toBeDefined()
		jestExpect(target:FindFirstChild("intValueOne").Value).toBe(42)

		reactRobloxRoot:unmount()
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#target:GetChildren()).toBe(0)
	end)

	it("should pass prop updates through to children", function()
		local target = Instance.new("Folder")

		local firstElement = ReactRoblox.createPortal({
			ChildValue = React.createElement("IntValue", { Value = 1 }),
		}, target)

		local secondElement = ReactRoblox.createPortal({
			ChildValue = React.createElement("IntValue", { Value = 2 }),
		}, target)

		reactRobloxRoot:render(firstElement)
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#target:GetChildren()).toBe(1)
		jestExpect(target:GetChildren()[1].Value).toBe(1)

		reactRobloxRoot:render(secondElement)
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#target:GetChildren()).toBe(1)
		jestExpect(target:GetChildren()[1].Value).toBe(2)

		reactRobloxRoot:unmount()
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#target:GetChildren()).toBe(0)
	end)

	-- ROBLOX TODO: Duplicated in ReactRobloxFiber. Should we delete here?
	it("should throw if `target` is nil", function()
		-- TODO: Relax this restriction?
		jestExpect(function()
			ReactRoblox.createPortal(React.createElement("IntValue", { Value = 1 }))
		end).toThrow()
	end)

	-- ROBLOX TODO: Duplicated in ReactRobloxFiber. Should we delete here?
	-- it("should throw if `target` is not a Roblox instance", function()
	-- 	local element = createElement(Portal, {
	-- 		target = {},
	-- 	})
	-- 	local hostParent = nil
	-- 	local hostKey = "Unleash the keys!"

	-- 	expect(function()
	-- 		reconciler.mountVirtualNode(element, hostParent, hostKey)
	-- 	end).to.throw()
	-- end)

	it("should recreate instances if `target` changes in an update", function()
		local firstTarget = Instance.new("Folder")
		local secondTarget = Instance.new("Folder")

		local firstElement = ReactRoblox.createPortal(
			React.createElement("IntValue", { Value = 1 }),
			firstTarget
		)

		local secondElement = ReactRoblox.createPortal(
			React.createElement("IntValue", { Value = 2 }),
			secondTarget
		)

		reactRobloxRoot:render(firstElement)
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#firstTarget:GetChildren()).toBe(1)
		jestExpect(#secondTarget:GetChildren()).toBe(0)
		jestExpect(firstTarget:GetChildren()[1].Value).toBe(1)

		reactRobloxRoot:render(secondElement)
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#firstTarget:GetChildren()).toBe(0)
		jestExpect(#secondTarget:GetChildren()).toBe(1)
		jestExpect(secondTarget:GetChildren()[1].Value).toBe(2)

		reactRobloxRoot:unmount()
		Scheduler.unstable_flushAllWithoutAsserting()

		jestExpect(#firstTarget:GetChildren()).toBe(0)
		jestExpect(#secondTarget:GetChildren()).toBe(0)
	end)
end)

-- describe("Fragments", function()
-- 	it("should parent the fragment's elements into the fragment's parent", function()
-- 		local hostParent = Instance.new("Folder")

-- 		local fragment = createFragment({
-- 			key = createElement("IntValue", {
-- 				Value = 1,
-- 			}),
-- 			key2 = createElement("IntValue", {
-- 				Value = 2,
-- 			}),
-- 		})

-- 		local node = reconciler.mountVirtualNode(fragment, hostParent, "test")

-- 		expect(hostParent:FindFirstChild("key")).toBeTruthy()
-- 		expect(hostParent.key.ClassName).toEqual("IntValue")
-- 		expect(hostParent.key.Value).toEqual(1)

-- 		expect(hostParent:FindFirstChild("key2")).toBeTruthy()
-- 		expect(hostParent.key2.ClassName).toEqual("IntValue")
-- 		expect(hostParent.key2.Value).toEqual(2)

-- 		reconciler.unmountVirtualNode(node)

-- 		expect(#hostParent:GetChildren()).toEqual(0)
-- 	end)

-- 	it("should allow sibling fragment to have common keys", function()
-- 		local hostParent = Instance.new("Folder")
-- 		local hostKey = "Test"

-- 		local function parent(props)
-- 			return createElement("IntValue", {}, {
-- 				fragmentA = createFragment({
-- 					key = createElement("StringValue", {
-- 						Value = "A",
-- 					}),
-- 					key2 = createElement("StringValue", {
-- 						Value = "B",
-- 					}),
-- 				}),
-- 				fragmentB = createFragment({
-- 					key = createElement("StringValue", {
-- 						Value = "C",
-- 					}),
-- 					key2 = createElement("StringValue", {
-- 						Value = "D",
-- 					}),
-- 				}),
-- 			})
-- 		end

-- 		local node = reconciler.mountVirtualNode(createElement(parent), hostParent, hostKey)
-- 		local parentChildren = hostParent[hostKey]:GetChildren()

-- 		expect(#parentChildren).toEqual(4)

-- 		local childValues = {}

-- 		for _, child in parentChildren do
-- 			expect(child.ClassName).toEqual("StringValue")
-- 			childValues[child.Value] = 1 + (childValues[child.Value] or 0)
-- 		end

-- 		-- check if the StringValues have not collided
-- 		expect(childValues.A).toEqual(1)
-- 		expect(childValues.B).toEqual(1)
-- 		expect(childValues.C).toEqual(1)
-- 		expect(childValues.D).toEqual(1)

-- 		reconciler.unmountVirtualNode(node)

-- 		expect(#hostParent:GetChildren()).toEqual(0)
-- 	end)

-- 	it("should render nested fragments", function()
-- 		local hostParent = Instance.new("Folder")

-- 		local fragment = createFragment({
-- 			key = createFragment({
-- 				TheValue = createElement("IntValue", {
-- 					Value = 1,
-- 				}),
-- 				TheOtherValue = createElement("IntValue", {
-- 					Value = 2,
-- 				})
-- 			})
-- 		})

-- 		local node = reconciler.mountVirtualNode(fragment, hostParent, "Test")

-- 		expect(hostParent:FindFirstChild("TheValue")).toBeTruthy()
-- 		expect(hostParent.TheValue.ClassName).toEqual("IntValue")
-- 		expect(hostParent.TheValue.Value).toEqual(1)

-- 		expect(hostParent:FindFirstChild("TheOtherValue")).toBeTruthy()
-- 		expect(hostParent.TheOtherValue.ClassName).toEqual("IntValue")
-- 		expect(hostParent.TheOtherValue.Value).toEqual(2)

-- 		reconciler.unmountVirtualNode(node)

-- 		expect(#hostParent:GetChildren()).toEqual(0)
-- 	end)

-- 	it("should not add any instances if the fragment is empty", function()
-- 		local hostParent = Instance.new("Folder")

-- 		local node = reconciler.mountVirtualNode(createFragment({}), hostParent, "test")

-- 		expect(#hostParent:GetChildren()).toEqual(0)

-- 		reconciler.unmountVirtualNode(node)

-- 		expect(#hostParent:GetChildren()).toEqual(0)
-- 	end)
-- end)

describe("Context", function()
	-- it("should pass context values through Roblox host nodes", function()
	-- 	local Consumer = Component:extend("Consumer")

	-- 	local capturedContext
	-- 	function Consumer:init()
	-- 		capturedContext = {
	-- 			hello = self:__getContext("hello")
	-- 		}
	-- 	end

	-- 	function Consumer:render()
	-- 	end

	-- 	local element = createElement("Folder", nil, {
	-- 		Consumer = createElement(Consumer)
	-- 	})
	-- 	local hostParent = nil
	-- 	local hostKey = "Context Test"
	-- 	local context = {
	-- 		hello = "world",
	-- 	}
	-- 	local node = reconciler.mountVirtualNode(element, hostParent, hostKey, context)

	-- 	expect(capturedContext).never.toEqual(context)
	-- 	assertDeepEqual(capturedContext, context)

	-- 	reconciler.unmountVirtualNode(node)
	-- end)

	it("should pass context values through portal nodes", function()
		local target = Instance.new("Folder")
		local Context = React.createContext(1)

		local function App(props)
			return React.createElement(Context.Provider, {
				value = props.value,
			}, {
				Portal = ReactRoblox.createPortal({
					Consumer = React.createElement(Context.Consumer, nil, function(value)
						return React.createElement(
							"TextLabel",
							{ Text = "Result: " .. tostring(value) }
						)
					end),
				}, target),
			})
		end

		reactRobloxRoot:render(React.createElement(App, { value = 2 }))
		Scheduler.unstable_flushAllWithoutAsserting()
		jestExpect(#target:GetChildren()).toBe(1)
		jestExpect(target:GetChildren()[1].Text).toBe("Result: 2")

		reactRobloxRoot:render(React.createElement(App, { value = 3 }))
		Scheduler.unstable_flushAllWithoutAsserting()
		jestExpect(#target:GetChildren()).toBe(1)
		jestExpect(target:GetChildren()[1].Text).toBe("Result: 3")
	end)
end)

-- describe("Legacy context", function()
-- 	it("should pass context values through Roblox host nodes", function()
-- 		local Consumer = Component:extend("Consumer")

-- 		local capturedContext
-- 		function Consumer:init()
-- 			capturedContext = self._context
-- 		end

-- 		function Consumer:render()
-- 		end

-- 		local element = createElement("Folder", nil, {
-- 			Consumer = createElement(Consumer)
-- 		})
-- 		local hostParent = nil
-- 		local hostKey = "Context Test"
-- 		local context = {
-- 			hello = "world",
-- 		}
-- 		local node = reconciler.mountVirtualNode(element, hostParent, hostKey, nil, context)

-- 		expect(capturedContext).never.toEqual(context)
-- 		assertDeepEqual(capturedContext, context)

-- 		reconciler.unmountVirtualNode(node)
-- 	end)

-- 	it("should pass context values through portal nodes", function()
-- 		local target = Instance.new("Folder")

-- 		local Provider = Component:extend("Provider")

-- 		function Provider:init()
-- 			self._context.foo = "bar"
-- 		end

-- 		function Provider:render()
-- 			return createElement("Folder", nil, self.props[Children])
-- 		end

-- 		local Consumer = Component:extend("Consumer")

-- 		local capturedContext
-- 		function Consumer:init()
-- 			capturedContext = self._context
-- 		end

-- 		function Consumer:render()
-- 			return nil
-- 		end

-- 		local element = createElement(Provider, nil, {
-- 			Portal = createElement(Portal, {
-- 				target = target,
-- 			}, {
-- 				Consumer = createElement(Consumer),
-- 			})
-- 		})
-- 		local hostParent = nil
-- 		local hostKey = "Some Key"
-- 		reconciler.mountVirtualNode(element, hostParent, hostKey)

-- 		assertDeepEqual(capturedContext, {
-- 			foo = "bar"
-- 		})
-- 	end)
-- end)
