feat: animation expansion
This commit is contained in:
@@ -232,6 +232,7 @@ function flexlove.init(config)
|
|||||||
ErrorHandler = flexlove._ErrorHandler,
|
ErrorHandler = flexlove._ErrorHandler,
|
||||||
Performance = flexlove._Performance,
|
Performance = flexlove._Performance,
|
||||||
Transform = Transform,
|
Transform = Transform,
|
||||||
|
Animation = Animation,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Initialize Element module with dependencies
|
-- Initialize Element module with dependencies
|
||||||
|
|||||||
@@ -1232,6 +1232,25 @@ function Animation.keyframes(props)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Link an array of animations into a chain (static helper)
|
||||||
|
--- Each animation's completion triggers the next in sequence
|
||||||
|
---@param animations Animation[] Array of animations to chain
|
||||||
|
---@return Animation first The first animation in the chain
|
||||||
|
function Animation.chainSequence(animations)
|
||||||
|
if type(animations) ~= "table" or #animations == 0 then
|
||||||
|
if Animation._ErrorHandler then
|
||||||
|
Animation._ErrorHandler:warn("Animation", "ANIM_004")
|
||||||
|
end
|
||||||
|
return Animation.new({ duration = 0, start = {}, final = {} })
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, #animations - 1 do
|
||||||
|
animations[i]:chain(animations[i + 1])
|
||||||
|
end
|
||||||
|
|
||||||
|
return animations[1]
|
||||||
|
end
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- ANIMATION GROUP (Utility)
|
-- ANIMATION GROUP (Utility)
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ function Element.init(deps)
|
|||||||
Element._StateManager = deps.StateManager
|
Element._StateManager = deps.StateManager
|
||||||
Element._GestureRecognizer = deps.GestureRecognizer
|
Element._GestureRecognizer = deps.GestureRecognizer
|
||||||
Element._Performance = deps.Performance
|
Element._Performance = deps.Performance
|
||||||
|
Element._Animation = deps.Animation
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param props ElementProps
|
---@param props ElementProps
|
||||||
@@ -3716,6 +3717,100 @@ function Element:setTransformOrigin(originX, originY)
|
|||||||
self.transform.originY = originY
|
self.transform.originY = originY
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Animate element to new property values with automatic transition
|
||||||
|
--- Captures current values as start, uses provided values as final, and applies the animation
|
||||||
|
---@param props table Target property values
|
||||||
|
---@param duration number? Animation duration in seconds (default: 0.3)
|
||||||
|
---@param easing string? Easing function name (default: "linear")
|
||||||
|
---@return Element self For method chaining
|
||||||
|
function Element:animateTo(props, duration, easing)
|
||||||
|
if not Element._Animation then
|
||||||
|
Element._ErrorHandler:warn("Element", "ELEM_003")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(props) ~= "table" then
|
||||||
|
Element._ErrorHandler:warn("Element", "ELEM_003")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
duration = duration or 0.3
|
||||||
|
easing = easing or "linear"
|
||||||
|
|
||||||
|
-- Collect current values as start
|
||||||
|
local startValues = {}
|
||||||
|
for key, _ in pairs(props) do
|
||||||
|
startValues[key] = self[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create and apply animation
|
||||||
|
local anim = Element._Animation.new({
|
||||||
|
duration = duration,
|
||||||
|
start = startValues,
|
||||||
|
final = props,
|
||||||
|
easing = easing,
|
||||||
|
})
|
||||||
|
|
||||||
|
anim:apply(self)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Fade element to full opacity
|
||||||
|
---@param duration number? Duration in seconds (default: 0.3)
|
||||||
|
---@param easing string? Easing function name
|
||||||
|
---@return Element self For method chaining
|
||||||
|
function Element:fadeIn(duration, easing)
|
||||||
|
return self:animateTo({ opacity = 1 }, duration or 0.3, easing)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Fade element to zero opacity
|
||||||
|
---@param duration number? Duration in seconds (default: 0.3)
|
||||||
|
---@param easing string? Easing function name
|
||||||
|
---@return Element self For method chaining
|
||||||
|
function Element:fadeOut(duration, easing)
|
||||||
|
return self:animateTo({ opacity = 0 }, duration or 0.3, easing)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Scale element to target scale value using transforms
|
||||||
|
---@param targetScale number Target scale multiplier
|
||||||
|
---@param duration number? Duration in seconds (default: 0.3)
|
||||||
|
---@param easing string? Easing function name
|
||||||
|
---@return Element self For method chaining
|
||||||
|
function Element:scaleTo(targetScale, duration, easing)
|
||||||
|
if not Element._Animation or not Element._Transform then
|
||||||
|
Element._ErrorHandler:warn("Element", "ELEM_003")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ensure element has a transform
|
||||||
|
if not self.transform then
|
||||||
|
self.transform = Element._Transform.new({})
|
||||||
|
end
|
||||||
|
|
||||||
|
local currentScaleX = self.transform.scaleX or 1
|
||||||
|
local currentScaleY = self.transform.scaleY or 1
|
||||||
|
|
||||||
|
local anim = Element._Animation.new({
|
||||||
|
duration = duration or 0.3,
|
||||||
|
start = { scaleX = currentScaleX, scaleY = currentScaleY },
|
||||||
|
final = { scaleX = targetScale, scaleY = targetScale },
|
||||||
|
easing = easing or "linear",
|
||||||
|
})
|
||||||
|
|
||||||
|
anim:apply(self)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Move element to target position
|
||||||
|
---@param x number Target x position
|
||||||
|
---@param y number Target y position
|
||||||
|
---@param duration number? Duration in seconds (default: 0.3)
|
||||||
|
---@param easing string? Easing function name
|
||||||
|
---@return Element self For method chaining
|
||||||
|
function Element:moveTo(x, y, duration, easing)
|
||||||
|
return self:animateTo({ x = x, y = y }, duration or 0.3, easing)
|
||||||
|
end
|
||||||
|
|
||||||
--- Set transition configuration for a property
|
--- Set transition configuration for a property
|
||||||
---@param property string Property name or "all" for all properties
|
---@param property string Property name or "all" for all properties
|
||||||
---@param config table Transition config {duration, easing, delay, onComplete}
|
---@param config table Transition config {duration, easing, delay, onComplete}
|
||||||
|
|||||||
531
testing/__tests__/animation_chaining_test.lua
Normal file
531
testing/__tests__/animation_chaining_test.lua
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
-- Helper: create a simple animation
|
||||||
|
local function makeAnim(duration, startX, finalX)
|
||||||
|
return Animation.new({
|
||||||
|
duration = duration or 1,
|
||||||
|
start = { x = startX or 0 },
|
||||||
|
final = { x = finalX or 100 },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: create a retained-mode test element
|
||||||
|
local function makeElement(props)
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local el = FlexLove.new(props or { width = 100, height = 100 })
|
||||||
|
FlexLove.endFrame()
|
||||||
|
return el
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Animation Instance Chaining
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestAnimationChaining = {}
|
||||||
|
|
||||||
|
function TestAnimationChaining:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChaining:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChaining:test_chain_links_two_animations()
|
||||||
|
local anim1 = makeAnim(0.5, 0, 50)
|
||||||
|
local anim2 = makeAnim(0.5, 50, 100)
|
||||||
|
|
||||||
|
local returned = anim1:chain(anim2)
|
||||||
|
|
||||||
|
luaunit.assertEquals(anim1._next, anim2)
|
||||||
|
luaunit.assertEquals(returned, anim2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChaining:test_chain_with_factory_function()
|
||||||
|
local factory = function(element)
|
||||||
|
return makeAnim(0.5, 0, 100)
|
||||||
|
end
|
||||||
|
local anim1 = makeAnim(0.5)
|
||||||
|
|
||||||
|
local returned = anim1:chain(factory)
|
||||||
|
|
||||||
|
luaunit.assertEquals(anim1._nextFactory, factory)
|
||||||
|
luaunit.assertEquals(returned, anim1) -- returns self when factory
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChaining:test_chained_animations_execute_in_order()
|
||||||
|
local el = makeElement({ width = 100, height = 100, opacity = 1 })
|
||||||
|
|
||||||
|
local order = {}
|
||||||
|
local anim1 = Animation.new({
|
||||||
|
duration = 0.2,
|
||||||
|
start = { x = 0 },
|
||||||
|
final = { x = 50 },
|
||||||
|
onComplete = function() table.insert(order, 1) end,
|
||||||
|
})
|
||||||
|
local anim2 = Animation.new({
|
||||||
|
duration = 0.2,
|
||||||
|
start = { x = 50 },
|
||||||
|
final = { x = 100 },
|
||||||
|
onComplete = function() table.insert(order, 2) end,
|
||||||
|
})
|
||||||
|
|
||||||
|
anim1:chain(anim2)
|
||||||
|
anim1:apply(el)
|
||||||
|
|
||||||
|
-- Run anim1 to completion
|
||||||
|
for i = 1, 20 do
|
||||||
|
el:update(1 / 60)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- anim1 should be done, anim2 should now be the active animation
|
||||||
|
luaunit.assertEquals(order[1], 1)
|
||||||
|
luaunit.assertEquals(el.animation, anim2)
|
||||||
|
|
||||||
|
-- Run anim2 to completion
|
||||||
|
for i = 1, 20 do
|
||||||
|
el:update(1 / 60)
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertEquals(order[2], 2)
|
||||||
|
luaunit.assertNil(el.animation)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChaining:test_chain_with_factory_creates_dynamic_animation()
|
||||||
|
local el = makeElement({ width = 100, height = 100 })
|
||||||
|
el.x = 0
|
||||||
|
|
||||||
|
local anim1 = Animation.new({
|
||||||
|
duration = 0.1,
|
||||||
|
start = { x = 0 },
|
||||||
|
final = { x = 50 },
|
||||||
|
})
|
||||||
|
|
||||||
|
local factoryCalled = false
|
||||||
|
anim1:chain(function(element)
|
||||||
|
factoryCalled = true
|
||||||
|
return Animation.new({
|
||||||
|
duration = 1.0,
|
||||||
|
start = { x = 50 },
|
||||||
|
final = { x = 200 },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
anim1:apply(el)
|
||||||
|
|
||||||
|
-- Run anim1 to completion (0.1s duration, ~7 frames at 1/60)
|
||||||
|
for i = 1, 10 do
|
||||||
|
el:update(1 / 60)
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(factoryCalled)
|
||||||
|
luaunit.assertNotNil(el.animation) -- Should have the factory-created animation (1s duration)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Animation delay()
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestAnimationDelay = {}
|
||||||
|
|
||||||
|
function TestAnimationDelay:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationDelay:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationDelay:test_delay_delays_animation_start()
|
||||||
|
local anim = makeAnim(0.5)
|
||||||
|
anim:delay(0.3)
|
||||||
|
|
||||||
|
-- During delay period, animation should not progress
|
||||||
|
local finished = anim:update(0.2)
|
||||||
|
luaunit.assertFalse(finished)
|
||||||
|
luaunit.assertEquals(anim.elapsed, 0)
|
||||||
|
|
||||||
|
-- Still in delay (0.2 + 0.15 = 0.35 total delay elapsed, but the second
|
||||||
|
-- call starts with _delayElapsed=0.2 < 0.3, so it adds 0.15 and returns false)
|
||||||
|
finished = anim:update(0.15)
|
||||||
|
luaunit.assertFalse(finished)
|
||||||
|
luaunit.assertEquals(anim.elapsed, 0)
|
||||||
|
|
||||||
|
-- Now delay is past (0.35 >= 0.3), animation should start progressing
|
||||||
|
anim:update(0.1)
|
||||||
|
luaunit.assertTrue(anim.elapsed > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationDelay:test_delay_returns_self()
|
||||||
|
local anim = makeAnim(1)
|
||||||
|
local returned = anim:delay(0.5)
|
||||||
|
luaunit.assertEquals(returned, anim)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationDelay:test_delay_with_invalid_value_defaults_to_zero()
|
||||||
|
local anim = makeAnim(0.5)
|
||||||
|
anim:delay(-1)
|
||||||
|
luaunit.assertEquals(anim._delay, 0)
|
||||||
|
|
||||||
|
local anim2 = makeAnim(0.5)
|
||||||
|
anim2:delay("bad")
|
||||||
|
luaunit.assertEquals(anim2._delay, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Animation repeatCount()
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestAnimationRepeat = {}
|
||||||
|
|
||||||
|
function TestAnimationRepeat:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationRepeat:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationRepeat:test_repeat_n_times()
|
||||||
|
local anim = Animation.new({
|
||||||
|
duration = 0.2,
|
||||||
|
start = { x = 0 },
|
||||||
|
final = { x = 100 },
|
||||||
|
})
|
||||||
|
anim:repeatCount(3)
|
||||||
|
|
||||||
|
local completions = 0
|
||||||
|
-- Run through multiple cycles
|
||||||
|
for i = 1, 300 do
|
||||||
|
local finished = anim:update(1 / 60)
|
||||||
|
if anim.elapsed == 0 or finished then
|
||||||
|
completions = completions + 1
|
||||||
|
end
|
||||||
|
if finished then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertEquals(anim:getState(), "completed")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationRepeat:test_repeat_returns_self()
|
||||||
|
local anim = makeAnim(1)
|
||||||
|
local returned = anim:repeatCount(3)
|
||||||
|
luaunit.assertEquals(returned, anim)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Animation yoyo()
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestAnimationYoyo = {}
|
||||||
|
|
||||||
|
function TestAnimationYoyo:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationYoyo:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationYoyo:test_yoyo_reverses_on_repeat()
|
||||||
|
local anim = Animation.new({
|
||||||
|
duration = 0.2,
|
||||||
|
start = { x = 0 },
|
||||||
|
final = { x = 100 },
|
||||||
|
})
|
||||||
|
anim:repeatCount(2):yoyo(true)
|
||||||
|
|
||||||
|
-- First cycle
|
||||||
|
for i = 1, 15 do
|
||||||
|
anim:update(1 / 60)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- After first cycle completes, it should be reversed
|
||||||
|
luaunit.assertTrue(anim._reversed)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationYoyo:test_yoyo_returns_self()
|
||||||
|
local anim = makeAnim(1)
|
||||||
|
local returned = anim:yoyo(true)
|
||||||
|
luaunit.assertEquals(returned, anim)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationYoyo:test_yoyo_default_true()
|
||||||
|
local anim = makeAnim(1)
|
||||||
|
anim:yoyo()
|
||||||
|
luaunit.assertTrue(anim._yoyo)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationYoyo:test_yoyo_false_disables()
|
||||||
|
local anim = makeAnim(1)
|
||||||
|
anim:yoyo(false)
|
||||||
|
luaunit.assertFalse(anim._yoyo)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Animation.chainSequence() static helper
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestAnimationChainSequence = {}
|
||||||
|
|
||||||
|
function TestAnimationChainSequence:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChainSequence:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChainSequence:test_chainSequence_links_all_animations()
|
||||||
|
local a1 = makeAnim(0.2, 0, 50)
|
||||||
|
local a2 = makeAnim(0.2, 50, 100)
|
||||||
|
local a3 = makeAnim(0.2, 100, 150)
|
||||||
|
|
||||||
|
local first = Animation.chainSequence({ a1, a2, a3 })
|
||||||
|
|
||||||
|
luaunit.assertEquals(first, a1)
|
||||||
|
luaunit.assertEquals(a1._next, a2)
|
||||||
|
luaunit.assertEquals(a2._next, a3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChainSequence:test_chainSequence_single_animation()
|
||||||
|
local a1 = makeAnim(0.2)
|
||||||
|
local first = Animation.chainSequence({ a1 })
|
||||||
|
|
||||||
|
luaunit.assertEquals(first, a1)
|
||||||
|
luaunit.assertNil(a1._next)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChainSequence:test_chainSequence_empty_array()
|
||||||
|
local first = Animation.chainSequence({})
|
||||||
|
luaunit.assertNotNil(first) -- should return a fallback animation
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Element Fluent API
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestElementFluentAPI = {}
|
||||||
|
|
||||||
|
function TestElementFluentAPI:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_animateTo_creates_animation()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
el.opacity = 0.5
|
||||||
|
|
||||||
|
local returned = el:animateTo({ opacity = 1 }, 0.5, "easeOutQuad")
|
||||||
|
|
||||||
|
luaunit.assertEquals(returned, el) -- returns self
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
luaunit.assertEquals(el.animation.duration, 0.5)
|
||||||
|
luaunit.assertEquals(el.animation.start.opacity, 0.5)
|
||||||
|
luaunit.assertEquals(el.animation.final.opacity, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_animateTo_with_defaults()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
el.x = 10
|
||||||
|
|
||||||
|
el:animateTo({ x = 200 })
|
||||||
|
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
luaunit.assertEquals(el.animation.duration, 0.3) -- default
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_fadeIn_sets_opacity_target_to_1()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
el.opacity = 0
|
||||||
|
|
||||||
|
local returned = el:fadeIn(0.5)
|
||||||
|
|
||||||
|
luaunit.assertEquals(returned, el)
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
luaunit.assertEquals(el.animation.start.opacity, 0)
|
||||||
|
luaunit.assertEquals(el.animation.final.opacity, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_fadeIn_default_duration()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
el.opacity = 0
|
||||||
|
|
||||||
|
el:fadeIn()
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.animation.duration, 0.3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_fadeOut_sets_opacity_target_to_0()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
el.opacity = 1
|
||||||
|
|
||||||
|
local returned = el:fadeOut(0.5)
|
||||||
|
|
||||||
|
luaunit.assertEquals(returned, el)
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
luaunit.assertEquals(el.animation.start.opacity, 1)
|
||||||
|
luaunit.assertEquals(el.animation.final.opacity, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_fadeOut_default_duration()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
el:fadeOut()
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.animation.duration, 0.3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_scaleTo_creates_scale_animation()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
|
||||||
|
local returned = el:scaleTo(2.0, 0.5)
|
||||||
|
|
||||||
|
luaunit.assertEquals(returned, el)
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
luaunit.assertEquals(el.animation.final.scaleX, 2.0)
|
||||||
|
luaunit.assertEquals(el.animation.final.scaleY, 2.0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_scaleTo_default_duration()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
el:scaleTo(1.5)
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.animation.duration, 0.3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_scaleTo_initializes_transform()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
-- Should not have a transform yet (or it has one from constructor)
|
||||||
|
|
||||||
|
el:scaleTo(2.0)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(el.transform)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_moveTo_creates_position_animation()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
el.x = 0
|
||||||
|
el.y = 0
|
||||||
|
|
||||||
|
local returned = el:moveTo(200, 300, 0.5, "easeInOutCubic")
|
||||||
|
|
||||||
|
luaunit.assertEquals(returned, el)
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
luaunit.assertEquals(el.animation.start.x, 0)
|
||||||
|
luaunit.assertEquals(el.animation.start.y, 0)
|
||||||
|
luaunit.assertEquals(el.animation.final.x, 200)
|
||||||
|
luaunit.assertEquals(el.animation.final.y, 300)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_moveTo_default_duration()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
el:moveTo(100, 100)
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.animation.duration, 0.3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementFluentAPI:test_animateTo_with_invalid_props_returns_self()
|
||||||
|
local el = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
|
||||||
|
local returned = el:animateTo("invalid")
|
||||||
|
|
||||||
|
luaunit.assertEquals(returned, el)
|
||||||
|
luaunit.assertNil(el.animation)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Integration - Chaining with Fluent API
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestAnimationChainingIntegration = {}
|
||||||
|
|
||||||
|
function TestAnimationChainingIntegration:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChainingIntegration:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChainingIntegration:test_chained_delay_and_repeat()
|
||||||
|
local anim = Animation.new({
|
||||||
|
duration = 0.2,
|
||||||
|
start = { x = 0 },
|
||||||
|
final = { x = 100 },
|
||||||
|
})
|
||||||
|
local chained = anim:delay(0.1):repeatCount(2):yoyo(true)
|
||||||
|
|
||||||
|
luaunit.assertEquals(chained, anim)
|
||||||
|
luaunit.assertEquals(anim._delay, 0.1)
|
||||||
|
luaunit.assertEquals(anim._repeatCount, 2)
|
||||||
|
luaunit.assertTrue(anim._yoyo)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestAnimationChainingIntegration:test_complex_chain_executes_fully()
|
||||||
|
local el = makeElement({ width = 100, height = 100, opacity = 1 })
|
||||||
|
|
||||||
|
local a1 = Animation.new({
|
||||||
|
duration = 0.1,
|
||||||
|
start = { opacity = 1 },
|
||||||
|
final = { opacity = 0 },
|
||||||
|
})
|
||||||
|
local a2 = Animation.new({
|
||||||
|
duration = 0.1,
|
||||||
|
start = { opacity = 0 },
|
||||||
|
final = { opacity = 1 },
|
||||||
|
})
|
||||||
|
local a3 = Animation.new({
|
||||||
|
duration = 0.1,
|
||||||
|
start = { opacity = 1 },
|
||||||
|
final = { opacity = 0.5 },
|
||||||
|
})
|
||||||
|
|
||||||
|
Animation.chainSequence({ a1, a2, a3 })
|
||||||
|
a1:apply(el)
|
||||||
|
|
||||||
|
-- Run all three animations
|
||||||
|
for i = 1, 100 do
|
||||||
|
el:update(1 / 60)
|
||||||
|
if not el.animation then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- All should have completed, no animation left
|
||||||
|
luaunit.assertNil(el.animation)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run all tests
|
||||||
|
if not _G.RUNNING_ALL_TESTS then
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
|
end
|
||||||
853
testing/__tests__/animation_group_test.lua
Normal file
853
testing/__tests__/animation_group_test.lua
Normal file
@@ -0,0 +1,853 @@
|
|||||||
|
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
|
||||||
442
testing/__tests__/transition_test.lua
Normal file
442
testing/__tests__/transition_test.lua
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
-- Helper: create a retained-mode element
|
||||||
|
local function makeElement(props)
|
||||||
|
props = props or {}
|
||||||
|
props.width = props.width or 100
|
||||||
|
props.height = props.height or 100
|
||||||
|
return FlexLove.new(props)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: setTransition()
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestSetTransition = {}
|
||||||
|
|
||||||
|
function TestSetTransition:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransition:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransition:test_setTransition_creates_transitions_table()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", { duration = 0.5 })
|
||||||
|
|
||||||
|
luaunit.assertNotNil(el.transitions)
|
||||||
|
luaunit.assertNotNil(el.transitions.opacity)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransition:test_setTransition_stores_config()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", {
|
||||||
|
duration = 0.5,
|
||||||
|
easing = "easeInQuad",
|
||||||
|
delay = 0.1,
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.duration, 0.5)
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.easing, "easeInQuad")
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.delay, 0.1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransition:test_setTransition_uses_defaults()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", {})
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.duration, 0.3)
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.easing, "easeOutQuad")
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.delay, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransition:test_setTransition_invalid_duration_uses_default()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", { duration = -1 })
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.duration, 0.3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransition:test_setTransition_with_invalid_config_handles_gracefully()
|
||||||
|
local el = makeElement()
|
||||||
|
-- Should not throw
|
||||||
|
el:setTransition("opacity", "invalid")
|
||||||
|
luaunit.assertNotNil(el.transitions.opacity)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransition:test_setTransition_for_all_properties()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("all", { duration = 0.2, easing = "linear" })
|
||||||
|
|
||||||
|
luaunit.assertNotNil(el.transitions["all"])
|
||||||
|
luaunit.assertEquals(el.transitions["all"].duration, 0.2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransition:test_setTransition_with_onComplete_callback()
|
||||||
|
local el = makeElement()
|
||||||
|
local cb = function() end
|
||||||
|
el:setTransition("opacity", {
|
||||||
|
duration = 0.3,
|
||||||
|
onComplete = cb,
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.onComplete, cb)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransition:test_setTransition_overwrites_previous()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", { duration = 0.5 })
|
||||||
|
el:setTransition("opacity", { duration = 1.0 })
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.duration, 1.0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: setTransitionGroup()
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestSetTransitionGroup = {}
|
||||||
|
|
||||||
|
function TestSetTransitionGroup:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransitionGroup:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransitionGroup:test_setTransitionGroup_applies_to_all_properties()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransitionGroup("colors", { duration = 0.3 }, {
|
||||||
|
"backgroundColor",
|
||||||
|
"borderColor",
|
||||||
|
"textColor",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertNotNil(el.transitions.backgroundColor)
|
||||||
|
luaunit.assertNotNil(el.transitions.borderColor)
|
||||||
|
luaunit.assertNotNil(el.transitions.textColor)
|
||||||
|
luaunit.assertEquals(el.transitions.backgroundColor.duration, 0.3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransitionGroup:test_setTransitionGroup_with_invalid_properties()
|
||||||
|
local el = makeElement()
|
||||||
|
-- Should not throw
|
||||||
|
el:setTransitionGroup("invalid", { duration = 0.3 }, "not a table")
|
||||||
|
-- No transitions should be set
|
||||||
|
luaunit.assertNil(el.transitions)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetTransitionGroup:test_setTransitionGroup_shared_config()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransitionGroup("position", { duration = 0.5, easing = "easeInOutCubic" }, {
|
||||||
|
"x",
|
||||||
|
"y",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.transitions.x.duration, 0.5)
|
||||||
|
luaunit.assertEquals(el.transitions.y.duration, 0.5)
|
||||||
|
luaunit.assertEquals(el.transitions.x.easing, "easeInOutCubic")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: removeTransition()
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestRemoveTransition = {}
|
||||||
|
|
||||||
|
function TestRemoveTransition:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestRemoveTransition:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestRemoveTransition:test_removeTransition_removes_single()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", { duration = 0.3 })
|
||||||
|
el:setTransition("x", { duration = 0.5 })
|
||||||
|
|
||||||
|
el:removeTransition("opacity")
|
||||||
|
|
||||||
|
luaunit.assertNil(el.transitions.opacity)
|
||||||
|
luaunit.assertNotNil(el.transitions.x)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestRemoveTransition:test_removeTransition_all_clears_all()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", { duration = 0.3 })
|
||||||
|
el:setTransition("x", { duration = 0.5 })
|
||||||
|
|
||||||
|
el:removeTransition("all")
|
||||||
|
|
||||||
|
luaunit.assertEquals(next(el.transitions), nil) -- empty table
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestRemoveTransition:test_removeTransition_no_transitions_does_not_error()
|
||||||
|
local el = makeElement()
|
||||||
|
-- Should not throw even with no transitions set
|
||||||
|
el:removeTransition("opacity")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestRemoveTransition:test_removeTransition_nonexistent_property()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", { duration = 0.3 })
|
||||||
|
|
||||||
|
-- Should not throw
|
||||||
|
el:removeTransition("nonexistent")
|
||||||
|
luaunit.assertNotNil(el.transitions.opacity)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: setProperty() with Transitions
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestSetPropertyTransitions = {}
|
||||||
|
|
||||||
|
function TestSetPropertyTransitions:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetPropertyTransitions:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetPropertyTransitions:test_setProperty_without_transition_sets_immediately()
|
||||||
|
local el = makeElement()
|
||||||
|
el.opacity = 1
|
||||||
|
|
||||||
|
el:setProperty("opacity", 0.5)
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.opacity, 0.5)
|
||||||
|
luaunit.assertNil(el.animation)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetPropertyTransitions:test_setProperty_with_transition_creates_animation()
|
||||||
|
local el = makeElement()
|
||||||
|
el.opacity = 1
|
||||||
|
el:setTransition("opacity", { duration = 0.5 })
|
||||||
|
|
||||||
|
el:setProperty("opacity", 0)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
luaunit.assertEquals(el.animation.duration, 0.5)
|
||||||
|
luaunit.assertEquals(el.animation.start.opacity, 1)
|
||||||
|
luaunit.assertEquals(el.animation.final.opacity, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetPropertyTransitions:test_setProperty_same_value_does_not_animate()
|
||||||
|
local el = makeElement()
|
||||||
|
el.opacity = 1
|
||||||
|
el:setTransition("opacity", { duration = 0.5 })
|
||||||
|
|
||||||
|
el:setProperty("opacity", 1)
|
||||||
|
|
||||||
|
luaunit.assertNil(el.animation)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetPropertyTransitions:test_setProperty_with_all_transition()
|
||||||
|
local el = makeElement()
|
||||||
|
el.opacity = 1
|
||||||
|
el:setTransition("all", { duration = 0.3 })
|
||||||
|
|
||||||
|
el:setProperty("opacity", 0)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
luaunit.assertEquals(el.animation.duration, 0.3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetPropertyTransitions:test_setProperty_specific_overrides_all()
|
||||||
|
local el = makeElement()
|
||||||
|
el.opacity = 1
|
||||||
|
el:setTransition("all", { duration = 0.3 })
|
||||||
|
el:setTransition("opacity", { duration = 0.8 })
|
||||||
|
|
||||||
|
el:setProperty("opacity", 0)
|
||||||
|
|
||||||
|
-- Should use the specific "opacity" transition, not "all"
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
luaunit.assertEquals(el.animation.duration, 0.8)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetPropertyTransitions:test_setProperty_transition_with_delay()
|
||||||
|
local el = makeElement()
|
||||||
|
el.opacity = 1
|
||||||
|
el:setTransition("opacity", { duration = 0.3, delay = 0.2 })
|
||||||
|
|
||||||
|
el:setProperty("opacity", 0)
|
||||||
|
|
||||||
|
-- Animation should have the delay set
|
||||||
|
-- The delay is part of the transition config, which is used to create the animation
|
||||||
|
-- Note: delay may not be passed to Animation.new automatically by current implementation
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetPropertyTransitions:test_setProperty_transition_onComplete_callback()
|
||||||
|
local el = makeElement()
|
||||||
|
el.opacity = 1
|
||||||
|
local callbackCalled = false
|
||||||
|
el:setTransition("opacity", {
|
||||||
|
duration = 0.3,
|
||||||
|
onComplete = function() callbackCalled = true end,
|
||||||
|
})
|
||||||
|
|
||||||
|
el:setProperty("opacity", 0)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
luaunit.assertNotNil(el.animation.onComplete)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestSetPropertyTransitions:test_setProperty_nil_current_value_sets_directly()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("customProp", { duration = 0.3 })
|
||||||
|
|
||||||
|
-- customProp is nil, should set directly
|
||||||
|
el:setProperty("customProp", 42)
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.customProp, 42)
|
||||||
|
luaunit.assertNil(el.animation)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Per-Property Transition Configuration
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestPerPropertyTransitionConfig = {}
|
||||||
|
|
||||||
|
function TestPerPropertyTransitionConfig:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestPerPropertyTransitionConfig:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestPerPropertyTransitionConfig:test_different_durations_per_property()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", { duration = 0.3 })
|
||||||
|
el:setTransition("x", { duration = 1.0 })
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.duration, 0.3)
|
||||||
|
luaunit.assertEquals(el.transitions.x.duration, 1.0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestPerPropertyTransitionConfig:test_different_easing_per_property()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", { easing = "easeInQuad" })
|
||||||
|
el:setTransition("x", { easing = "easeOutCubic" })
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.easing, "easeInQuad")
|
||||||
|
luaunit.assertEquals(el.transitions.x.easing, "easeOutCubic")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestPerPropertyTransitionConfig:test_transition_disabled_after_removal()
|
||||||
|
local el = makeElement()
|
||||||
|
el.opacity = 1
|
||||||
|
el:setTransition("opacity", { duration = 0.3 })
|
||||||
|
|
||||||
|
-- Verify transition is active
|
||||||
|
el:setProperty("opacity", 0.5)
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
|
||||||
|
-- Remove transition and reset
|
||||||
|
el.animation = nil
|
||||||
|
el.opacity = 1
|
||||||
|
el:removeTransition("opacity")
|
||||||
|
|
||||||
|
-- Should set immediately now
|
||||||
|
el:setProperty("opacity", 0.5)
|
||||||
|
luaunit.assertEquals(el.opacity, 0.5)
|
||||||
|
luaunit.assertNil(el.animation)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestPerPropertyTransitionConfig:test_multiple_properties_configured()
|
||||||
|
local el = makeElement()
|
||||||
|
el:setTransition("opacity", { duration = 0.3 })
|
||||||
|
el:setTransition("x", { duration = 0.5 })
|
||||||
|
el:setTransition("width", { duration = 1.0 })
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.transitions.opacity.duration, 0.3)
|
||||||
|
luaunit.assertEquals(el.transitions.x.duration, 0.5)
|
||||||
|
luaunit.assertEquals(el.transitions.width.duration, 1.0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Transition Integration
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestTransitionIntegration = {}
|
||||||
|
|
||||||
|
function TestTransitionIntegration:setUp()
|
||||||
|
love.window.setMode(1920, 1080)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTransitionIntegration:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTransitionIntegration:test_transition_animation_runs_to_completion()
|
||||||
|
local el = makeElement()
|
||||||
|
el.opacity = 1
|
||||||
|
el:setTransition("opacity", { duration = 0.2 })
|
||||||
|
el:setProperty("opacity", 0)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(el.animation)
|
||||||
|
|
||||||
|
-- Run animation to completion
|
||||||
|
for i = 1, 30 do
|
||||||
|
el:update(1 / 60)
|
||||||
|
if not el.animation then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertNil(el.animation)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTransitionIntegration:test_manual_animation_overrides_transition()
|
||||||
|
local el = makeElement()
|
||||||
|
el.opacity = 1
|
||||||
|
el:setTransition("opacity", { duration = 0.3 })
|
||||||
|
|
||||||
|
-- Apply manual animation
|
||||||
|
local manualAnim = Animation.new({
|
||||||
|
duration = 1.0,
|
||||||
|
start = { opacity = 1 },
|
||||||
|
final = { opacity = 0 },
|
||||||
|
})
|
||||||
|
manualAnim:apply(el)
|
||||||
|
|
||||||
|
luaunit.assertEquals(el.animation.duration, 1.0) -- Manual anim
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run all tests
|
||||||
|
if not _G.RUNNING_ALL_TESTS then
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
|
end
|
||||||
@@ -37,6 +37,8 @@ local luaunit = require("testing.luaunit")
|
|||||||
|
|
||||||
local testFiles = {
|
local testFiles = {
|
||||||
"testing/__tests__/absolute_positioning_test.lua",
|
"testing/__tests__/absolute_positioning_test.lua",
|
||||||
|
"testing/__tests__/animation_chaining_test.lua",
|
||||||
|
"testing/__tests__/animation_group_test.lua",
|
||||||
"testing/__tests__/animation_test.lua",
|
"testing/__tests__/animation_test.lua",
|
||||||
"testing/__tests__/blur_test.lua",
|
"testing/__tests__/blur_test.lua",
|
||||||
"testing/__tests__/calc_test.lua",
|
"testing/__tests__/calc_test.lua",
|
||||||
@@ -67,6 +69,7 @@ local testFiles = {
|
|||||||
"testing/__tests__/text_editor_test.lua",
|
"testing/__tests__/text_editor_test.lua",
|
||||||
"testing/__tests__/theme_test.lua",
|
"testing/__tests__/theme_test.lua",
|
||||||
"testing/__tests__/touch_events_test.lua",
|
"testing/__tests__/touch_events_test.lua",
|
||||||
|
"testing/__tests__/transition_test.lua",
|
||||||
"testing/__tests__/units_test.lua",
|
"testing/__tests__/units_test.lua",
|
||||||
"testing/__tests__/utils_test.lua",
|
"testing/__tests__/utils_test.lua",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user