package.path = package.path .. ";./?.lua;./modules/?.lua" local originalSearchers = package.searchers or package.loaders table.insert(originalSearchers, 2, function(modname) if modname:match("^FlexLove%.modules%.") then local moduleName = modname:gsub("^FlexLove%.modules%.", "") return function() return require("modules." .. moduleName) end end end) require("testing.loveStub") local luaunit = require("testing.luaunit") local FlexLove = require("FlexLove") FlexLove.init() local Animation = FlexLove.Animation local AnimationGroup = Animation.Group -- Helper: create a simple animation with given duration local function makeAnim(duration, startVal, finalVal) return Animation.new({ duration = duration or 1, start = { x = startVal or 0 }, final = { x = finalVal or 100 }, }) end -- Helper: advance an animation group to completion local function runToCompletion(group, dt) dt = dt or 1 / 60 local maxFrames = 10000 for i = 1, maxFrames do if group:update(dt) then return i end end return maxFrames end -- ============================================================================ -- Test Suite: AnimationGroup Construction -- ============================================================================ TestAnimationGroupConstruction = {} function TestAnimationGroupConstruction:setUp() love.window.setMode(1920, 1080) FlexLove.beginFrame() end function TestAnimationGroupConstruction:tearDown() FlexLove.endFrame() end function TestAnimationGroupConstruction:test_new_creates_group_with_defaults() local anim1 = makeAnim(1) local anim2 = makeAnim(1) local group = AnimationGroup.new({ animations = { anim1, anim2 } }) luaunit.assertNotNil(group) luaunit.assertEquals(group.mode, "parallel") luaunit.assertEquals(group.stagger, 0.1) luaunit.assertEquals(#group.animations, 2) luaunit.assertEquals(group:getState(), "ready") end function TestAnimationGroupConstruction:test_new_with_sequence_mode() local group = AnimationGroup.new({ animations = { makeAnim(1) }, mode = "sequence", }) luaunit.assertEquals(group.mode, "sequence") end function TestAnimationGroupConstruction:test_new_with_stagger_mode() local group = AnimationGroup.new({ animations = { makeAnim(1) }, mode = "stagger", stagger = 0.2, }) luaunit.assertEquals(group.mode, "stagger") luaunit.assertEquals(group.stagger, 0.2) end function TestAnimationGroupConstruction:test_new_with_invalid_mode_defaults_to_parallel() local group = AnimationGroup.new({ animations = { makeAnim(1) }, mode = "invalid", }) luaunit.assertEquals(group.mode, "parallel") end function TestAnimationGroupConstruction:test_new_with_nil_props_does_not_error() local group = AnimationGroup.new(nil) luaunit.assertNotNil(group) luaunit.assertEquals(#group.animations, 0) end function TestAnimationGroupConstruction:test_new_with_empty_animations() local group = AnimationGroup.new({ animations = {} }) luaunit.assertNotNil(group) luaunit.assertEquals(#group.animations, 0) end function TestAnimationGroupConstruction:test_new_with_callbacks() local onStart = function() end local onComplete = function() end local group = AnimationGroup.new({ animations = { makeAnim(1) }, onStart = onStart, onComplete = onComplete, }) luaunit.assertEquals(group.onStart, onStart) luaunit.assertEquals(group.onComplete, onComplete) end -- ============================================================================ -- Test Suite: Parallel Mode -- ============================================================================ TestAnimationGroupParallel = {} function TestAnimationGroupParallel:setUp() love.window.setMode(1920, 1080) FlexLove.beginFrame() end function TestAnimationGroupParallel:tearDown() FlexLove.endFrame() end function TestAnimationGroupParallel:test_parallel_runs_all_animations_simultaneously() local anim1 = makeAnim(1) local anim2 = makeAnim(1) local group = AnimationGroup.new({ mode = "parallel", animations = { anim1, anim2 }, }) group:update(0.5) -- Both animations should have progressed luaunit.assertTrue(anim1.elapsed > 0) luaunit.assertTrue(anim2.elapsed > 0) end function TestAnimationGroupParallel:test_parallel_completes_when_all_finish() local anim1 = makeAnim(0.5) local anim2 = makeAnim(1.0) local group = AnimationGroup.new({ mode = "parallel", animations = { anim1, anim2 }, }) -- After 0.6s: anim1 done, anim2 not done local finished = group:update(0.6) luaunit.assertFalse(finished) luaunit.assertEquals(group:getState(), "playing") -- After another 0.5s: both done finished = group:update(0.5) luaunit.assertTrue(finished) luaunit.assertEquals(group:getState(), "completed") end function TestAnimationGroupParallel:test_parallel_uses_max_duration() local anim1 = makeAnim(0.3) local anim2 = makeAnim(0.5) local anim3 = makeAnim(0.8) local group = AnimationGroup.new({ mode = "parallel", animations = { anim1, anim2, anim3 }, }) -- At 0.5s, anim3 is not yet done local finished = group:update(0.5) luaunit.assertFalse(finished) -- At 0.9s total, all should be done finished = group:update(0.4) luaunit.assertTrue(finished) end function TestAnimationGroupParallel:test_parallel_does_not_update_completed_animations() local anim1 = makeAnim(0.2) local anim2 = makeAnim(1.0) local group = AnimationGroup.new({ mode = "parallel", animations = { anim1, anim2 }, }) -- Run past anim1's completion group:update(0.3) local anim1Elapsed = anim1.elapsed -- Update again - anim1 should not be updated further group:update(0.1) -- anim1 is completed, so its elapsed might stay clamped luaunit.assertEquals(anim1:getState(), "completed") end -- ============================================================================ -- Test Suite: Sequence Mode -- ============================================================================ TestAnimationGroupSequence = {} function TestAnimationGroupSequence:setUp() love.window.setMode(1920, 1080) FlexLove.beginFrame() end function TestAnimationGroupSequence:tearDown() FlexLove.endFrame() end function TestAnimationGroupSequence:test_sequence_runs_one_at_a_time() local anim1 = makeAnim(0.5) local anim2 = makeAnim(0.5) local group = AnimationGroup.new({ mode = "sequence", animations = { anim1, anim2 }, }) -- After 0.3s, only anim1 should have progressed group:update(0.3) luaunit.assertTrue(anim1.elapsed > 0) luaunit.assertEquals(anim2.elapsed, 0) -- anim2 hasn't started end function TestAnimationGroupSequence:test_sequence_advances_to_next_on_completion() local anim1 = makeAnim(0.5) local anim2 = makeAnim(0.5) local group = AnimationGroup.new({ mode = "sequence", animations = { anim1, anim2 }, }) -- Complete anim1 group:update(0.6) luaunit.assertEquals(anim1:getState(), "completed") -- Now anim2 should receive updates group:update(0.3) luaunit.assertTrue(anim2.elapsed > 0) end function TestAnimationGroupSequence:test_sequence_completes_when_last_finishes() local anim1 = makeAnim(0.3) local anim2 = makeAnim(0.3) local group = AnimationGroup.new({ mode = "sequence", animations = { anim1, anim2 }, }) -- Complete anim1 group:update(0.4) luaunit.assertFalse(group:getState() == "completed") -- Complete anim2 group:update(0.4) luaunit.assertEquals(group:getState(), "completed") end function TestAnimationGroupSequence:test_sequence_maintains_order() local order = {} local anim1 = Animation.new({ duration = 0.2, start = { x = 0 }, final = { x = 100 }, onStart = function() table.insert(order, 1) end, }) local anim2 = Animation.new({ duration = 0.2, start = { x = 0 }, final = { x = 100 }, onStart = function() table.insert(order, 2) end, }) local anim3 = Animation.new({ duration = 0.2, start = { x = 0 }, final = { x = 100 }, onStart = function() table.insert(order, 3) end, }) local group = AnimationGroup.new({ mode = "sequence", animations = { anim1, anim2, anim3 }, }) runToCompletion(group, 0.05) luaunit.assertEquals(order, { 1, 2, 3 }) end -- ============================================================================ -- Test Suite: Stagger Mode -- ============================================================================ TestAnimationGroupStagger = {} function TestAnimationGroupStagger:setUp() love.window.setMode(1920, 1080) FlexLove.beginFrame() end function TestAnimationGroupStagger:tearDown() FlexLove.endFrame() end function TestAnimationGroupStagger:test_stagger_delays_animation_starts() local anim1 = makeAnim(1) local anim2 = makeAnim(1) local anim3 = makeAnim(1) local group = AnimationGroup.new({ mode = "stagger", stagger = 0.5, animations = { anim1, anim2, anim3 }, }) -- At t=0.3: only anim1 should have started (stagger=0.5 means anim2 starts at t=0.5) group:update(0.3) luaunit.assertTrue(anim1.elapsed > 0) luaunit.assertEquals(anim2.elapsed, 0) luaunit.assertEquals(anim3.elapsed, 0) end function TestAnimationGroupStagger:test_stagger_timing_is_correct() local anim1 = makeAnim(1) local anim2 = makeAnim(1) local anim3 = makeAnim(1) local group = AnimationGroup.new({ mode = "stagger", stagger = 0.2, animations = { anim1, anim2, anim3 }, }) -- At t=0.15: only anim1 started (anim2 starts at t=0.2, anim3 at t=0.4) group:update(0.15) luaunit.assertTrue(anim1.elapsed > 0) luaunit.assertEquals(anim2.elapsed, 0) luaunit.assertEquals(anim3.elapsed, 0) -- At t=0.3: anim1 and anim2 started, anim3 not yet group:update(0.15) luaunit.assertTrue(anim1.elapsed > 0) luaunit.assertTrue(anim2.elapsed > 0) luaunit.assertEquals(anim3.elapsed, 0) -- At t=0.5: all started group:update(0.2) luaunit.assertTrue(anim3.elapsed > 0) end function TestAnimationGroupStagger:test_stagger_completes_when_all_finish() -- With stagger, animations get the full dt once their stagger offset is reached. -- Use a longer stagger so anim2 hasn't started yet at the first check. local anim1 = makeAnim(0.5) local anim2 = makeAnim(0.5) local group = AnimationGroup.new({ mode = "stagger", stagger = 0.5, animations = { anim1, anim2 }, }) -- At t=0.3: anim1 started, anim2 not yet (starts at t=0.5) local finished = group:update(0.3) luaunit.assertFalse(finished) luaunit.assertTrue(anim1.elapsed > 0) luaunit.assertEquals(anim2.elapsed, 0) -- At t=0.6: anim1 completed, anim2 just started and got 0.3s dt finished = group:update(0.3) luaunit.assertFalse(finished) luaunit.assertEquals(anim1:getState(), "completed") luaunit.assertTrue(anim2.elapsed > 0) -- At t=0.9: anim2 should be completed (got 0.3 + 0.3 = 0.6s of updates) finished = group:update(0.3) luaunit.assertTrue(finished) luaunit.assertEquals(group:getState(), "completed") end -- ============================================================================ -- Test Suite: Callbacks -- ============================================================================ TestAnimationGroupCallbacks = {} function TestAnimationGroupCallbacks:setUp() love.window.setMode(1920, 1080) FlexLove.beginFrame() end function TestAnimationGroupCallbacks:tearDown() FlexLove.endFrame() end function TestAnimationGroupCallbacks:test_onStart_called_once() local startCount = 0 local group = AnimationGroup.new({ animations = { makeAnim(0.5) }, onStart = function() startCount = startCount + 1 end, }) group:update(0.1) group:update(0.1) group:update(0.1) luaunit.assertEquals(startCount, 1) end function TestAnimationGroupCallbacks:test_onStart_receives_group_reference() local receivedGroup = nil local group = AnimationGroup.new({ animations = { makeAnim(0.5) }, onStart = function(g) receivedGroup = g end, }) group:update(0.1) luaunit.assertEquals(receivedGroup, group) end function TestAnimationGroupCallbacks:test_onComplete_called_when_all_finish() local completeCount = 0 local group = AnimationGroup.new({ animations = { makeAnim(0.3) }, onComplete = function() completeCount = completeCount + 1 end, }) runToCompletion(group) luaunit.assertEquals(completeCount, 1) end function TestAnimationGroupCallbacks:test_onComplete_not_called_before_completion() local completed = false local group = AnimationGroup.new({ animations = { makeAnim(1) }, onComplete = function() completed = true end, }) group:update(0.5) luaunit.assertFalse(completed) end function TestAnimationGroupCallbacks:test_callback_error_does_not_crash() local group = AnimationGroup.new({ animations = { makeAnim(0.1) }, onStart = function() error("onStart error") end, onComplete = function() error("onComplete error") end, }) -- Should not throw runToCompletion(group) luaunit.assertEquals(group:getState(), "completed") end -- ============================================================================ -- Test Suite: Control Methods -- ============================================================================ TestAnimationGroupControl = {} function TestAnimationGroupControl:setUp() love.window.setMode(1920, 1080) FlexLove.beginFrame() end function TestAnimationGroupControl:tearDown() FlexLove.endFrame() end function TestAnimationGroupControl:test_pause_stops_updates() local anim1 = makeAnim(1) local group = AnimationGroup.new({ animations = { anim1 }, }) group:update(0.2) local elapsedBefore = anim1.elapsed group:pause() group:update(0.3) -- Elapsed should not have increased luaunit.assertEquals(anim1.elapsed, elapsedBefore) luaunit.assertTrue(group:isPaused()) end function TestAnimationGroupControl:test_resume_continues_updates() local anim1 = makeAnim(1) local group = AnimationGroup.new({ animations = { anim1 }, }) group:update(0.2) group:pause() group:update(0.3) -- Should be ignored group:resume() group:update(0.2) -- Should have progressed past the paused value luaunit.assertTrue(anim1.elapsed > 0.2) luaunit.assertFalse(group:isPaused()) end function TestAnimationGroupControl:test_reverse_reverses_all_animations() local anim1 = makeAnim(1) local anim2 = makeAnim(1) local group = AnimationGroup.new({ animations = { anim1, anim2 }, }) group:update(0.5) group:reverse() luaunit.assertTrue(anim1._reversed) luaunit.assertTrue(anim2._reversed) end function TestAnimationGroupControl:test_setSpeed_affects_all_animations() local anim1 = makeAnim(1) local anim2 = makeAnim(1) local group = AnimationGroup.new({ animations = { anim1, anim2 }, }) group:setSpeed(2.0) luaunit.assertEquals(anim1._speed, 2.0) luaunit.assertEquals(anim2._speed, 2.0) end function TestAnimationGroupControl:test_cancel_cancels_all_animations() local anim1 = makeAnim(1) local anim2 = makeAnim(1) local group = AnimationGroup.new({ animations = { anim1, anim2 }, }) group:update(0.3) group:cancel() luaunit.assertEquals(group:getState(), "cancelled") luaunit.assertEquals(anim1:getState(), "cancelled") luaunit.assertEquals(anim2:getState(), "cancelled") end function TestAnimationGroupControl:test_cancel_prevents_further_updates() local anim1 = makeAnim(1) local group = AnimationGroup.new({ animations = { anim1 }, }) group:update(0.2) group:cancel() local elapsedAfterCancel = anim1.elapsed group:update(0.3) luaunit.assertEquals(anim1.elapsed, elapsedAfterCancel) end function TestAnimationGroupControl:test_reset_restores_initial_state() local anim1 = makeAnim(0.5) local group = AnimationGroup.new({ mode = "sequence", animations = { anim1 }, }) runToCompletion(group) luaunit.assertEquals(group:getState(), "completed") group:reset() luaunit.assertEquals(group:getState(), "ready") luaunit.assertFalse(group._hasStarted) luaunit.assertEquals(group._currentIndex, 1) luaunit.assertEquals(group._staggerElapsed, 0) end function TestAnimationGroupControl:test_reset_allows_replaying() local completeCount = 0 local group = AnimationGroup.new({ animations = { makeAnim(0.2) }, onComplete = function() completeCount = completeCount + 1 end, }) runToCompletion(group) luaunit.assertEquals(completeCount, 1) group:reset() runToCompletion(group) luaunit.assertEquals(completeCount, 2) end -- ============================================================================ -- Test Suite: State and Progress -- ============================================================================ TestAnimationGroupStateProgress = {} function TestAnimationGroupStateProgress:setUp() love.window.setMode(1920, 1080) FlexLove.beginFrame() end function TestAnimationGroupStateProgress:tearDown() FlexLove.endFrame() end function TestAnimationGroupStateProgress:test_state_transitions() local group = AnimationGroup.new({ animations = { makeAnim(0.5) }, }) luaunit.assertEquals(group:getState(), "ready") group:update(0.1) luaunit.assertEquals(group:getState(), "playing") runToCompletion(group) luaunit.assertEquals(group:getState(), "completed") end function TestAnimationGroupStateProgress:test_progress_parallel() local anim1 = makeAnim(1) local anim2 = makeAnim(1) local group = AnimationGroup.new({ mode = "parallel", animations = { anim1, anim2 }, }) luaunit.assertAlmostEquals(group:getProgress(), 0, 0.01) group:update(0.5) local progress = group:getProgress() luaunit.assertTrue(progress > 0) luaunit.assertTrue(progress < 1) runToCompletion(group) luaunit.assertAlmostEquals(group:getProgress(), 1, 0.01) end function TestAnimationGroupStateProgress:test_progress_sequence() local anim1 = makeAnim(1) local anim2 = makeAnim(1) local group = AnimationGroup.new({ mode = "sequence", animations = { anim1, anim2 }, }) -- Before any update luaunit.assertAlmostEquals(group:getProgress(), 0, 0.01) -- Halfway through first animation (25% total) group:update(0.5) local progress = group:getProgress() luaunit.assertTrue(progress > 0) luaunit.assertTrue(progress <= 0.5) -- Complete first animation (50% total) group:update(0.6) progress = group:getProgress() luaunit.assertTrue(progress >= 0.5) end function TestAnimationGroupStateProgress:test_empty_group_progress_is_1() local group = AnimationGroup.new({ animations = {} }) luaunit.assertAlmostEquals(group:getProgress(), 1, 0.01) end -- ============================================================================ -- Test Suite: Empty and Edge Cases -- ============================================================================ TestAnimationGroupEdgeCases = {} function TestAnimationGroupEdgeCases:setUp() love.window.setMode(1920, 1080) FlexLove.beginFrame() end function TestAnimationGroupEdgeCases:tearDown() FlexLove.endFrame() end function TestAnimationGroupEdgeCases:test_empty_group_completes_immediately() local completed = false local group = AnimationGroup.new({ animations = {}, onComplete = function() completed = true end, }) local finished = group:update(0.1) luaunit.assertTrue(finished) luaunit.assertEquals(group:getState(), "completed") end function TestAnimationGroupEdgeCases:test_single_animation_group() local anim = makeAnim(0.5) local group = AnimationGroup.new({ animations = { anim }, }) runToCompletion(group) luaunit.assertEquals(group:getState(), "completed") luaunit.assertEquals(anim:getState(), "completed") end function TestAnimationGroupEdgeCases:test_update_after_completion_returns_true() local group = AnimationGroup.new({ animations = { makeAnim(0.1) }, }) runToCompletion(group) local finished = group:update(0.1) luaunit.assertTrue(finished) end function TestAnimationGroupEdgeCases:test_invalid_dt_is_handled() local group = AnimationGroup.new({ animations = { makeAnim(1) }, }) -- Should not throw for invalid dt values group:update(-1) group:update(0 / 0) -- NaN group:update(math.huge) luaunit.assertNotNil(group) end function TestAnimationGroupEdgeCases:test_apply_assigns_group_to_element() local group = AnimationGroup.new({ animations = { makeAnim(1) }, }) local mockElement = {} group:apply(mockElement) luaunit.assertEquals(mockElement.animationGroup, group) end function TestAnimationGroupEdgeCases:test_apply_with_nil_element_does_not_crash() local group = AnimationGroup.new({ animations = { makeAnim(1) }, }) -- Should not throw group:apply(nil) end -- ============================================================================ -- Test Suite: Nested Groups -- ============================================================================ TestAnimationGroupNested = {} function TestAnimationGroupNested:setUp() love.window.setMode(1920, 1080) FlexLove.beginFrame() end function TestAnimationGroupNested:tearDown() FlexLove.endFrame() end function TestAnimationGroupNested:test_nested_parallel_in_sequence() local anim1 = makeAnim(0.3) local anim2 = makeAnim(0.3) local innerGroup = AnimationGroup.new({ mode = "parallel", animations = { anim1, anim2 }, }) local anim3 = makeAnim(0.3) local outerGroup = AnimationGroup.new({ mode = "sequence", animations = { innerGroup, anim3 }, }) -- Inner group should run first outerGroup:update(0.2) luaunit.assertTrue(anim1.elapsed > 0) luaunit.assertTrue(anim2.elapsed > 0) luaunit.assertEquals(anim3.elapsed, 0) -- Complete inner group, anim3 should start outerGroup:update(0.2) outerGroup:update(0.2) luaunit.assertTrue(anim3.elapsed > 0) end function TestAnimationGroupNested:test_nested_sequence_in_parallel() local anim1 = makeAnim(0.2) local anim2 = makeAnim(0.2) local innerSeq = AnimationGroup.new({ mode = "sequence", animations = { anim1, anim2 }, }) local anim3 = makeAnim(0.3) local outerGroup = AnimationGroup.new({ mode = "parallel", animations = { innerSeq, anim3 }, }) -- Both innerSeq and anim3 should run in parallel outerGroup:update(0.1) luaunit.assertTrue(anim1.elapsed > 0) luaunit.assertTrue(anim3.elapsed > 0) end function TestAnimationGroupNested:test_nested_group_completes() local innerGroup = AnimationGroup.new({ mode = "parallel", animations = { makeAnim(0.2), makeAnim(0.2) }, }) local outerGroup = AnimationGroup.new({ mode = "sequence", animations = { innerGroup, makeAnim(0.2) }, }) runToCompletion(outerGroup) luaunit.assertEquals(outerGroup:getState(), "completed") luaunit.assertEquals(innerGroup:getState(), "completed") end function TestAnimationGroupNested:test_deeply_nested_groups() local leaf1 = makeAnim(0.1) local leaf2 = makeAnim(0.1) local inner = AnimationGroup.new({ mode = "parallel", animations = { leaf1, leaf2 }, }) local middle = AnimationGroup.new({ mode = "sequence", animations = { inner, makeAnim(0.1) }, }) local outer = AnimationGroup.new({ mode = "parallel", animations = { middle, makeAnim(0.2) }, }) runToCompletion(outer) luaunit.assertEquals(outer:getState(), "completed") luaunit.assertEquals(middle:getState(), "completed") luaunit.assertEquals(inner:getState(), "completed") end -- Run all tests if not _G.RUNNING_ALL_TESTS then os.exit(luaunit.LuaUnit.run()) end