local G=require("\109\111\115\101\115")local z=require("\108\117\097\115\115\101\114\116\046\109\097\116\099\104")local f=(require("\115\117\112\112\111\114\116\046\102\097\099\116\111\114\121")).create local u=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\109\098\111\115\046\067\111\109\098\111")local p=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\109\098\111\115\046\067\111\109\098\111\115")local q=require("\105\110\118\111\107\097\116\105\111\110\046\100\111\116\097\050\046\116\097\108\101\110\116\115")local c=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\109\098\111\115\046\104\101\114\111")local w=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\109\098\111\115\046\099\111\109\109\117\110\105\099\097\116\105\111\110")local g=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\109\098\111\115\046\115\111\117\110\100")local Q=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\109\098\111\115\046\070\114\101\101\115\116\121\108\101\067\111\109\098\111")local T=require("\105\110\118\111\107\097\116\105\111\110\046\100\111\116\097\050\046\068\097\109\097\103\101\073\110\115\116\097\110\099\101")local j=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\110\115\116\046\117\110\105\116\115")local s=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\110\115\116\046\099\111\109\098\111\115")local M=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\110\115\116\046\105\110\118\111\107\101\114")local X=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\110\115\116\046\110\101\116\095\116\097\098\108\101")describe("\067\111\109\098\111\115",function()local P local t={}local v=f("\101\110\116\105\116\121")local i=f("\100\111\116\097\095\104\101\114\111\095\105\110\118\111\107\101\114")local h=f("\100\111\116\097\095\112\108\097\121\101\114",{id=13,hero=i})setup(function()stub.new(Entities,"\070\105\110\100\066\121\078\097\109\101",function(G,z,f)return f==j.DUMMY_TARGET_SPAWN and v or nil end)end)teardown(function()Entities.FindByName:revert()end)before_each(function()stub.new(t,"\083\101\116")P=p(t)end)after_each(function()t.Set:clear()end)describe("\099\111\110\115\116\114\117\099\116\111\114",function()it("\108\111\097\100\115 \097\110\100 \116\114\097\110\115\102\111\114\109\115 \115\112\101\099\115",function()assert.equal(G.count(s),G.count(P.specs))for G,z in pairs(P.specs)do assert.is_table(z)assert.not_nil(z.id)for G,z in pairs(z.sequence)do assert.is_table(z)assert.not_nil(z.id)end end end)it("\115\101\116\115 \078\101\116\084\097\098\108\101 \107\101\121 \119\105\116\104 \108\111\097\100\101\100 \115\112\101\099\115",function()(assert.stub(t.Set)).was.self.called_with(X.MAIN.KEYS.COMBOS,P.specs)end)end)describe("\035\079\110\065\098\105\108\105\116\121\085\115\101\100",function()before_each(function()stub.new(P,"\067\097\112\116\117\114\101\065\098\105\108\105\116\121")stub.new(P,"\080\114\111\103\114\101\115\115")end)after_each(function()P.CaptureAbility:revert()P.Progress:revert()end)local G=f("\104\101\114\111\095\105\110\118\111\107\101\114")describe("\119\105\116\104 \114\101\108\101\118\097\110\116 \097\098\105\108\105\116\121",function()local z=f("\097\098\105\108\105\116\121",{name=M.ABILITY_SUN_STRIKE})it("\104\097\110\100\108\101\115 \097\098\105\108\105\116\121 \099\097\112\116\117\114\101 \097\110\100 \099\111\109\098\111 \112\114\111\103\114\101\115\115",function()P:OnAbilityUsed(h,G,z);(assert.stub(P.CaptureAbility)).was.self.called_with(h,z);(assert.stub(P.Progress)).was.self.called_with(h,z)end)end)describe("\119\105\116\104 \105\103\110\111\114\101\100 \097\098\105\108\105\116\121",function()local z=f("\097\098\105\108\105\116\121",{name="\105\116\101\109\095\112\104\097\115\101\095\098\111\111\116\115"})it("\105\115 \097 \110\111\111\112",function()P:OnAbilityUsed(h,G,z);(assert.stub(P.CaptureAbility)).was_not.called();(assert.stub(P.Progress)).was_not.called()end)end)end)describe("\035\079\110\069\110\116\105\116\121\072\117\114\116",function()before_each(function()stub.new(P,"\080\114\111\103\114\101\115\115\068\097\109\097\103\101")end)after_each(function()P.ProgressDamage:revert()end)local G=f("\117\110\105\116",{name=j.DUMMY_TARGET})describe("\119\105\116\104 \112\108\097\121\101\114-\111\119\110\101\100 \097\116\116\097\099\107\101\114",function()local z=f("\104\101\114\111\095\105\110\118\111\107\101\114",{playerOwner=h})local u=f("\097\098\105\108\105\116\121",{name=M.ABILITY_SUN_STRIKE})it("\104\097\110\100\108\101\115 \099\111\109\098\111 \100\097\109\097\103\101 \112\114\111\103\114\101\115\115",function()local f=T(G,123.4,z,u)local p=f:AttackerPlayerOwner()P:OnEntityHurt(f);(assert.stub(P.ProgressDamage)).was.self.called_with(p,f)end)end)describe("\119\105\116\104 \110\111\110-\112\108\097\121\101\114 \097\116\116\097\099\107\101\114",function()local z=f("\117\110\105\116",{name="\110\112\099\095\100\111\116\097\095\104\101\114\111\095\097\120\101"})local u=f("\097\098\105\108\105\116\121",{name="\105\116\101\109\095\100\097\103\111\110\095\053"})it("\105\115 \097 \110\111\111\112",function()local f=T(G,123.4,z,u)P:OnEntityHurt(f);(assert.stub(P.ProgressDamage)).was_not.called()end)end)end)describe("\035\079\110\073\116\101\109\080\117\114\099\104\097\115\101\100",function()local G={item="\105\116\101\109\095\098\108\105\110\107";cost=1234.5}describe("\119\104\101\110 \110\111 \099\111\109\098\111 \105\115 \097\099\116\105\118\101",function()it("\105\115 \097 \110\111\111\112",function()i:SetGold(0,true)i:SetGold(0,false)assert.equal(0,i:GetGold())P:OnItemPurchased(h,G)assert.equal(0,i:GetGold())end)end)describe("\119\104\101\110 \097 \114\101\103\117\108\097\114 \099\111\109\098\111 \105\115 \097\099\116\105\118\101",function()it("\105\115 \097 \110\111\111\112",function()P:Start(h,P:Create(1))i:SetGold(0,true)i:SetGold(0,false)assert.equal(0,i:GetGold())P:OnItemPurchased(h,G)assert.equal(0,i:GetGold())end)end)describe("\119\104\101\110 \102\114\101\101\115\116\121\108\101 \099\111\109\098\111 \105\115 \097\099\116\105\118\101",function()it("\114\101\102\117\110\100\115 \116\104\101 \112\117\114\099\104\097\115\101",function()P:Start(h,P:Create(Q.COMBO_ID))i:SetGold(0,true)i:SetGold(0,false)assert.equal(0,i:GetGold())P:OnItemPurchased(h,G)assert.equal(1234.5,i:GetGold())end)end)end)describe("\035\067\114\101\097\116\101",function()describe("\119\105\116\104 \097\110 \105\110\118\097\108\105\100 \105\100",function()it("\116\104\114\111\119\115 \097\110 \101\114\114\111\114",function()local z=G.bindn(P.Create,P,-1)assert.error(z,"\067\111\109\098\111 \"-\049\" \110\111\116 \102\111\117\110\100")end)end)describe("\119\105\116\104 \102\114\101\101\115\116\121\108\101 \105\100",function()it("\099\114\101\097\116\101\115 \097 \102\114\101\101\115\116\121\108\101 \099\111\109\098\111",function()local G=P:Create(Q.COMBO_ID)assert.not_nil(G)assert.is_true(Q:class_of(G))end)end)describe("\119\105\116\104 \097 \118\097\108\105\100 \105\100",function()it("\099\114\101\097\116\101\115 \097 \099\111\109\098\111",function()local G=P:Create(13)assert.not_nil(G)assert.is_true(u:class_of(G))end)end)end)describe("\035\083\116\097\114\116",function()before_each(function()spy.on(c,"\115\101\116\117\112")spy.on(c,"\116\101\097\114\100\111\119\110")spy.on(g,"\111\110\068\117\109\109\121\067\114\101\097\116\101")spy.on(g,"\111\110\067\111\109\098\111\083\116\097\114\116")spy.on(g,"\111\110\067\111\109\098\111\083\116\111\112")spy.on(w,"\115\101\110\100\083\116\097\114\116\101\100")spy.on(w,"\115\101\110\100\083\116\111\112\112\101\100")end)after_each(function()c.setup:revert()c.teardown:revert()g.onDummyCreate:revert()g.onComboStart:revert()g.onComboStop:revert()w.sendStarted:revert()w.sendStopped:revert()end)describe("\119\104\101\110 \110\111 \099\111\109\098\111 \105\115 \097\099\116\105\118\101",function()it("\115\116\097\114\116\115 \116\104\101 \099\111\109\098\111",function()assert.is_nil(P.state[h].combo)P:Start(h,P:Create(13))local G=P.state[h].combo local f=P.state[h].dummy assert.not_nil(G)assert.not_nil(f)assert.equal(13,G.id)assert.is_true(f:IsAlive());(assert.spy(c.teardown)).was.called_with(z.is_ref(h),{hardReset=true});(assert.spy(c.setup)).was.called_with(z.is_ref(h),z.is_ref(G));(assert.spy(g.onDummyCreate)).was.called_with(z.is_ref(f));(assert.spy(g.onComboStop)).was_not.called();(assert.spy(g.onComboStart)).was.called_with(z.is_ref(h));(assert.spy(w.sendStarted)).was.called_with(z.is_ref(h),z.is_ref(G));(assert.spy(w.sendStopped)).was_not.called()end)end)describe("\119\104\101\110 \116\104\101 \115\097\109\101 \099\111\109\098\111 \105\115 \097\108\114\101\097\100\121 \097\099\116\105\118\101",function()it("\114\101\115\116\097\114\116\115 \116\104\101 \099\111\109\098\111",function()P:Start(h,P:Create(13))local G=P.state[h].combo assert.not_nil(G)assert.equal(13,G.id)c.setup:clear()c.teardown:clear()g.onDummyCreate:clear()g.onComboStart:clear()g.onComboStop:clear()w.sendStarted:clear()w.sendStopped:clear()P:Start(h,P:Create(13))local f=P.state[h].combo local u=P.state[h].dummy assert.not_nil(f)assert.not_nil(u)assert.not_equal(f,G)assert.equal(13,f.id)assert.is_true(u:IsAlive());(assert.spy(c.teardown)).was.called_with(z.is_ref(h),{hardReset=false});(assert.spy(c.setup)).was.called_with(z.is_ref(h),z.is_ref(f));(assert.spy(g.onDummyCreate)).was_not.called_with(z.is_ref(u));(assert.spy(g.onComboStop)).was_not.called();(assert.spy(g.onComboStart)).was.called_with(z.is_ref(h));(assert.spy(w.sendStarted)).was.called_with(z.is_ref(h),z.is_ref(f));(assert.spy(w.sendStopped)).was_not.called()end)end)describe("\119\104\101\110 \097 \100\105\102\102\101\114\101\110\116 \099\111\109\098\111 \105\115 \097\108\114\101\097\100\121 \097\099\116\105\118\101",function()it("\115\116\111\112\115 \116\104\101 \112\114\101\118\105\111\117\115 \099\111\109\098\111 \097\110\100 \115\116\097\114\116\115 \116\104\101 \110\101\119 \099\111\109\098\111",function()P:Start(h,P:Create(13))local G=P.state[h].combo assert.not_nil(G)assert.equal(13,G.id)c.setup:clear()c.teardown:clear()g.onDummyCreate:clear()g.onComboStart:clear()g.onComboStop:clear()w.sendStarted:clear()w.sendStopped:clear()P:Start(h,P:Create(3))local f=P.state[h].combo local u=P.state[h].dummy assert.not_nil(f)assert.not_nil(u)assert.not_equal(f,G)assert.equal(3,f.id)assert.is_true(u:IsAlive());(assert.spy(c.teardown)).was.called_with(z.is_ref(h),{hardReset=true});(assert.spy(c.setup)).was.called_with(z.is_ref(h),z.is_ref(f));(assert.spy(g.onDummyCreate)).was_not.called_with(z.is_ref(u));(assert.spy(g.onComboStop)).was.called_with(z.is_ref(h));(assert.spy(g.onComboStart)).was.called_with(z.is_ref(h));(assert.spy(w.sendStarted)).was.called_with(z.is_ref(h),z.is_ref(f));(assert.spy(w.sendStopped)).was.called_with(z.is_ref(h),z.is_ref(G))end)end)end)describe("\035\083\116\111\112",function()before_each(function()spy.on(c,"\116\101\097\114\100\111\119\110")spy.on(g,"\111\110\067\111\109\098\111\083\116\111\112")spy.on(w,"\115\101\110\100\083\116\111\112\112\101\100")end)after_each(function()c.teardown:revert()g.onComboStop:revert()w.sendStopped:revert()end)describe("\119\104\101\110 \110\111 \099\111\109\098\111 \105\115 \097\099\116\105\118\101",function()it("\116\104\114\111\119\115 \097\110 \101\114\114\111\114",function()local z=G.bindn(P.Stop,P,h)assert.error(z,"\080\108\097\121\101\114 \049\051 \104\097\115 \110\111 \097\099\116\105\118\101 \099\111\109\098\111");(assert.spy(c.teardown)).was_not.called();(assert.spy(g.onComboStop)).was_not.called();(assert.spy(w.sendStopped)).was_not.called()end)end)describe("\119\104\101\110 \097 \099\111\109\098\111 \105\115 \097\099\116\105\118\101",function()it("\115\116\111\112\115 \116\104\101 \097\099\116\105\118\101 \099\111\109\098\111",function()P:Start(h,P:Create(13))local G=P.state[h].combo local f=P.state[h].dummy assert.not_nil(G)assert.not_nil(f)assert.equal(13,G.id)assert.is_true(f:IsAlive())c.teardown:clear()g.onComboStop:clear()w.sendStopped:clear()P:Stop(h)local u=P.state[h].combo local p=P.state[h].dummy assert.is_nil(u)assert.not_nil(p)assert.equal(f,p)assert.is_false(p:IsAlive());(assert.spy(c.teardown)).was.called_with(z.is_ref(h),{hardReset=true});(assert.spy(g.onComboStop)).was.called_with(z.is_ref(h));(assert.spy(w.sendStopped)).was.called_with(z.is_ref(h),z.is_ref(G))end)end)end)describe("\035\082\101\115\116\097\114\116",function()before_each(function()spy.on(c,"\115\101\116\117\112")spy.on(c,"\116\101\097\114\100\111\119\110")spy.on(g,"\111\110\068\117\109\109\121\067\114\101\097\116\101")spy.on(g,"\111\110\067\111\109\098\111\083\116\097\114\116")spy.on(g,"\111\110\067\111\109\098\111\083\116\111\112")spy.on(w,"\115\101\110\100\083\116\097\114\116\101\100")spy.on(w,"\115\101\110\100\083\116\111\112\112\101\100")end)after_each(function()c.setup:revert()c.teardown:revert()g.onDummyCreate:revert()g.onComboStart:revert()g.onComboStop:revert()w.sendStarted:revert()w.sendStopped:revert()end)describe("\119\104\101\110 \110\111 \099\111\109\098\111 \105\115 \097\099\116\105\118\101",function()it("\116\104\114\111\119\115 \097\110 \101\114\114\111\114",function()local z=G.bindn(P.Restart,P,h)assert.error(z,"\080\108\097\121\101\114 \049\051 \104\097\115 \110\111 \097\099\116\105\118\101 \099\111\109\098\111");(assert.spy(c.teardown)).was_not.called();(assert.spy(c.setup)).was_not.called();(assert.spy(g.onDummyCreate)).was_not.called();(assert.spy(g.onComboStart)).was_not.called();(assert.spy(g.onComboStop)).was_not.called();(assert.spy(w.sendStarted)).was_not.called();(assert.spy(w.sendStopped)).was_not.called()end)end)describe("\119\104\101\110 \097 \099\111\109\098\111 \105\115 \097\099\116\105\118\101",function()describe("\119\105\116\104\111\117\116 \104\097\114\100 \114\101\115\101\116 \111\112\116\105\111\110",function()it("\114\101\115\116\097\114\116\115 \116\104\101 \099\111\109\098\111",function()P:Start(h,P:Create(13))local G=P.state[h].combo assert.not_nil(G)assert.equal(13,G.id)c.setup:clear()c.teardown:clear()g.onDummyCreate:clear()g.onComboStart:clear()g.onComboStop:clear()w.sendStarted:clear()w.sendStopped:clear()P:Restart(h,{hardReset=false})local f=P.state[h].combo local u=P.state[h].dummy assert.not_nil(f)assert.not_nil(u)assert.not_equal(f,G)assert.equal(13,f.id)assert.is_true(u:IsAlive());(assert.spy(c.teardown)).was.called_with(z.is_ref(h),{hardReset=false});(assert.spy(c.setup)).was.called_with(z.is_ref(h),z.is_ref(f));(assert.spy(g.onDummyCreate)).was_not.called_with(z.is_ref(u));(assert.spy(g.onComboStop)).was_not.called();(assert.spy(g.onComboStart)).was.called_with(z.is_ref(h));(assert.spy(w.sendStarted)).was.called_with(z.is_ref(h),z.is_ref(f));(assert.spy(w.sendStopped)).was_not.called()end)end)describe("\119\105\116\104 \104\097\114\100 \114\101\115\101\116 \111\112\116\105\111\110",function()it("\114\101\115\116\097\114\116\115 \116\104\101 \099\111\109\098\111",function()P:Start(h,P:Create(13))local G=P.state[h].combo assert.not_nil(G)assert.equal(13,G.id)c.setup:clear()c.teardown:clear()g.onDummyCreate:clear()g.onComboStart:clear()g.onComboStop:clear()w.sendStarted:clear()w.sendStopped:clear()P:Restart(h,{hardReset=true})local f=P.state[h].combo local u=P.state[h].dummy assert.not_nil(f)assert.not_nil(u)assert.not_equal(f,G)assert.equal(13,f.id)assert.is_true(u:IsAlive());(assert.spy(c.teardown)).was.called_with(z.is_ref(h),{hardReset=true});(assert.spy(c.setup)).was.called_with(z.is_ref(h),z.is_ref(f));(assert.spy(g.onDummyCreate)).was_not.called_with(z.is_ref(u));(assert.spy(g.onComboStop)).was_not.called();(assert.spy(g.onComboStart)).was.called_with(z.is_ref(h));(assert.spy(w.sendStarted)).was.called_with(z.is_ref(h),z.is_ref(f));(assert.spy(w.sendStopped)).was_not.called()end)end)end)end)describe("\035\080\114\111\103\114\101\115\115",function()local p before_each(function()p=u({id=G.uniqueId("\067\111\109\098\111\115\095\115\112\101\099-\080\114\111\103\114\101\115\115-\037\100"),specialty="\113\119",stance="\100\101\102\101\110\115\105\118\101",heroLevel=25,damageRating=5;difficultyRating=5,gold=1234;tags={"\108\097\116\101-\103\097\109\101"};items={"\105\116\101\109\095\098\108\105\110\107"};orbs={7;7,7},talents=q.Select(q.L10_LEFT,q.L15_RIGHT,q.L20_RIGHT,q.L25_RIGHT),sequence={{id=1,name=M.ABILITY_COLD_SNAP,required=true,next={2;3}};{id=2,name=M.ABILITY_GHOST_WALK,next={3}};{id=3,name=M.ABILITY_EMP,required=true}}})spy.on(P,"\080\114\101\070\105\110\105\115\104")spy.on(P,"\070\097\105\108")spy.on(w,"\115\101\110\100\073\110\080\114\111\103\114\101\115\115")spy.on(w,"\115\101\110\100\080\114\111\103\114\101\115\115")end)after_each(function()P.PreFinish:revert()P.Fail:revert()w.sendInProgress:revert()w.sendProgress:revert()end)describe("\119\105\116\104 \110\111 \097\099\116\105\118\101 \099\111\109\098\111",function()it("\100\111\101\115 \110\111\116\104\105\110\103",function()P:Progress(h,f("\097\098\105\108\105\116\121",{name=M.ABILITY_COLD_SNAP}));(assert.spy(P.PreFinish)).was_not.called();(assert.spy(P.Fail)).was_not.called();(assert.spy(w.sendInProgress)).was_not.called();(assert.spy(w.sendProgress)).was_not.called()end)end)describe("\119\105\116\104 \102\097\105\108\101\100 \099\111\109\098\111",function()it("\100\111\101\115 \110\111\116\104\105\110\103",function()P:Start(h,p)p:Fail()P:Progress(h,f("\097\098\105\108\105\116\121",{name=M.ABILITY_COLD_SNAP}));(assert.spy(P.PreFinish)).was_not.called();(assert.spy(P.Fail)).was_not.called();(assert.spy(w.sendInProgress)).was_not.called();(assert.spy(w.sendProgress)).was_not.called()end)end)describe("\119\105\116\104 \112\114\101-\102\105\110\105\115\104\101\100 \099\111\109\098\111",function()it("\100\111\101\115 \110\111\116\104\105\110\103",function()P:Start(h,p)assert.is_true(p:Progress(f("\097\098\105\108\105\116\121",{name=M.ABILITY_COLD_SNAP})))assert.is_true(p:Progress(f("\097\098\105\108\105\116\121",{name=M.ABILITY_GHOST_WALK})))assert.is_true(p:Progress(f("\097\098\105\108\105\116\121",{name=M.ABILITY_EMP})))assert.is_true(p:PreFinish())assert.is_true(p.preFinished)P:Progress(h,f("\097\098\105\108\105\116\121",{name=M.ABILITY_COLD_SNAP}));(assert.spy(P.PreFinish)).was_not.called();(assert.spy(P.Fail)).was_not.called();(assert.spy(w.sendInProgress)).was_not.called();(assert.spy(w.sendProgress)).was_not.called()end)end)describe("\119\105\116\104 \102\105\110\105\115\104\101\100 \099\111\109\098\111",function()it("\100\111\101\115 \110\111\116\104\105\110\103",function()P:Start(h,p)assert.is_true(p:Progress(f("\097\098\105\108\105\116\121",{name=M.ABILITY_COLD_SNAP})))assert.is_true(p:Progress(f("\097\098\105\108\105\116\121",{name=M.ABILITY_GHOST_WALK})))assert.is_true(p:Progress(f("\097\098\105\108\105\116\121",{name=M.ABILITY_EMP})))assert.is_true(p:PreFinish())assert.is_true(p:Finish())assert.is_true(p.finished)P:Progress(h,f("\097\098\105\108\105\116\121",{name=M.ABILITY_COLD_SNAP}));(assert.spy(P.PreFinish)).was_not.called();(assert.spy(P.Fail)).was_not.called();(assert.spy(w.sendInProgress)).was_not.called();(assert.spy(w.sendProgress)).was_not.called()end)end)describe("\119\105\116\104 \114\101\103\117\108\097\114 \097\099\116\105\118\101 \099\111\109\098\111",function()before_each(function()P:Start(h,p)end)describe("\119\105\116\104 \105\110\118\111\099\097\116\105\111\110 \097\098\105\108\105\116\121",function()it("\100\111\101\115 \110\111\116\104\105\110\103",function()P:Progress(h,f("\097\098\105\108\105\116\121",{name=M.ABILITY_EXORT}));(assert.spy(P.PreFinish)).was_not.called();(assert.spy(P.Fail)).was_not.called();(assert.spy(w.sendInProgress)).was_not.called();(assert.spy(w.sendProgress)).was_not.called()end)end)describe("\119\105\116\104 \105\110\099\111\114\114\101\099\116 \097\098\105\108\105\116\121",function()local G=f("\097\098\105\108\105\116\121",{name=M.ABILITY_SUN_STRIKE})it("\102\097\105\108\115 \116\104\101 \099\111\109\098\111",function()P:Progress(h,G)assert.is_true(p.failed);(assert.spy(P.Fail)).was.self.called_with(z.is_ref(h),z.is_ref(G));(assert.spy(w.sendInProgress)).was_not.called();(assert.spy(w.sendProgress)).was_not.called()end)end)describe("\119\105\116\104 \099\111\114\114\101\099\116 \097\098\105\108\105\116\121",function()it("\112\114\111\103\114\101\115\115\101\115 \116\104\101 \099\111\109\098\111",function()P:Progress(h,f("\097\098\105\108\105\116\121",{name=M.ABILITY_COLD_SNAP}));(assert.spy(P.PreFinish)).was_not.called();(assert.spy(P.Fail)).was_not.called();(assert.spy(w.sendProgress)).was.called_with(z.is_ref(h),z.is_ref(p))end)describe("\119\104\101\110 \105\116\'\115 \116\104\101 \102\105\114\115\116 \099\111\109\098\111 \115\116\101\112",function()it("\099\111\109\109\117\110\105\099\097\116\101\115 \116\104\097\116 \099\111\109\098\111 \105\115 \105\110 \112\114\111\103\114\101\115\115",function()local G=f("\097\098\105\108\105\116\121",{name=M.ABILITY_COLD_SNAP})P:Progress(h,G);(assert.spy(w.sendInProgress)).was.called_with(z.is_ref(h),z.is_ref(p))end)end)describe("\119\104\101\110 \105\116\'\115 \116\104\101 \108\097\115\116 \099\111\109\098\111 \115\116\101\112",function()it("\112\114\101-\102\105\110\105\115\104\101\115 \116\104\101 \099\111\109\098\111",function()assert.is_true(p:Progress(f("\097\098\105\108\105\116\121",{name=M.ABILITY_COLD_SNAP})))assert.is_true(p:Progress(f("\097\098\105\108\105\116\121",{name=M.ABILITY_GHOST_WALK})))P:Progress(h,f("\097\098\105\108\105\116\121",{name=M.ABILITY_EMP}))assert.is_true(p.preFinished);(assert.spy(P.PreFinish)).was.self.called_with(z.is_ref(h));(assert.spy(P.Fail)).was_not.called()end)end)end)end)end)end)
local m = require("moses")
local match = require("luassert.match")
local create = require("support.factory").create

local Combo = require("invokation.combos.Combo")
local Combos = require("invokation.combos.Combos")
local Talents = require("invokation.dota2.talents")
local CombosHero = require("invokation.combos.hero")
local CombosComm = require("invokation.combos.communication")
local CombosSound = require("invokation.combos.sound")
local FreestyleCombo = require("invokation.combos.FreestyleCombo")
local DamageInstance = require("invokation.dota2.DamageInstance")

local UNITS = require("invokation.const.units")
local COMBOS = require("invokation.const.combos")
local INVOKER = require("invokation.const.invoker")
local NET_TABLE = require("invokation.const.net_table")

describe("Combos", function()
  local combos
  local netTable = {}
  local dummySpawn = create("entity")
  local hero = create("dota_hero_invoker")
  local player = create("dota_player", { id = 13, hero = hero })

  setup(function()
    stub.new(Entities, "FindByName", function(_, _, name)
      return name == UNITS.DUMMY_TARGET_SPAWN and dummySpawn or nil
    end)
  end)

  teardown(function()
    -- selene: allow(incorrect_standard_library_use)
    Entities.FindByName:revert()
  end)

  before_each(function()
    stub.new(netTable, "Set")

    combos = Combos(netTable)
  end)

  after_each(function()
    netTable.Set:clear()
  end)

  describe("constructor", function()
    it("loads and transforms specs", function()
      assert.equal(m.count(COMBOS), m.count(combos.specs))

      for _, spec in pairs(combos.specs) do
        assert.is_table(spec)
        assert.not_nil(spec.id)

        for _, step in pairs(spec.sequence) do
          assert.is_table(step)
          assert.not_nil(step.id)
        end
      end
    end)

    it("sets NetTable key with loaded specs", function()
      assert.stub(netTable.Set).was.self.called_with(NET_TABLE.MAIN.KEYS.COMBOS, combos.specs)
    end)
  end)

  describe("#OnAbilityUsed", function()
    before_each(function()
      stub.new(combos, "CaptureAbility")
      stub.new(combos, "Progress")
    end)

    after_each(function()
      combos.CaptureAbility:revert()
      combos.Progress:revert()
    end)

    local unit = create("hero_invoker")

    describe("with relevant ability", function()
      local ability = create("ability", { name = INVOKER.ABILITY_SUN_STRIKE })

      it("handles ability capture and combo progress", function()
        combos:OnAbilityUsed(player, unit, ability)

        assert.stub(combos.CaptureAbility).was.self.called_with(player, ability)
        assert.stub(combos.Progress).was.self.called_with(player, ability)
      end)
    end)

    describe("with ignored ability", function()
      local ability = create("ability", { name = "item_phase_boots" })

      it("is a noop", function()
        combos:OnAbilityUsed(player, unit, ability)

        assert.stub(combos.CaptureAbility).was_not.called()
        assert.stub(combos.Progress).was_not.called()
      end)
    end)
  end)

  describe("#OnEntityHurt", function()
    before_each(function()
      stub.new(combos, "ProgressDamage")
    end)

    after_each(function()
      combos.ProgressDamage:revert()
    end)

    local victim = create("unit", { name = UNITS.DUMMY_TARGET })

    describe("with player-owned attacker", function()
      local attacker = create("hero_invoker", { playerOwner = player })
      local inflictor = create("ability", { name = INVOKER.ABILITY_SUN_STRIKE })

      it("handles combo damage progress", function()
        local damage = DamageInstance(victim, 123.4, attacker, inflictor)
        local owner = damage:AttackerPlayerOwner()

        combos:OnEntityHurt(damage)

        assert.stub(combos.ProgressDamage).was.self.called_with(owner, damage)
      end)
    end)

    describe("with non-player attacker", function()
      local attacker = create("unit", { name = "npc_dota_hero_axe" })
      local inflictor = create("ability", { name = "item_dagon_5" })

      it("is a noop", function()
        local damage = DamageInstance(victim, 123.4, attacker, inflictor)

        combos:OnEntityHurt(damage)

        assert.stub(combos.ProgressDamage).was_not.called()
      end)
    end)
  end)

  describe("#OnItemPurchased", function()
    local purchase = { item = "item_blink", cost = 1234.5 }

    describe("when no combo is active", function()
      it("is a noop", function()
        hero:SetGold(0, true)
        hero:SetGold(0, false)

        assert.equal(0, hero:GetGold())

        combos:OnItemPurchased(player, purchase)

        assert.equal(0, hero:GetGold())
      end)
    end)

    describe("when a regular combo is active", function()
      it("is a noop", function()
        combos:Start(player, combos:Create(1))
        hero:SetGold(0, true)
        hero:SetGold(0, false)

        assert.equal(0, hero:GetGold())

        combos:OnItemPurchased(player, purchase)

        assert.equal(0, hero:GetGold())
      end)
    end)

    describe("when freestyle combo is active", function()
      it("refunds the purchase", function()
        combos:Start(player, combos:Create(FreestyleCombo.COMBO_ID))
        hero:SetGold(0, true)
        hero:SetGold(0, false)

        assert.equal(0, hero:GetGold())

        combos:OnItemPurchased(player, purchase)

        assert.equal(1234.5, hero:GetGold())
      end)
    end)
  end)

  describe("#Create", function()
    describe("with an invalid id", function()
      it("throws an error", function()
        local createFn = m.bindn(combos.Create, combos, -1)
        assert.error(createFn, 'Combo "-1" not found')
      end)
    end)

    describe("with freestyle id", function()
      it("creates a freestyle combo", function()
        local combo = combos:Create(FreestyleCombo.COMBO_ID)

        assert.not_nil(combo)
        assert.is_true(FreestyleCombo:class_of(combo))
      end)
    end)

    describe("with a valid id", function()
      it("creates a combo", function()
        local combo = combos:Create(13)

        assert.not_nil(combo)
        assert.is_true(Combo:class_of(combo))
      end)
    end)
  end)

  describe("#Start", function()
    before_each(function()
      spy.on(CombosHero, "setup")
      spy.on(CombosHero, "teardown")
      spy.on(CombosSound, "onDummyCreate")
      spy.on(CombosSound, "onComboStart")
      spy.on(CombosSound, "onComboStop")
      spy.on(CombosComm, "sendStarted")
      spy.on(CombosComm, "sendStopped")
    end)

    after_each(function()
      CombosHero.setup:revert()
      CombosHero.teardown:revert()
      CombosSound.onDummyCreate:revert()
      CombosSound.onComboStart:revert()
      CombosSound.onComboStop:revert()
      CombosComm.sendStarted:revert()
      CombosComm.sendStopped:revert()
    end)

    describe("when no combo is active", function()
      it("starts the combo", function()
        assert.is_nil(combos.state[player].combo)

        combos:Start(player, combos:Create(13))

        local combo = combos.state[player].combo
        local dummy = combos.state[player].dummy

        assert.not_nil(combo)
        assert.not_nil(dummy)

        assert.equal(13, combo.id)
        assert.is_true(dummy:IsAlive())

        assert.spy(CombosHero.teardown).was.called_with(match.is_ref(player), { hardReset = true })
        assert.spy(CombosHero.setup).was.called_with(match.is_ref(player), match.is_ref(combo))
        assert.spy(CombosSound.onDummyCreate).was.called_with(match.is_ref(dummy))
        assert.spy(CombosSound.onComboStop).was_not.called()
        assert.spy(CombosSound.onComboStart).was.called_with(match.is_ref(player))
        assert.spy(CombosComm.sendStarted).was.called_with(match.is_ref(player), match.is_ref(combo))
        assert.spy(CombosComm.sendStopped).was_not.called()
      end)
    end)

    describe("when the same combo is already active", function()
      it("restarts the combo", function()
        combos:Start(player, combos:Create(13))

        local comboBefore = combos.state[player].combo

        assert.not_nil(comboBefore)
        assert.equal(13, comboBefore.id)

        CombosHero.setup:clear()
        CombosHero.teardown:clear()
        CombosSound.onDummyCreate:clear()
        CombosSound.onComboStart:clear()
        CombosSound.onComboStop:clear()
        CombosComm.sendStarted:clear()
        CombosComm.sendStopped:clear()

        combos:Start(player, combos:Create(13))

        local comboAfter = combos.state[player].combo
        local dummy = combos.state[player].dummy

        assert.not_nil(comboAfter)
        assert.not_nil(dummy)

        assert.not_equal(comboAfter, comboBefore)
        assert.equal(13, comboAfter.id)
        assert.is_true(dummy:IsAlive())

        assert.spy(CombosHero.teardown).was.called_with(match.is_ref(player), { hardReset = false })

        assert.spy(CombosHero.setup).was.called_with(match.is_ref(player), match.is_ref(comboAfter))

        assert.spy(CombosSound.onDummyCreate).was_not.called_with(match.is_ref(dummy))
        assert.spy(CombosSound.onComboStop).was_not.called()
        assert.spy(CombosSound.onComboStart).was.called_with(match.is_ref(player))

        assert.spy(CombosComm.sendStarted).was.called_with(match.is_ref(player), match.is_ref(comboAfter))

        assert.spy(CombosComm.sendStopped).was_not.called()
      end)
    end)

    describe("when a different combo is already active", function()
      it("stops the previous combo and starts the new combo", function()
        combos:Start(player, combos:Create(13))

        local comboBefore = combos.state[player].combo

        assert.not_nil(comboBefore)
        assert.equal(13, comboBefore.id)

        CombosHero.setup:clear()
        CombosHero.teardown:clear()
        CombosSound.onDummyCreate:clear()
        CombosSound.onComboStart:clear()
        CombosSound.onComboStop:clear()
        CombosComm.sendStarted:clear()
        CombosComm.sendStopped:clear()

        combos:Start(player, combos:Create(3))

        local comboAfter = combos.state[player].combo
        local dummy = combos.state[player].dummy

        assert.not_nil(comboAfter)
        assert.not_nil(dummy)

        assert.not_equal(comboAfter, comboBefore)
        assert.equal(3, comboAfter.id)
        assert.is_true(dummy:IsAlive())

        assert.spy(CombosHero.teardown).was.called_with(match.is_ref(player), { hardReset = true })

        assert.spy(CombosHero.setup).was.called_with(match.is_ref(player), match.is_ref(comboAfter))

        assert.spy(CombosSound.onDummyCreate).was_not.called_with(match.is_ref(dummy))
        assert.spy(CombosSound.onComboStop).was.called_with(match.is_ref(player))
        assert.spy(CombosSound.onComboStart).was.called_with(match.is_ref(player))

        assert.spy(CombosComm.sendStarted).was.called_with(match.is_ref(player), match.is_ref(comboAfter))

        assert.spy(CombosComm.sendStopped).was.called_with(match.is_ref(player), match.is_ref(comboBefore))
      end)
    end)
  end)

  describe("#Stop", function()
    before_each(function()
      spy.on(CombosHero, "teardown")
      spy.on(CombosSound, "onComboStop")
      spy.on(CombosComm, "sendStopped")
    end)

    after_each(function()
      CombosHero.teardown:revert()
      CombosSound.onComboStop:revert()
      CombosComm.sendStopped:revert()
    end)

    describe("when no combo is active", function()
      it("throws an error", function()
        local stopFn = m.bindn(combos.Stop, combos, player)

        assert.error(stopFn, "Player 13 has no active combo")
        assert.spy(CombosHero.teardown).was_not.called()
        assert.spy(CombosSound.onComboStop).was_not.called()
        assert.spy(CombosComm.sendStopped).was_not.called()
      end)
    end)

    describe("when a combo is active", function()
      it("stops the active combo", function()
        combos:Start(player, combos:Create(13))

        local comboBefore = combos.state[player].combo
        local dummyBefore = combos.state[player].dummy

        assert.not_nil(comboBefore)
        assert.not_nil(dummyBefore)
        assert.equal(13, comboBefore.id)
        assert.is_true(dummyBefore:IsAlive())

        CombosHero.teardown:clear()
        CombosSound.onComboStop:clear()
        CombosComm.sendStopped:clear()

        combos:Stop(player)

        local comboAfter = combos.state[player].combo
        local dummyAfter = combos.state[player].dummy

        assert.is_nil(comboAfter)
        assert.not_nil(dummyAfter)
        assert.equal(dummyBefore, dummyAfter)
        assert.is_false(dummyAfter:IsAlive())

        assert.spy(CombosHero.teardown).was.called_with(match.is_ref(player), { hardReset = true })
        assert.spy(CombosSound.onComboStop).was.called_with(match.is_ref(player))
        assert.spy(CombosComm.sendStopped).was.called_with(match.is_ref(player), match.is_ref(comboBefore))
      end)
    end)
  end)

  describe("#Restart", function()
    before_each(function()
      spy.on(CombosHero, "setup")
      spy.on(CombosHero, "teardown")
      spy.on(CombosSound, "onDummyCreate")
      spy.on(CombosSound, "onComboStart")
      spy.on(CombosSound, "onComboStop")
      spy.on(CombosComm, "sendStarted")
      spy.on(CombosComm, "sendStopped")
    end)

    after_each(function()
      CombosHero.setup:revert()
      CombosHero.teardown:revert()
      CombosSound.onDummyCreate:revert()
      CombosSound.onComboStart:revert()
      CombosSound.onComboStop:revert()
      CombosComm.sendStarted:revert()
      CombosComm.sendStopped:revert()
    end)

    describe("when no combo is active", function()
      it("throws an error", function()
        local restartFn = m.bindn(combos.Restart, combos, player)

        assert.error(restartFn, "Player 13 has no active combo")

        assert.spy(CombosHero.teardown).was_not.called()
        assert.spy(CombosHero.setup).was_not.called()
        assert.spy(CombosSound.onDummyCreate).was_not.called()
        assert.spy(CombosSound.onComboStart).was_not.called()
        assert.spy(CombosSound.onComboStop).was_not.called()
        assert.spy(CombosComm.sendStarted).was_not.called()
        assert.spy(CombosComm.sendStopped).was_not.called()
      end)
    end)

    describe("when a combo is active", function()
      describe("without hard reset option", function()
        it("restarts the combo", function()
          combos:Start(player, combos:Create(13))

          local comboBefore = combos.state[player].combo

          assert.not_nil(comboBefore)
          assert.equal(13, comboBefore.id)

          CombosHero.setup:clear()
          CombosHero.teardown:clear()
          CombosSound.onDummyCreate:clear()
          CombosSound.onComboStart:clear()
          CombosSound.onComboStop:clear()
          CombosComm.sendStarted:clear()
          CombosComm.sendStopped:clear()

          combos:Restart(player, { hardReset = false })

          local comboAfter = combos.state[player].combo
          local dummy = combos.state[player].dummy

          assert.not_nil(comboAfter)
          assert.not_nil(dummy)

          assert.not_equal(comboAfter, comboBefore)
          assert.equal(13, comboAfter.id)
          assert.is_true(dummy:IsAlive())

          assert.spy(CombosHero.teardown).was.called_with(match.is_ref(player), { hardReset = false })

          assert.spy(CombosHero.setup).was.called_with(match.is_ref(player), match.is_ref(comboAfter))

          assert.spy(CombosSound.onDummyCreate).was_not.called_with(match.is_ref(dummy))
          assert.spy(CombosSound.onComboStop).was_not.called()
          assert.spy(CombosSound.onComboStart).was.called_with(match.is_ref(player))

          assert.spy(CombosComm.sendStarted).was.called_with(match.is_ref(player), match.is_ref(comboAfter))

          assert.spy(CombosComm.sendStopped).was_not.called()
        end)
      end)

      describe("with hard reset option", function()
        it("restarts the combo", function()
          combos:Start(player, combos:Create(13))

          local comboBefore = combos.state[player].combo

          assert.not_nil(comboBefore)
          assert.equal(13, comboBefore.id)

          CombosHero.setup:clear()
          CombosHero.teardown:clear()
          CombosSound.onDummyCreate:clear()
          CombosSound.onComboStart:clear()
          CombosSound.onComboStop:clear()
          CombosComm.sendStarted:clear()
          CombosComm.sendStopped:clear()

          combos:Restart(player, { hardReset = true })

          local comboAfter = combos.state[player].combo
          local dummy = combos.state[player].dummy

          assert.not_nil(comboAfter)
          assert.not_nil(dummy)

          assert.not_equal(comboAfter, comboBefore)
          assert.equal(13, comboAfter.id)
          assert.is_true(dummy:IsAlive())

          assert.spy(CombosHero.teardown).was.called_with(match.is_ref(player), { hardReset = true })

          assert.spy(CombosHero.setup).was.called_with(match.is_ref(player), match.is_ref(comboAfter))

          assert.spy(CombosSound.onDummyCreate).was_not.called_with(match.is_ref(dummy))
          assert.spy(CombosSound.onComboStop).was_not.called()
          assert.spy(CombosSound.onComboStart).was.called_with(match.is_ref(player))

          assert.spy(CombosComm.sendStarted).was.called_with(match.is_ref(player), match.is_ref(comboAfter))

          assert.spy(CombosComm.sendStopped).was_not.called()
        end)
      end)
    end)
  end)

  describe("#Progress", function()
    local combo

    before_each(function()
      combo = Combo({
        id = m.uniqueId("Combos_spec-Progress-%d"),
        specialty = "qw",
        stance = "defensive",
        heroLevel = 25,
        damageRating = 5,
        difficultyRating = 5,
        gold = 1234,
        tags = { "late-game" },
        items = { "item_blink" },
        orbs = { 7, 7, 7 },
        talents = Talents.Select(Talents.L10_LEFT, Talents.L15_RIGHT, Talents.L20_RIGHT, Talents.L25_RIGHT),
        sequence = {
          { id = 1, name = INVOKER.ABILITY_COLD_SNAP, required = true, next = { 2, 3 } },
          { id = 2, name = INVOKER.ABILITY_GHOST_WALK, next = { 3 } },
          { id = 3, name = INVOKER.ABILITY_EMP, required = true },
        },
      })

      spy.on(combos, "PreFinish")
      spy.on(combos, "Fail")
      spy.on(CombosComm, "sendInProgress")
      spy.on(CombosComm, "sendProgress")
    end)

    after_each(function()
      combos.PreFinish:revert()
      combos.Fail:revert()
      CombosComm.sendInProgress:revert()
      CombosComm.sendProgress:revert()
    end)

    describe("with no active combo", function()
      it("does nothing", function()
        combos:Progress(player, create("ability", { name = INVOKER.ABILITY_COLD_SNAP }))

        assert.spy(combos.PreFinish).was_not.called()
        assert.spy(combos.Fail).was_not.called()
        assert.spy(CombosComm.sendInProgress).was_not.called()
        assert.spy(CombosComm.sendProgress).was_not.called()
      end)
    end)

    describe("with failed combo", function()
      it("does nothing", function()
        combos:Start(player, combo)
        combo:Fail()
        combos:Progress(player, create("ability", { name = INVOKER.ABILITY_COLD_SNAP }))

        assert.spy(combos.PreFinish).was_not.called()
        assert.spy(combos.Fail).was_not.called()
        assert.spy(CombosComm.sendInProgress).was_not.called()
        assert.spy(CombosComm.sendProgress).was_not.called()
      end)
    end)

    describe("with pre-finished combo", function()
      it("does nothing", function()
        combos:Start(player, combo)

        assert.is_true(combo:Progress(create("ability", { name = INVOKER.ABILITY_COLD_SNAP })))
        assert.is_true(combo:Progress(create("ability", { name = INVOKER.ABILITY_GHOST_WALK })))
        assert.is_true(combo:Progress(create("ability", { name = INVOKER.ABILITY_EMP })))
        assert.is_true(combo:PreFinish())

        assert.is_true(combo.preFinished)

        combos:Progress(player, create("ability", { name = INVOKER.ABILITY_COLD_SNAP }))

        assert.spy(combos.PreFinish).was_not.called()
        assert.spy(combos.Fail).was_not.called()
        assert.spy(CombosComm.sendInProgress).was_not.called()
        assert.spy(CombosComm.sendProgress).was_not.called()
      end)
    end)

    describe("with finished combo", function()
      it("does nothing", function()
        combos:Start(player, combo)

        assert.is_true(combo:Progress(create("ability", { name = INVOKER.ABILITY_COLD_SNAP })))
        assert.is_true(combo:Progress(create("ability", { name = INVOKER.ABILITY_GHOST_WALK })))
        assert.is_true(combo:Progress(create("ability", { name = INVOKER.ABILITY_EMP })))
        assert.is_true(combo:PreFinish())
        assert.is_true(combo:Finish())

        assert.is_true(combo.finished)

        combos:Progress(player, create("ability", { name = INVOKER.ABILITY_COLD_SNAP }))

        assert.spy(combos.PreFinish).was_not.called()
        assert.spy(combos.Fail).was_not.called()
        assert.spy(CombosComm.sendInProgress).was_not.called()
        assert.spy(CombosComm.sendProgress).was_not.called()
      end)
    end)

    describe("with regular active combo", function()
      before_each(function()
        combos:Start(player, combo)
      end)

      describe("with invocation ability", function()
        it("does nothing", function()
          combos:Progress(player, create("ability", { name = INVOKER.ABILITY_EXORT }))

          assert.spy(combos.PreFinish).was_not.called()
          assert.spy(combos.Fail).was_not.called()
          assert.spy(CombosComm.sendInProgress).was_not.called()
          assert.spy(CombosComm.sendProgress).was_not.called()
        end)
      end)

      describe("with incorrect ability", function()
        local ability = create("ability", { name = INVOKER.ABILITY_SUN_STRIKE })

        it("fails the combo", function()
          combos:Progress(player, ability)

          assert.is_true(combo.failed)
          assert.spy(combos.Fail).was.self.called_with(match.is_ref(player), match.is_ref(ability))
          assert.spy(CombosComm.sendInProgress).was_not.called()
          assert.spy(CombosComm.sendProgress).was_not.called()
        end)
      end)

      describe("with correct ability", function()
        it("progresses the combo", function()
          combos:Progress(player, create("ability", { name = INVOKER.ABILITY_COLD_SNAP }))

          assert.spy(combos.PreFinish).was_not.called()
          assert.spy(combos.Fail).was_not.called()
          assert.spy(CombosComm.sendProgress).was.called_with(match.is_ref(player), match.is_ref(combo))
        end)

        describe("when it's the first combo step", function()
          it("communicates that combo is in progress", function()
            local ability = create("ability", { name = INVOKER.ABILITY_COLD_SNAP })

            combos:Progress(player, ability)

            assert.spy(CombosComm.sendInProgress).was.called_with(match.is_ref(player), match.is_ref(combo))
          end)
        end)

        describe("when it's the last combo step", function()
          it("pre-finishes the combo", function()
            assert.is_true(combo:Progress(create("ability", { name = INVOKER.ABILITY_COLD_SNAP })))
            assert.is_true(combo:Progress(create("ability", { name = INVOKER.ABILITY_GHOST_WALK })))

            combos:Progress(player, create("ability", { name = INVOKER.ABILITY_EMP }))

            assert.is_true(combo.preFinished)
            assert.spy(combos.PreFinish).was.self.called_with(match.is_ref(player))
            assert.spy(combos.Fail).was_not.called()
          end)
        end)
      end)
    end)
  end)
end)
