return function()local Z=require(script.Parent)local X=game:GetService("\067\111\108\108\101\099\116\105\111\110\083\101\114\118\105\099\101")local v=game:GetService("\082\117\110\083\101\114\118\105\099\101")local o="\095\095\075\110\105\116\084\101\115\116\067\111\109\112\111\110\101\110\116\095\095"local h local function g()local Z=Instance.new("\070\111\108\100\101\114")X:AddTag(Z,o)Z.Name="\067\111\109\112\111\110\101\110\116\084\101\115\116"Z.Archivable=false Z.Parent=h return Z end local b={}function b.ShouldConstruct(Z)return true end function b.Constructing(Z)Z.Data="\097"Z.DidHeartbeat=false Z.DidStepped=false Z.DidRenderStepped=false end function b.Constructed(Z)Z.Data..="\099"end function b.Starting(Z)Z.Data..="\100"end function b.Started(Z)Z.Data..="\102"end function b.Stopping(Z)Z.Data..="\103"end function b.Stopped(Z)Z.Data..="\105"end local S=Z.new({Tag=o;Ancestors={workspace,game:GetService("\076\105\103\104\116\105\110\103")},Extensions={b}})local i=Z.new({Tag=o})function i.GetData(Z)return true end function S.Construct(Z)Z.Data..="\098"end function S.Start(Z)Z.Another=Z:GetComponent(i)Z.Data..="\101"end function S.Stop(Z)Z.Data..="\104"end function S.HeartbeatUpdate(X,Z)X.DidHeartbeat=true end function S.SteppedUpdate(X,Z)X.DidStepped=true end function S.RenderSteppedUpdate(X,Z)X.DidRenderStepped=true end beforeAll(function()h=Instance.new("\070\111\108\100\101\114")h.Name="\075\110\105\116\067\111\109\112\111\110\101\110\116\084\101\115\116"h.Archivable=false h.Parent=workspace end)afterEach(function()h:ClearAllChildren()end)afterAll(function()h:Destroy()S:Destroy()end)describe("\067\111\109\112\111\110\101\110\116",function()it("\115\104\111\117\108\100 \099\097\112\116\117\114\101 \115\116\097\114\116 \097\110\100 \115\116\111\112 \101\118\101\110\116\115",function()local Z=0 local X=0 local v=S.Started:Connect(function()Z+=1 end)local o=S.Stopped:Connect(function()X+=1 end)local h=g()task.wait()h:Destroy()task.wait()v:Disconnect()o:Disconnect();(expect(Z)).to.equal(1);(expect(X)).to.equal(1)end)it("\115\104\111\117\108\100 \098\101 \097\098\108\101 \116\111 \103\101\116 \099\111\109\112\111\110\101\110\116 \102\114\111\109 \116\104\101 \105\110\115\116\097\110\099\101",function()local Z=g()task.wait()local X=S:FromInstance(Z);(expect(X)).to.be.ok()end)it("\115\104\111\117\108\100 \098\101 \097\098\108\101 \116\111 \103\101\116 \097\108\108 \099\111\109\112\111\110\101\110\116 \105\110\115\116\097\110\099\101\115 \101\120\105\115\116\105\110\103",function()local Z=3 local X=table.create(Z)for Z=1,Z,1 do local v=g()X[Z]=v end task.wait()local v=S:GetAll();(expect(v)).to.be.a("\116\097\098\108\101");(expect(#v)).to.equal(Z)for Z,v in ipairs(v)do(expect(table.find(X,v.Instance))).to.be.ok()end end)it("\115\104\111\117\108\100 \099\097\108\108 \108\105\102\101\099\121\099\108\101 \109\101\116\104\111\100\115 \097\110\100 \101\120\116\101\110\115\105\111\110 \102\117\110\099\116\105\111\110\115",function()local Z=g()task.wait(.2)local X=S:FromInstance(Z);(expect(X)).to.be.ok();(expect(X.Data)).to.equal("\097\098\099\100\101\102");(expect(X.DidHeartbeat)).to.equal(true);(expect(X.DidStepped)).to.equal(v:IsRunning());(expect(X.DidRenderStepped)).to.never.equal(true)Z:Destroy()task.wait();(expect(X.Data)).to.equal("\097\098\099\100\101\102\103\104\105")end)it("\115\104\111\117\108\100 \103\101\116 \097\110\111\116\104\101\114 \099\111\109\112\111\110\101\110\116 \108\105\110\107\101\100 \116\111 \116\104\101 \115\097\109\101 \105\110\115\116\097\110\099\101",function()local Z=g()task.wait()local X=S:FromInstance(Z);(expect(X)).to.be.ok();(expect(X.Another)).to.be.ok();(expect(X.Another:GetData())).to.equal(true)end)it("\115\104\111\117\108\100 \117\115\101 \101\120\116\101\110\115\105\111\110 \116\111 \100\101\099\105\100\101 \119\104\101\116\104\101\114 \111\114 \110\111\116 \116\111 \099\111\110\115\116\114\117\099\116",function()local X={c=true}function X.ShouldConstruct(Z)return X.c end local v={c=true}function v.ShouldConstruct(Z)return v.c end local h={c=true}function h.ShouldConstruct(Z)return h.c end local b=Z.new({Tag=o;Extensions={X}})local S=Z.new({Tag=o;Extensions={X;v}})local i=Z.new({Tag=o,Extensions={X;v,h}})local function w(Z,o,g)X.c=Z v.c=o h.c=g end local function K(Z,X,v)local o=X:FromInstance(Z)if v then(expect(o)).to.be.ok()else(expect(o)).to.never.be.ok()end end local function P(Z,X,v)local o=g()task.wait()K(o,b,Z)K(o,S,X)K(o,i,v)end w(true,true,true)P(true,true,true)w(false,false,false)P(false,false,false)w(true,false,true)P(true,false,false)w(false,false,true)P(false,false,false)end)it("\115\104\111\117\108\100 \100\101\099\105\100\101 \119\104\101\116\104\101\114 \111\114 \110\111\116 \116\111 \117\115\101 \101\120\116\101\110\100",function()local X={extend=true}function X.ShouldExtend(Z)return X.extend end function X.Constructing(Z)Z.E1=true end local v={extend=true}function v.ShouldExtend(Z)return v.extend end function v.Constructing(Z)Z.E2=true end local h=Z.new({Tag=o;Extensions={X,v}})local function b(Z,o)X.extend=Z v.extend=o local b=g()task.wait()local S=h:FromInstance(b);(expect(S)).to.be.ok()if Z then(expect(S.E1)).to.equal(true)else(expect(S.E1)).to.never.be.ok()end if o then(expect(S.E2)).to.equal(true)else(expect(S.E2)).to.never.be.ok()end end b(true,true)b(false,false)b(true,false)b(false,true)end)it("\115\104\111\117\108\100 \097\108\108\111\119 \121\105\101\108\100\105\110\103 \119\105\116\104\105\110 \099\111\110\115\116\114\117\099\116",function()local v="\067\117\115\116\111\109\084\097\103"local o=Z.new({Tag=v})local h=0 function o.Construct(Z)h+=1 task.wait(.5)end local g=Instance.new("\080\097\114\116")g.Anchored=true g.Parent=game:GetService("\082\101\112\108\105\099\097\116\101\100\083\116\111\114\097\103\101")X:AddTag(g,v)local b=g:Clone()b.Parent=workspace task.wait(.6);(expect(h)).to.equal(1)g:Destroy()b:Destroy()end)it("\115\104\111\117\108\100 \119\097\105\116 \102\111\114 \105\110\115\116\097\110\099\101",function()local Z=Instance.new("\080\097\114\116")Z.Anchored=true Z.Parent=workspace task.delay(.1,function()X:AddTag(Z,o)end)local v,h=((S:WaitForInstance(Z)):timeout(1)):await();(expect(v)).to.equal(true);(expect(h)).to.be.a("\116\097\098\108\101");(expect(h.Instance)).to.equal(Z)Z:Destroy()end)end)end
return function()
	local Component = require(script.Parent)

	local CollectionService = game:GetService("CollectionService")
	local RunService = game:GetService("RunService")

	local TAG = "__KnitTestComponent__"

	local taggedInstanceFolder

	local function CreateTaggedInstance()
		local folder = Instance.new("Folder")
		CollectionService:AddTag(folder, TAG)
		folder.Name = "ComponentTest"
		folder.Archivable = false
		folder.Parent = taggedInstanceFolder
		return folder
	end

	local ExtensionTest = {}
	function ExtensionTest.ShouldConstruct(_component)
		return true
	end
	function ExtensionTest.Constructing(component)
		component.Data = "a"
		component.DidHeartbeat = false
		component.DidStepped = false
		component.DidRenderStepped = false
	end
	function ExtensionTest.Constructed(component)
		component.Data ..= "c"
	end
	function ExtensionTest.Starting(component)
		component.Data ..= "d"
	end
	function ExtensionTest.Started(component)
		component.Data ..= "f"
	end
	function ExtensionTest.Stopping(component)
		component.Data ..= "g"
	end
	function ExtensionTest.Stopped(component)
		component.Data ..= "i"
	end

	local TestComponentMain = Component.new({
		Tag = TAG,
		Ancestors = { workspace, game:GetService("Lighting") },
		Extensions = { ExtensionTest },
	})

	local AnotherComponent = Component.new({ Tag = TAG })
	function AnotherComponent:GetData()
		return true
	end

	function TestComponentMain:Construct()
		self.Data ..= "b"
	end

	function TestComponentMain:Start()
		self.Another = self:GetComponent(AnotherComponent)
		self.Data ..= "e"
	end

	function TestComponentMain:Stop()
		self.Data ..= "h"
	end

	function TestComponentMain:HeartbeatUpdate(_dt)
		self.DidHeartbeat = true
	end

	function TestComponentMain:SteppedUpdate(_dt)
		self.DidStepped = true
	end

	function TestComponentMain:RenderSteppedUpdate(_dt)
		self.DidRenderStepped = true
	end

	beforeAll(function()
		taggedInstanceFolder = Instance.new("Folder")
		taggedInstanceFolder.Name = "KnitComponentTest"
		taggedInstanceFolder.Archivable = false
		taggedInstanceFolder.Parent = workspace
	end)

	afterEach(function()
		taggedInstanceFolder:ClearAllChildren()
	end)

	afterAll(function()
		taggedInstanceFolder:Destroy()
		TestComponentMain:Destroy()
	end)

	describe("Component", function()
		it("should capture start and stop events", function()
			local didStart = 0
			local didStop = 0
			local started = TestComponentMain.Started:Connect(function()
				didStart += 1
			end)
			local stopped = TestComponentMain.Stopped:Connect(function()
				didStop += 1
			end)
			local instance = CreateTaggedInstance()
			task.wait()
			instance:Destroy()
			task.wait()
			started:Disconnect()
			stopped:Disconnect()
			expect(didStart).to.equal(1)
			expect(didStop).to.equal(1)
		end)

		it("should be able to get component from the instance", function()
			local instance = CreateTaggedInstance()
			task.wait()
			local component = TestComponentMain:FromInstance(instance)
			expect(component).to.be.ok()
		end)

		it("should be able to get all component instances existing", function()
			local numComponents = 3
			local instances = table.create(numComponents)
			for i = 1, numComponents do
				local instance = CreateTaggedInstance()
				instances[i] = instance
			end
			task.wait()
			local components = TestComponentMain:GetAll()
			expect(components).to.be.a("table")
			expect(#components).to.equal(numComponents)
			for _, c in ipairs(components) do
				expect(table.find(instances, c.Instance)).to.be.ok()
			end
		end)

		it("should call lifecycle methods and extension functions", function()
			local instance = CreateTaggedInstance()
			task.wait(0.2)
			local component = TestComponentMain:FromInstance(instance)
			expect(component).to.be.ok()
			expect(component.Data).to.equal("abcdef")
			expect(component.DidHeartbeat).to.equal(true)
			expect(component.DidStepped).to.equal(RunService:IsRunning())
			expect(component.DidRenderStepped).to.never.equal(true)
			instance:Destroy()
			task.wait()
			expect(component.Data).to.equal("abcdefghi")
		end)

		it("should get another component linked to the same instance", function()
			local instance = CreateTaggedInstance()
			task.wait()
			local component = TestComponentMain:FromInstance(instance)
			expect(component).to.be.ok()
			expect(component.Another).to.be.ok()
			expect(component.Another:GetData()).to.equal(true)
		end)

		it("should use extension to decide whether or not to construct", function()
			local e1 = { c = true }
			function e1.ShouldConstruct(_component)
				return e1.c
			end

			local e2 = { c = true }
			function e2.ShouldConstruct(_component)
				return e2.c
			end

			local e3 = { c = true }
			function e3.ShouldConstruct(_component)
				return e3.c
			end

			local c1 = Component.new({ Tag = TAG, Extensions = { e1 } })
			local c2 = Component.new({ Tag = TAG, Extensions = { e1, e2 } })
			local c3 = Component.new({ Tag = TAG, Extensions = { e1, e2, e3 } })

			local function SetE(a, b, c)
				e1.c = a
				e2.c = b
				e3.c = c
			end

			local function Check(inst, comp, shouldExist)
				local c = comp:FromInstance(inst)
				if shouldExist then
					expect(c).to.be.ok()
				else
					expect(c).to.never.be.ok()
				end
			end

			local function CreateAndCheckAll(a, b, c)
				local instance = CreateTaggedInstance()
				task.wait()
				Check(instance, c1, a)
				Check(instance, c2, b)
				Check(instance, c3, c)
			end

			-- All green:
			SetE(true, true, true)
			CreateAndCheckAll(true, true, true)

			-- All red:
			SetE(false, false, false)
			CreateAndCheckAll(false, false, false)

			-- One red:
			SetE(true, false, true)
			CreateAndCheckAll(true, false, false)

			-- One green:
			SetE(false, false, true)
			CreateAndCheckAll(false, false, false)
		end)

		it("should decide whether or not to use extend", function()
			local e1 = { extend = true }
			function e1.ShouldExtend(_component)
				return e1.extend
			end
			function e1.Constructing(component)
				component.E1 = true
			end

			local e2 = { extend = true }
			function e2.ShouldExtend(_component)
				return e2.extend
			end
			function e2.Constructing(component)
				component.E2 = true
			end

			local TestComponent = Component.new({ Tag = TAG, Extensions = { e1, e2 } })

			local function SetAndCheck(ex1, ex2)
				e1.extend = ex1
				e2.extend = ex2
				local instance = CreateTaggedInstance()
				task.wait()
				local component = TestComponent:FromInstance(instance)
				expect(component).to.be.ok()
				if ex1 then
					expect(component.E1).to.equal(true)
				else
					expect(component.E1).to.never.be.ok()
				end
				if ex2 then
					expect(component.E2).to.equal(true)
				else
					expect(component.E2).to.never.be.ok()
				end
			end

			SetAndCheck(true, true)
			SetAndCheck(false, false)
			SetAndCheck(true, false)
			SetAndCheck(false, true)
		end)

		it("should allow yielding within construct", function()
			local CUSTOM_TAG = "CustomTag"

			local TestComponent = Component.new({ Tag = CUSTOM_TAG })

			local numConstruct = 0

			function TestComponent:Construct()
				numConstruct += 1
				task.wait(0.5)
			end

			local p = Instance.new("Part")
			p.Anchored = true
			p.Parent = game:GetService("ReplicatedStorage")
			CollectionService:AddTag(p, CUSTOM_TAG)
			local newP = p:Clone()
			newP.Parent = workspace

			task.wait(0.6)

			expect(numConstruct).to.equal(1)
			p:Destroy()
			newP:Destroy()
		end)

		it("should wait for instance", function()
			local p = Instance.new("Part")
			p.Anchored = true
			p.Parent = workspace
			task.delay(0.1, function()
				CollectionService:AddTag(p, TAG)
			end)
			local success, c = TestComponentMain:WaitForInstance(p):timeout(1):await()
			expect(success).to.equal(true)
			expect(c).to.be.a("table")
			expect(c.Instance).to.equal(p)
			p:Destroy()
		end)
	end)
end
