1769 lines
49 KiB
Lua
1769 lines
49 KiB
Lua
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 Easing = Animation.Easing
|
|
local Color = FlexLove.Color
|
|
|
|
-- ============================================================================
|
|
-- Test Suite: Animation Validation and Error Handling
|
|
-- ============================================================================
|
|
|
|
TestAnimationValidation = {}
|
|
|
|
function TestAnimationValidation:setUp()
|
|
love.window.setMode(1920, 1080)
|
|
FlexLove.beginFrame()
|
|
end
|
|
|
|
function TestAnimationValidation:tearDown()
|
|
FlexLove.endFrame()
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_invalid_props()
|
|
-- Should handle non-table props gracefully
|
|
local anim = Animation.new(nil)
|
|
luaunit.assertNotNil(anim)
|
|
luaunit.assertEquals(anim.duration, 1)
|
|
|
|
local anim2 = Animation.new("invalid")
|
|
luaunit.assertNotNil(anim2)
|
|
luaunit.assertEquals(anim2.duration, 1)
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_negative_duration()
|
|
-- Should warn and use default duration (1 second) for invalid duration
|
|
local anim = Animation.new({
|
|
duration = -1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
})
|
|
luaunit.assertNotNil(anim)
|
|
luaunit.assertEquals(anim.duration, 1) -- Default value
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_zero_duration()
|
|
-- Should warn and use default duration (1 second) for invalid duration
|
|
local anim = Animation.new({
|
|
duration = 0,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
})
|
|
luaunit.assertNotNil(anim)
|
|
luaunit.assertEquals(anim.duration, 1) -- Default value
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_string_duration()
|
|
-- Non-number duration
|
|
local anim = Animation.new({
|
|
duration = "invalid",
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
luaunit.assertEquals(anim.duration, 1)
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_nil_duration()
|
|
-- Duration is nil, should use default
|
|
local anim = Animation.new({
|
|
duration = 0.0001, -- Very small instead of nil to avoid nil errors
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
})
|
|
luaunit.assertNotNil(anim)
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_invalid_start_final()
|
|
-- Invalid start table
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = "invalid",
|
|
final = { x = 100 },
|
|
})
|
|
luaunit.assertEquals(type(anim.start), "table")
|
|
|
|
-- Invalid final table
|
|
local anim2 = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = "invalid",
|
|
})
|
|
luaunit.assertEquals(type(anim2.final), "table")
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_invalid_easing()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
easing = "invalidEasing",
|
|
})
|
|
-- Should fallback to linear
|
|
luaunit.assertNotNil(anim)
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
-- Linear at 0.5 should be 0.5
|
|
luaunit.assertAlmostEquals(result.opacity, 0.5, 0.01)
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_nil_easing()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
easing = nil,
|
|
})
|
|
-- Should use linear as default
|
|
luaunit.assertNotNil(anim)
|
|
end
|
|
|
|
function TestAnimationValidation:test_easing_string_and_function()
|
|
-- Valid easing string
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
easing = "easeInQuad",
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
luaunit.assertEquals(type(anim.easing), "function")
|
|
|
|
-- Invalid easing string (should default to linear)
|
|
local anim2 = Animation.new({
|
|
duration = 1,
|
|
easing = "invalidEasing",
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
luaunit.assertEquals(type(anim2.easing), "function")
|
|
|
|
-- Custom easing function
|
|
local customEasing = function(t)
|
|
return t * t
|
|
end
|
|
local anim3 = Animation.new({
|
|
duration = 1,
|
|
easing = customEasing,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
luaunit.assertEquals(anim3.easing, customEasing)
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_missing_start_values()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = {},
|
|
final = { opacity = 1 },
|
|
})
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
-- Should not have opacity since start.opacity is missing
|
|
luaunit.assertNil(result.opacity)
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_missing_final_values()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = {},
|
|
})
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
-- Should not have opacity since final.opacity is missing
|
|
luaunit.assertNil(result.opacity)
|
|
end
|
|
|
|
function TestAnimationValidation:test_new_with_mismatched_properties()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0, width = 100 },
|
|
final = { opacity = 1 }, -- width missing
|
|
})
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
luaunit.assertNotNil(result.opacity)
|
|
luaunit.assertNil(result.width)
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- Test Suite: Animation Update and State Control
|
|
-- ============================================================================
|
|
|
|
TestAnimationUpdate = {}
|
|
|
|
function TestAnimationUpdate:setUp()
|
|
love.window.setMode(1920, 1080)
|
|
FlexLove.beginFrame()
|
|
end
|
|
|
|
function TestAnimationUpdate:tearDown()
|
|
FlexLove.endFrame()
|
|
end
|
|
|
|
function TestAnimationUpdate:test_update_with_negative_dt()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
})
|
|
anim:update(-0.5)
|
|
-- Elapsed should be negative, but shouldn't crash
|
|
luaunit.assertNotNil(anim.elapsed)
|
|
end
|
|
|
|
function TestAnimationUpdate:test_update_with_huge_dt()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
})
|
|
local done = anim:update(999999)
|
|
luaunit.assertTrue(done)
|
|
-- Should clamp to 1.0
|
|
local result = anim:interpolate()
|
|
luaunit.assertAlmostEquals(result.opacity, 1.0, 0.01)
|
|
end
|
|
|
|
function TestAnimationUpdate:test_update_with_invalid_dt()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
-- Negative dt
|
|
anim:update(-1)
|
|
luaunit.assertEquals(anim.elapsed, 0)
|
|
|
|
-- NaN dt
|
|
anim:update(0 / 0)
|
|
luaunit.assertEquals(anim.elapsed, 0)
|
|
|
|
-- Infinite dt
|
|
anim:update(math.huge)
|
|
luaunit.assertEquals(anim.elapsed, 0)
|
|
|
|
-- String dt (non-number)
|
|
anim:update("invalid")
|
|
luaunit.assertEquals(anim.elapsed, 0)
|
|
end
|
|
|
|
function TestAnimationUpdate:test_update_while_paused()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
anim:pause()
|
|
local complete = anim:update(0.5)
|
|
|
|
luaunit.assertFalse(complete)
|
|
luaunit.assertEquals(anim.elapsed, 0)
|
|
end
|
|
|
|
function TestAnimationUpdate:test_callbacks()
|
|
local onStartCalled = false
|
|
local onUpdateCalled = false
|
|
local onCompleteCalled = false
|
|
|
|
local anim = Animation.new({
|
|
duration = 0.1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
onStart = function()
|
|
onStartCalled = true
|
|
end,
|
|
onUpdate = function()
|
|
onUpdateCalled = true
|
|
end,
|
|
onComplete = function()
|
|
onCompleteCalled = true
|
|
end,
|
|
})
|
|
|
|
-- First update should trigger onStart
|
|
anim:update(0.05)
|
|
luaunit.assertTrue(onStartCalled)
|
|
luaunit.assertTrue(onUpdateCalled)
|
|
luaunit.assertFalse(onCompleteCalled)
|
|
|
|
-- Complete the animation
|
|
anim:update(0.1)
|
|
luaunit.assertTrue(onCompleteCalled)
|
|
end
|
|
|
|
function TestAnimationUpdate:test_onCancel_callback()
|
|
local onCancelCalled = false
|
|
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
onCancel = function()
|
|
onCancelCalled = true
|
|
end,
|
|
})
|
|
|
|
anim:update(0.5)
|
|
anim:cancel()
|
|
|
|
luaunit.assertTrue(onCancelCalled)
|
|
end
|
|
|
|
function TestAnimationUpdate:test_pause_resume()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local elapsed1 = anim.elapsed
|
|
|
|
anim:pause()
|
|
anim:update(0.5)
|
|
luaunit.assertEquals(anim.elapsed, elapsed1) -- Should not advance
|
|
|
|
anim:resume()
|
|
anim:update(0.1)
|
|
luaunit.assertTrue(anim.elapsed > elapsed1) -- Should advance
|
|
end
|
|
|
|
function TestAnimationUpdate:test_reverse()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
anim:reverse()
|
|
|
|
luaunit.assertTrue(anim._reversed)
|
|
|
|
-- Continue updating - it should go backwards
|
|
anim:update(0.3)
|
|
luaunit.assertTrue(anim.elapsed < 0.5)
|
|
end
|
|
|
|
function TestAnimationUpdate:test_setSpeed()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
anim:setSpeed(2.0)
|
|
luaunit.assertEquals(anim._speed, 2.0)
|
|
|
|
-- Update with 0.1 seconds at 2x speed should advance 0.2 seconds
|
|
anim:update(0.1)
|
|
luaunit.assertAlmostEquals(anim.elapsed, 0.2, 0.01)
|
|
end
|
|
|
|
function TestAnimationUpdate:test_reset()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
anim:update(0.7)
|
|
luaunit.assertTrue(anim.elapsed > 0)
|
|
|
|
anim:reset()
|
|
luaunit.assertEquals(anim.elapsed, 0)
|
|
luaunit.assertFalse(anim._hasStarted)
|
|
end
|
|
|
|
function TestAnimationUpdate:test_isPaused_isComplete()
|
|
local anim = Animation.new({
|
|
duration = 0.5,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
luaunit.assertFalse(anim:isPaused())
|
|
|
|
anim:pause()
|
|
luaunit.assertTrue(anim:isPaused())
|
|
|
|
anim:resume()
|
|
luaunit.assertFalse(anim:isPaused())
|
|
|
|
local complete = anim:update(1.0) -- Complete it
|
|
luaunit.assertTrue(complete)
|
|
luaunit.assertEquals(anim:getState(), "completed")
|
|
end
|
|
|
|
function TestAnimationUpdate:test_delay()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
anim:delay(0.5)
|
|
|
|
-- Update during delay - animation should not start yet
|
|
local result = anim:update(0.3)
|
|
luaunit.assertFalse(result)
|
|
luaunit.assertEquals(anim:getState(), "pending")
|
|
|
|
-- Update past delay - animation should be ready to start
|
|
anim:update(0.3) -- Now delay elapsed is > 0.5
|
|
luaunit.assertEquals(anim:getState(), "pending") -- Still pending until next update
|
|
|
|
-- One more update to actually start
|
|
anim:update(0.01)
|
|
luaunit.assertEquals(anim:getState(), "playing")
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- Test Suite: Animation Interpolation
|
|
-- ============================================================================
|
|
|
|
TestAnimationInterpolation = {}
|
|
|
|
function TestAnimationInterpolation:setUp()
|
|
love.window.setMode(1920, 1080)
|
|
FlexLove.beginFrame()
|
|
end
|
|
|
|
function TestAnimationInterpolation:tearDown()
|
|
FlexLove.endFrame()
|
|
end
|
|
|
|
function TestAnimationInterpolation:test_interpolate_before_update()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
})
|
|
-- Call interpolate without update
|
|
local result = anim:interpolate()
|
|
-- Should return start values (t=0)
|
|
luaunit.assertAlmostEquals(result.opacity, 0, 0.01)
|
|
end
|
|
|
|
function TestAnimationInterpolation:test_interpolate_multiple_times_without_update()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
})
|
|
anim:update(0.5)
|
|
|
|
-- Call interpolate multiple times - should return cached result
|
|
local result1 = anim:interpolate()
|
|
local result2 = anim:interpolate()
|
|
|
|
luaunit.assertEquals(result1, result2) -- Should be same table
|
|
luaunit.assertAlmostEquals(result1.opacity, 0.5, 0.01)
|
|
end
|
|
|
|
function TestAnimationInterpolation:test_apply_with_empty_table()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
})
|
|
|
|
-- Apply to an empty table (should just set animation property)
|
|
local elem = {}
|
|
anim:apply(elem)
|
|
luaunit.assertNotNil(elem.animation)
|
|
luaunit.assertEquals(elem.animation, anim)
|
|
end
|
|
|
|
function TestAnimationInterpolation:test_cached_result()
|
|
-- Test that cached results work correctly
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result1 = anim:interpolate()
|
|
local result2 = anim:interpolate() -- Should use cached result
|
|
|
|
luaunit.assertEquals(result1, result2) -- Same table reference
|
|
luaunit.assertAlmostEquals(result1.x, 50, 0.01)
|
|
end
|
|
|
|
function TestAnimationInterpolation:test_result_invalidated_on_update()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result1 = anim:interpolate()
|
|
local x1 = result1.x -- Store value, not reference
|
|
|
|
anim:update(0.25) -- Update again
|
|
local result2 = anim:interpolate()
|
|
local x2 = result2.x
|
|
|
|
-- Should recalculate
|
|
-- Note: result1 and result2 are the same cached table, but values should be updated
|
|
luaunit.assertAlmostEquals(x1, 50, 0.01)
|
|
luaunit.assertAlmostEquals(x2, 75, 0.01)
|
|
-- result1.x will actually be 75 now since it's the same table reference
|
|
luaunit.assertAlmostEquals(result1.x, 75, 0.01)
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- Test Suite: Animation Helper Functions
|
|
-- ============================================================================
|
|
|
|
TestAnimationHelpers = {}
|
|
|
|
function TestAnimationHelpers:setUp()
|
|
love.window.setMode(1920, 1080)
|
|
FlexLove.beginFrame()
|
|
end
|
|
|
|
function TestAnimationHelpers:tearDown()
|
|
FlexLove.endFrame()
|
|
end
|
|
|
|
function TestAnimationHelpers:test_fade_with_negative_opacity()
|
|
local anim = Animation.fade(1, -1, 2)
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
-- Should interpolate even with negative values
|
|
luaunit.assertAlmostEquals(result.opacity, 0.5, 0.01)
|
|
end
|
|
|
|
function TestAnimationHelpers:test_fade_with_same_opacity()
|
|
local anim = Animation.fade(1, 0.5, 0.5)
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
luaunit.assertAlmostEquals(result.opacity, 0.5, 0.01)
|
|
end
|
|
|
|
function TestAnimationHelpers:test_fade_helper_backwards_compatibility()
|
|
local anim = Animation.fade(1, 0, 1)
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.opacity, 0.5, 0.01)
|
|
end
|
|
|
|
function TestAnimationHelpers:test_scale_with_negative_dimensions()
|
|
local anim = Animation.scale(1, { width = -100, height = -50 }, { width = 100, height = 50 })
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
-- Should interpolate even with negative values
|
|
luaunit.assertAlmostEquals(result.width, 0, 0.1)
|
|
luaunit.assertAlmostEquals(result.height, 0, 0.1)
|
|
end
|
|
|
|
function TestAnimationHelpers:test_scale_with_zero_dimensions()
|
|
local anim = Animation.scale(1, { width = 0, height = 0 }, { width = 100, height = 100 })
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
luaunit.assertAlmostEquals(result.width, 50, 0.1)
|
|
luaunit.assertAlmostEquals(result.height, 50, 0.1)
|
|
end
|
|
|
|
function TestAnimationHelpers:test_scale_helper_backwards_compatibility()
|
|
local anim = Animation.scale(1, { width = 100, height = 100 }, { width = 200, height = 200 })
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.width, 150, 0.01)
|
|
luaunit.assertAlmostEquals(result.height, 150, 0.01)
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- Test Suite: Animation Transform Property
|
|
-- ============================================================================
|
|
|
|
TestAnimationTransform = {}
|
|
|
|
function TestAnimationTransform:setUp()
|
|
love.window.setMode(1920, 1080)
|
|
FlexLove.beginFrame()
|
|
end
|
|
|
|
function TestAnimationTransform:tearDown()
|
|
FlexLove.endFrame()
|
|
end
|
|
|
|
function TestAnimationTransform:test_transform_property()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
transform = { rotation = 45 },
|
|
})
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
-- Transform should be applied
|
|
luaunit.assertEquals(result.rotation, 45)
|
|
end
|
|
|
|
function TestAnimationTransform:test_transform_with_multiple_properties()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
transform = { rotation = 45, scale = 2, custom = "value" },
|
|
})
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
luaunit.assertEquals(result.rotation, 45)
|
|
luaunit.assertEquals(result.scale, 2)
|
|
luaunit.assertEquals(result.custom, "value")
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- Test Suite: Easing Functions
|
|
-- ============================================================================
|
|
|
|
TestEasing = {}
|
|
|
|
function TestEasing:setUp()
|
|
love.window.setMode(1920, 1080)
|
|
FlexLove.beginFrame()
|
|
end
|
|
|
|
function TestEasing:tearDown()
|
|
FlexLove.endFrame()
|
|
end
|
|
|
|
-- Test that all easing functions exist
|
|
function TestEasing:test_all_easing_functions_exist()
|
|
local easings = {
|
|
-- Linear
|
|
"linear",
|
|
-- Quad
|
|
"easeInQuad",
|
|
"easeOutQuad",
|
|
"easeInOutQuad",
|
|
-- Cubic
|
|
"easeInCubic",
|
|
"easeOutCubic",
|
|
"easeInOutCubic",
|
|
-- Quart
|
|
"easeInQuart",
|
|
"easeOutQuart",
|
|
"easeInOutQuart",
|
|
-- Quint
|
|
"easeInQuint",
|
|
"easeOutQuint",
|
|
"easeInOutQuint",
|
|
-- Expo
|
|
"easeInExpo",
|
|
"easeOutExpo",
|
|
"easeInOutExpo",
|
|
-- Sine
|
|
"easeInSine",
|
|
"easeOutSine",
|
|
"easeInOutSine",
|
|
-- Circ
|
|
"easeInCirc",
|
|
"easeOutCirc",
|
|
"easeInOutCirc",
|
|
-- Back
|
|
"easeInBack",
|
|
"easeOutBack",
|
|
"easeInOutBack",
|
|
-- Elastic
|
|
"easeInElastic",
|
|
"easeOutElastic",
|
|
"easeInOutElastic",
|
|
-- Bounce
|
|
"easeInBounce",
|
|
"easeOutBounce",
|
|
"easeInOutBounce",
|
|
}
|
|
|
|
for _, name in ipairs(easings) do
|
|
luaunit.assertNotNil(Easing[name], "Easing function " .. name .. " should exist")
|
|
luaunit.assertEquals(type(Easing[name]), "function", name .. " should be a function")
|
|
end
|
|
end
|
|
|
|
function TestEasing:test_all_easing_functions_with_animation()
|
|
local easings = {
|
|
"linear",
|
|
"easeInQuad",
|
|
"easeOutQuad",
|
|
"easeInOutQuad",
|
|
"easeInCubic",
|
|
"easeOutCubic",
|
|
"easeInOutCubic",
|
|
"easeInQuart",
|
|
"easeOutQuart",
|
|
"easeInExpo",
|
|
"easeOutExpo",
|
|
}
|
|
|
|
for _, easingName in ipairs(easings) do
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
easing = easingName,
|
|
})
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
-- All should produce valid values
|
|
luaunit.assertNotNil(result.opacity)
|
|
luaunit.assertTrue(result.opacity >= 0 and result.opacity <= 1)
|
|
end
|
|
end
|
|
|
|
-- Test that all easing functions accept t parameter (0-1)
|
|
function TestEasing:test_easing_functions_accept_parameter()
|
|
local result = Easing.linear(0.5)
|
|
luaunit.assertNotNil(result)
|
|
luaunit.assertEquals(type(result), "number")
|
|
end
|
|
|
|
-- Test linear easing
|
|
function TestEasing:test_linear()
|
|
luaunit.assertEquals(Easing.linear(0), 0)
|
|
luaunit.assertEquals(Easing.linear(0.5), 0.5)
|
|
luaunit.assertEquals(Easing.linear(1), 1)
|
|
end
|
|
|
|
-- Test easeInQuad
|
|
function TestEasing:test_easeInQuad()
|
|
luaunit.assertEquals(Easing.easeInQuad(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeInQuad(0.5), 0.25, 0.01)
|
|
luaunit.assertEquals(Easing.easeInQuad(1), 1)
|
|
end
|
|
|
|
-- Test easeOutQuad
|
|
function TestEasing:test_easeOutQuad()
|
|
luaunit.assertEquals(Easing.easeOutQuad(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeOutQuad(0.5), 0.75, 0.01)
|
|
luaunit.assertEquals(Easing.easeOutQuad(1), 1)
|
|
end
|
|
|
|
-- Test easeInOutQuad
|
|
function TestEasing:test_easeInOutQuad()
|
|
luaunit.assertEquals(Easing.easeInOutQuad(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeInOutQuad(0.5), 0.5, 0.01)
|
|
luaunit.assertEquals(Easing.easeInOutQuad(1), 1)
|
|
end
|
|
|
|
-- Test easeInSine
|
|
function TestEasing:test_easeInSine()
|
|
luaunit.assertEquals(Easing.easeInSine(0), 0)
|
|
local mid = Easing.easeInSine(0.5)
|
|
luaunit.assertTrue(mid > 0 and mid < 1, "easeInSine(0.5) should be between 0 and 1")
|
|
luaunit.assertAlmostEquals(Easing.easeInSine(1), 1, 0.01)
|
|
end
|
|
|
|
-- Test easeOutSine
|
|
function TestEasing:test_easeOutSine()
|
|
luaunit.assertEquals(Easing.easeOutSine(0), 0)
|
|
local mid = Easing.easeOutSine(0.5)
|
|
luaunit.assertTrue(mid > 0 and mid < 1, "easeOutSine(0.5) should be between 0 and 1")
|
|
luaunit.assertAlmostEquals(Easing.easeOutSine(1), 1, 0.01)
|
|
end
|
|
|
|
-- Test easeInOutSine
|
|
function TestEasing:test_easeInOutSine()
|
|
luaunit.assertEquals(Easing.easeInOutSine(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeInOutSine(0.5), 0.5, 0.01)
|
|
luaunit.assertAlmostEquals(Easing.easeInOutSine(1), 1, 0.01)
|
|
end
|
|
|
|
-- Test easeInQuint
|
|
function TestEasing:test_easeInQuint()
|
|
luaunit.assertEquals(Easing.easeInQuint(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeInQuint(0.5), 0.03125, 0.01)
|
|
luaunit.assertEquals(Easing.easeInQuint(1), 1)
|
|
end
|
|
|
|
-- Test easeOutQuint
|
|
function TestEasing:test_easeOutQuint()
|
|
luaunit.assertEquals(Easing.easeOutQuint(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeOutQuint(0.5), 0.96875, 0.01)
|
|
luaunit.assertEquals(Easing.easeOutQuint(1), 1)
|
|
end
|
|
|
|
-- Test easeInCirc
|
|
function TestEasing:test_easeInCirc()
|
|
luaunit.assertEquals(Easing.easeInCirc(0), 0)
|
|
local mid = Easing.easeInCirc(0.5)
|
|
luaunit.assertTrue(mid > 0 and mid < 1, "easeInCirc(0.5) should be between 0 and 1")
|
|
luaunit.assertAlmostEquals(Easing.easeInCirc(1), 1, 0.01)
|
|
end
|
|
|
|
-- Test easeOutCirc
|
|
function TestEasing:test_easeOutCirc()
|
|
luaunit.assertEquals(Easing.easeOutCirc(0), 0)
|
|
local mid = Easing.easeOutCirc(0.5)
|
|
luaunit.assertTrue(mid > 0 and mid < 1, "easeOutCirc(0.5) should be between 0 and 1")
|
|
luaunit.assertAlmostEquals(Easing.easeOutCirc(1), 1, 0.01)
|
|
end
|
|
|
|
-- Test easeInOutCirc
|
|
function TestEasing:test_easeInOutCirc()
|
|
luaunit.assertEquals(Easing.easeInOutCirc(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeInOutCirc(0.5), 0.5, 0.01)
|
|
luaunit.assertAlmostEquals(Easing.easeInOutCirc(1), 1, 0.01)
|
|
end
|
|
|
|
-- Test easeInBack (should overshoot at start)
|
|
function TestEasing:test_easeInBack()
|
|
luaunit.assertEquals(Easing.easeInBack(0), 0)
|
|
local early = Easing.easeInBack(0.3)
|
|
luaunit.assertTrue(early < 0, "easeInBack should go negative (overshoot) early on")
|
|
luaunit.assertAlmostEquals(Easing.easeInBack(1), 1, 0.001)
|
|
end
|
|
|
|
-- Test easeOutBack (should overshoot at end)
|
|
function TestEasing:test_easeOutBack()
|
|
luaunit.assertAlmostEquals(Easing.easeOutBack(0), 0, 0.001)
|
|
local late = Easing.easeOutBack(0.7)
|
|
luaunit.assertTrue(late > 0.7, "easeOutBack should overshoot at the end")
|
|
luaunit.assertAlmostEquals(Easing.easeOutBack(1), 1, 0.01)
|
|
end
|
|
|
|
-- Test easeInElastic (should oscillate)
|
|
function TestEasing:test_easeInElastic()
|
|
luaunit.assertEquals(Easing.easeInElastic(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeInElastic(1), 1, 0.01)
|
|
-- Elastic should go negative at some point
|
|
local hasNegative = false
|
|
for i = 1, 9 do
|
|
local t = i / 10
|
|
if Easing.easeInElastic(t) < 0 then
|
|
hasNegative = true
|
|
break
|
|
end
|
|
end
|
|
luaunit.assertTrue(hasNegative, "easeInElastic should have negative values (oscillation)")
|
|
end
|
|
|
|
-- Test easeOutElastic (should oscillate)
|
|
function TestEasing:test_easeOutElastic()
|
|
luaunit.assertEquals(Easing.easeOutElastic(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeOutElastic(1), 1, 0.01)
|
|
-- Elastic should go above 1 at some point
|
|
local hasOvershoot = false
|
|
for i = 1, 9 do
|
|
local t = i / 10
|
|
if Easing.easeOutElastic(t) > 1 then
|
|
hasOvershoot = true
|
|
break
|
|
end
|
|
end
|
|
luaunit.assertTrue(hasOvershoot, "easeOutElastic should overshoot 1 (oscillation)")
|
|
end
|
|
|
|
-- Test easeInBounce
|
|
function TestEasing:test_easeInBounce()
|
|
luaunit.assertEquals(Easing.easeInBounce(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeInBounce(1), 1, 0.01)
|
|
-- Bounce should have multiple "bounces" (local minima)
|
|
local result = Easing.easeInBounce(0.5)
|
|
luaunit.assertTrue(result >= 0 and result <= 1, "easeInBounce should stay within 0-1 range")
|
|
end
|
|
|
|
-- Test easeOutBounce
|
|
function TestEasing:test_easeOutBounce()
|
|
luaunit.assertEquals(Easing.easeOutBounce(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeOutBounce(1), 1, 0.01)
|
|
-- Bounce should have bounces
|
|
local result = Easing.easeOutBounce(0.8)
|
|
luaunit.assertTrue(result >= 0 and result <= 1, "easeOutBounce should stay within 0-1 range")
|
|
end
|
|
|
|
-- Test easeInOutBounce
|
|
function TestEasing:test_easeInOutBounce()
|
|
luaunit.assertEquals(Easing.easeInOutBounce(0), 0)
|
|
luaunit.assertAlmostEquals(Easing.easeInOutBounce(0.5), 0.5, 0.01)
|
|
luaunit.assertAlmostEquals(Easing.easeInOutBounce(1), 1, 0.01)
|
|
end
|
|
|
|
function TestEasing:test_easeInExpo_at_zero()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
easing = "easeInExpo",
|
|
})
|
|
-- t=0 should return 0
|
|
local result = anim:interpolate()
|
|
luaunit.assertAlmostEquals(result.opacity, 0, 0.01)
|
|
end
|
|
|
|
function TestEasing:test_easeOutExpo_at_one()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { opacity = 0 },
|
|
final = { opacity = 1 },
|
|
easing = "easeOutExpo",
|
|
})
|
|
anim:update(1)
|
|
local result = anim:interpolate()
|
|
luaunit.assertAlmostEquals(result.opacity, 1.0, 0.01)
|
|
end
|
|
|
|
-- Test configurable back() factory
|
|
function TestEasing:test_back_factory()
|
|
local customBack = Easing.back(2.5)
|
|
luaunit.assertEquals(type(customBack), "function")
|
|
luaunit.assertEquals(customBack(0), 0)
|
|
luaunit.assertEquals(customBack(1), 1)
|
|
-- Should overshoot with custom amount
|
|
local mid = customBack(0.3)
|
|
luaunit.assertTrue(mid < 0, "Custom back easing should overshoot")
|
|
end
|
|
|
|
-- Test configurable elastic() factory
|
|
function TestEasing:test_elastic_factory()
|
|
local customElastic = Easing.elastic(1.5, 0.4)
|
|
luaunit.assertEquals(type(customElastic), "function")
|
|
luaunit.assertEquals(customElastic(0), 0)
|
|
luaunit.assertAlmostEquals(customElastic(1), 1, 0.01)
|
|
end
|
|
|
|
-- Test that all InOut easings are symmetric around 0.5
|
|
function TestEasing:test_inOut_symmetry()
|
|
local inOutEasings = {
|
|
"easeInOutQuad",
|
|
"easeInOutCubic",
|
|
"easeInOutQuart",
|
|
"easeInOutQuint",
|
|
"easeInOutExpo",
|
|
"easeInOutSine",
|
|
"easeInOutCirc",
|
|
"easeInOutBack",
|
|
"easeInOutElastic",
|
|
"easeInOutBounce",
|
|
}
|
|
|
|
for _, name in ipairs(inOutEasings) do
|
|
local easing = Easing[name]
|
|
-- At t=0.5, all InOut easings should be close to 0.5
|
|
local mid = easing(0.5)
|
|
luaunit.assertAlmostEquals(mid, 0.5, 0.1, name .. " should be close to 0.5 at t=0.5")
|
|
end
|
|
end
|
|
|
|
-- Test boundary conditions for all easings
|
|
function TestEasing:test_boundary_conditions()
|
|
local easings = {
|
|
"linear",
|
|
"easeInQuad",
|
|
"easeOutQuad",
|
|
"easeInOutQuad",
|
|
"easeInCubic",
|
|
"easeOutCubic",
|
|
"easeInOutCubic",
|
|
"easeInQuart",
|
|
"easeOutQuart",
|
|
"easeInOutQuart",
|
|
"easeInQuint",
|
|
"easeOutQuint",
|
|
"easeInOutQuint",
|
|
"easeInExpo",
|
|
"easeOutExpo",
|
|
"easeInOutExpo",
|
|
"easeInSine",
|
|
"easeOutSine",
|
|
"easeInOutSine",
|
|
"easeInCirc",
|
|
"easeOutCirc",
|
|
"easeInOutCirc",
|
|
"easeInBack",
|
|
"easeOutBack",
|
|
"easeInOutBack",
|
|
"easeInElastic",
|
|
"easeOutElastic",
|
|
"easeInOutElastic",
|
|
"easeInBounce",
|
|
"easeOutBounce",
|
|
"easeInOutBounce",
|
|
}
|
|
|
|
for _, name in ipairs(easings) do
|
|
local easing = Easing[name]
|
|
-- All easings should start at 0
|
|
local start = easing(0)
|
|
luaunit.assertAlmostEquals(start, 0, 0.01, name .. " should start at 0")
|
|
|
|
-- All easings should end at 1
|
|
local finish = easing(1)
|
|
luaunit.assertAlmostEquals(finish, 1, 0.01, name .. " should end at 1")
|
|
end
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- Test Suite: Animation Properties (Color, Position, Numeric, Tables)
|
|
-- ============================================================================
|
|
|
|
TestAnimationProperties = {}
|
|
|
|
function TestAnimationProperties:setUp()
|
|
love.window.setMode(1920, 1080)
|
|
FlexLove.beginFrame()
|
|
end
|
|
|
|
function TestAnimationProperties:tearDown()
|
|
FlexLove.endFrame()
|
|
end
|
|
|
|
-- Test Color.lerp() method
|
|
function TestAnimationProperties:test_color_lerp_midpoint()
|
|
local colorA = Color.new(0, 0, 0, 1) -- Black
|
|
local colorB = Color.new(1, 1, 1, 1) -- White
|
|
local result = Color.lerp(colorA, colorB, 0.5)
|
|
|
|
luaunit.assertAlmostEquals(result.r, 0.5, 0.01)
|
|
luaunit.assertAlmostEquals(result.g, 0.5, 0.01)
|
|
luaunit.assertAlmostEquals(result.b, 0.5, 0.01)
|
|
luaunit.assertAlmostEquals(result.a, 1, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_color_lerp_start_point()
|
|
local colorA = Color.new(1, 0, 0, 1) -- Red
|
|
local colorB = Color.new(0, 0, 1, 1) -- Blue
|
|
local result = Color.lerp(colorA, colorB, 0)
|
|
|
|
luaunit.assertAlmostEquals(result.r, 1, 0.01)
|
|
luaunit.assertAlmostEquals(result.g, 0, 0.01)
|
|
luaunit.assertAlmostEquals(result.b, 0, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_color_lerp_end_point()
|
|
local colorA = Color.new(1, 0, 0, 1) -- Red
|
|
local colorB = Color.new(0, 0, 1, 1) -- Blue
|
|
local result = Color.lerp(colorA, colorB, 1)
|
|
|
|
luaunit.assertAlmostEquals(result.r, 0, 0.01)
|
|
luaunit.assertAlmostEquals(result.g, 0, 0.01)
|
|
luaunit.assertAlmostEquals(result.b, 1, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_color_lerp_alpha()
|
|
local colorA = Color.new(1, 1, 1, 0) -- Transparent white
|
|
local colorB = Color.new(1, 1, 1, 1) -- Opaque white
|
|
local result = Color.lerp(colorA, colorB, 0.5)
|
|
|
|
luaunit.assertAlmostEquals(result.a, 0.5, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_color_lerp_clamp_t()
|
|
local colorA = Color.new(0, 0, 0, 1)
|
|
local colorB = Color.new(1, 1, 1, 1)
|
|
|
|
-- Test t > 1
|
|
local result1 = Color.lerp(colorA, colorB, 1.5)
|
|
luaunit.assertAlmostEquals(result1.r, 1, 0.01)
|
|
|
|
-- Test t < 0
|
|
local result2 = Color.lerp(colorA, colorB, -0.5)
|
|
luaunit.assertAlmostEquals(result2.r, 0, 0.01)
|
|
end
|
|
|
|
-- Test Position Animation (x, y)
|
|
function TestAnimationProperties:test_position_animation_x()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0 },
|
|
final = { x = 100 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.x, 50, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_position_animation_y()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { y = 0 },
|
|
final = { y = 200 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.y, 100, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_position_animation_xy()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 10, y = 20 },
|
|
final = { x = 110, y = 220 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.x, 60, 0.01)
|
|
luaunit.assertAlmostEquals(result.y, 120, 0.01)
|
|
end
|
|
|
|
-- Test Color Property Animation
|
|
function TestAnimationProperties:test_color_animation_backgroundColor()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { backgroundColor = Color.new(1, 0, 0, 1) }, -- Red
|
|
final = { backgroundColor = Color.new(0, 0, 1, 1) }, -- Blue
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertNotNil(result.backgroundColor)
|
|
luaunit.assertAlmostEquals(result.backgroundColor.r, 0.5, 0.01)
|
|
luaunit.assertAlmostEquals(result.backgroundColor.b, 0.5, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_color_animation_multiple_colors()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = {
|
|
backgroundColor = Color.new(1, 0, 0, 1),
|
|
borderColor = Color.new(0, 1, 0, 1),
|
|
textColor = Color.new(0, 0, 1, 1),
|
|
},
|
|
final = {
|
|
backgroundColor = Color.new(0, 1, 0, 1),
|
|
borderColor = Color.new(0, 0, 1, 1),
|
|
textColor = Color.new(1, 0, 0, 1),
|
|
},
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertNotNil(result.backgroundColor)
|
|
luaunit.assertNotNil(result.borderColor)
|
|
luaunit.assertNotNil(result.textColor)
|
|
|
|
-- Mid-point should be (0.5, 0.5, 0.5) for backgroundColor
|
|
luaunit.assertAlmostEquals(result.backgroundColor.r, 0.5, 0.01)
|
|
luaunit.assertAlmostEquals(result.backgroundColor.g, 0.5, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_color_animation_hex_colors()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { backgroundColor = "#FF0000" }, -- Red
|
|
final = { backgroundColor = "#0000FF" }, -- Blue
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertNotNil(result.backgroundColor)
|
|
luaunit.assertAlmostEquals(result.backgroundColor.r, 0.5, 0.01)
|
|
end
|
|
|
|
-- Test Numeric Property Animation
|
|
function TestAnimationProperties:test_numeric_animation_gap()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { gap = 0 },
|
|
final = { gap = 20 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.gap, 10, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_numeric_animation_image_opacity()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { imageOpacity = 0 },
|
|
final = { imageOpacity = 1 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.imageOpacity, 0.5, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_numeric_animation_border_width()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { borderWidth = 1 },
|
|
final = { borderWidth = 10 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.borderWidth, 5.5, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_numeric_animation_font_size()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { fontSize = 12 },
|
|
final = { fontSize = 24 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.fontSize, 18, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_numeric_animation_multiple_properties()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { gap = 0, imageOpacity = 0, borderWidth = 1 },
|
|
final = { gap = 20, imageOpacity = 1, borderWidth = 5 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.gap, 10, 0.01)
|
|
luaunit.assertAlmostEquals(result.imageOpacity, 0.5, 0.01)
|
|
luaunit.assertAlmostEquals(result.borderWidth, 3, 0.01)
|
|
end
|
|
|
|
-- Test Table Property Animation (padding, margin, cornerRadius)
|
|
function TestAnimationProperties:test_table_animation_padding()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { padding = { top = 0, right = 0, bottom = 0, left = 0 } },
|
|
final = { padding = { top = 10, right = 20, bottom = 10, left = 20 } },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertNotNil(result.padding)
|
|
luaunit.assertAlmostEquals(result.padding.top, 5, 0.01)
|
|
luaunit.assertAlmostEquals(result.padding.right, 10, 0.01)
|
|
luaunit.assertAlmostEquals(result.padding.bottom, 5, 0.01)
|
|
luaunit.assertAlmostEquals(result.padding.left, 10, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_table_animation_margin()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { margin = { top = 0, right = 0, bottom = 0, left = 0 } },
|
|
final = { margin = { top = 20, right = 20, bottom = 20, left = 20 } },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertNotNil(result.margin)
|
|
luaunit.assertAlmostEquals(result.margin.top, 10, 0.01)
|
|
luaunit.assertAlmostEquals(result.margin.right, 10, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_table_animation_corner_radius()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { cornerRadius = { topLeft = 0, topRight = 0, bottomLeft = 0, bottomRight = 0 } },
|
|
final = { cornerRadius = { topLeft = 10, topRight = 10, bottomLeft = 10, bottomRight = 10 } },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertNotNil(result.cornerRadius)
|
|
luaunit.assertAlmostEquals(result.cornerRadius.topLeft, 5, 0.01)
|
|
luaunit.assertAlmostEquals(result.cornerRadius.topRight, 5, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_table_animation_partial_keys()
|
|
-- Test when start and final have different keys
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { padding = { top = 0, left = 0 } },
|
|
final = { padding = { top = 10, right = 20, left = 10 } },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertNotNil(result.padding)
|
|
luaunit.assertAlmostEquals(result.padding.top, 5, 0.01)
|
|
luaunit.assertAlmostEquals(result.padding.left, 5, 0.01)
|
|
luaunit.assertNotNil(result.padding.right)
|
|
end
|
|
|
|
function TestAnimationProperties:test_table_animation_non_numeric_values()
|
|
-- Should skip non-numeric values in tables
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { padding = { top = 0, special = "value" } },
|
|
final = { padding = { top = 10, special = "value" } },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertNotNil(result.padding)
|
|
luaunit.assertAlmostEquals(result.padding.top, 5, 0.01)
|
|
end
|
|
|
|
-- Test Combined Animations
|
|
function TestAnimationProperties:test_combined_animation_all_types()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = {
|
|
width = 100,
|
|
height = 100,
|
|
x = 0,
|
|
y = 0,
|
|
opacity = 0,
|
|
backgroundColor = Color.new(1, 0, 0, 1),
|
|
gap = 0,
|
|
padding = { top = 0, left = 0 },
|
|
},
|
|
final = {
|
|
width = 200,
|
|
height = 200,
|
|
x = 100,
|
|
y = 100,
|
|
opacity = 1,
|
|
backgroundColor = Color.new(0, 0, 1, 1),
|
|
gap = 20,
|
|
padding = { top = 10, left = 10 },
|
|
},
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
-- Check all properties interpolated correctly
|
|
luaunit.assertAlmostEquals(result.width, 150, 0.01)
|
|
luaunit.assertAlmostEquals(result.height, 150, 0.01)
|
|
luaunit.assertAlmostEquals(result.x, 50, 0.01)
|
|
luaunit.assertAlmostEquals(result.y, 50, 0.01)
|
|
luaunit.assertAlmostEquals(result.opacity, 0.5, 0.01)
|
|
luaunit.assertAlmostEquals(result.gap, 10, 0.01)
|
|
luaunit.assertNotNil(result.backgroundColor)
|
|
luaunit.assertNotNil(result.padding)
|
|
end
|
|
|
|
function TestAnimationProperties:test_combined_animation_with_easing()
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { x = 0, backgroundColor = Color.new(0, 0, 0, 1) },
|
|
final = { x = 100, backgroundColor = Color.new(1, 1, 1, 1) },
|
|
easing = "easeInQuad",
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
-- With easeInQuad, at t=0.5, eased value should be 0.25
|
|
luaunit.assertAlmostEquals(result.x, 25, 0.01)
|
|
luaunit.assertAlmostEquals(result.backgroundColor.r, 0.25, 0.01)
|
|
end
|
|
|
|
function TestAnimationProperties:test_backwards_compatibility_width_height_opacity()
|
|
-- Ensure old animations still work
|
|
local anim = Animation.new({
|
|
duration = 1,
|
|
start = { width = 100, height = 100, opacity = 0 },
|
|
final = { width = 200, height = 200, opacity = 1 },
|
|
})
|
|
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.width, 150, 0.01)
|
|
luaunit.assertAlmostEquals(result.height, 150, 0.01)
|
|
luaunit.assertAlmostEquals(result.opacity, 0.5, 0.01)
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- Test Suite: Keyframe Animation
|
|
-- ============================================================================
|
|
|
|
TestKeyframeAnimation = {}
|
|
|
|
function TestKeyframeAnimation:setUp()
|
|
love.window.setMode(1920, 1080)
|
|
FlexLove.beginFrame()
|
|
end
|
|
|
|
function TestKeyframeAnimation:tearDown()
|
|
FlexLove.endFrame()
|
|
end
|
|
|
|
-- Test basic keyframe animation creation
|
|
function TestKeyframeAnimation:test_create_keyframe_animation()
|
|
local anim = Animation.keyframes({
|
|
duration = 2,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0, opacity = 0 } },
|
|
{ at = 1, values = { x = 100, opacity = 1 } },
|
|
},
|
|
})
|
|
|
|
luaunit.assertNotNil(anim)
|
|
luaunit.assertEquals(type(anim), "table")
|
|
luaunit.assertEquals(anim.duration, 2)
|
|
luaunit.assertNotNil(anim.keyframes)
|
|
luaunit.assertEquals(#anim.keyframes, 2)
|
|
end
|
|
|
|
-- Test keyframe animation with multiple waypoints
|
|
function TestKeyframeAnimation:test_multiple_waypoints()
|
|
local anim = Animation.keyframes({
|
|
duration = 3,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0, opacity = 0 } },
|
|
{ at = 0.25, values = { x = 50, opacity = 1 } },
|
|
{ at = 0.75, values = { x = 150, opacity = 1 } },
|
|
{ at = 1, values = { x = 200, opacity = 0 } },
|
|
},
|
|
})
|
|
|
|
luaunit.assertEquals(#anim.keyframes, 4)
|
|
luaunit.assertEquals(anim.keyframes[1].at, 0)
|
|
luaunit.assertEquals(anim.keyframes[2].at, 0.25)
|
|
luaunit.assertEquals(anim.keyframes[3].at, 0.75)
|
|
luaunit.assertEquals(anim.keyframes[4].at, 1)
|
|
end
|
|
|
|
-- Test keyframe sorting
|
|
function TestKeyframeAnimation:test_keyframe_sorting()
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 1, values = { x = 100 } },
|
|
{ at = 0, values = { x = 0 } },
|
|
{ at = 0.5, values = { x = 50 } },
|
|
},
|
|
})
|
|
|
|
-- Should be sorted by 'at' position
|
|
luaunit.assertEquals(anim.keyframes[1].at, 0)
|
|
luaunit.assertEquals(anim.keyframes[2].at, 0.5)
|
|
luaunit.assertEquals(anim.keyframes[3].at, 1)
|
|
end
|
|
|
|
-- Test keyframe interpolation at start
|
|
function TestKeyframeAnimation:test_interpolation_at_start()
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0, opacity = 0 } },
|
|
{ at = 1, values = { x = 100, opacity = 1 } },
|
|
},
|
|
})
|
|
|
|
anim.elapsed = 0
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertNotNil(result.x)
|
|
luaunit.assertNotNil(result.opacity)
|
|
luaunit.assertAlmostEquals(result.x, 0, 0.01)
|
|
luaunit.assertAlmostEquals(result.opacity, 0, 0.01)
|
|
end
|
|
|
|
-- Test keyframe interpolation at end
|
|
function TestKeyframeAnimation:test_interpolation_at_end()
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0, opacity = 0 } },
|
|
{ at = 1, values = { x = 100, opacity = 1 } },
|
|
},
|
|
})
|
|
|
|
anim.elapsed = 1
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.x, 100, 0.01)
|
|
luaunit.assertAlmostEquals(result.opacity, 1, 0.01)
|
|
end
|
|
|
|
-- Test keyframe interpolation at midpoint
|
|
function TestKeyframeAnimation:test_interpolation_at_midpoint()
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0 } },
|
|
{ at = 1, values = { x = 100 } },
|
|
},
|
|
})
|
|
|
|
anim.elapsed = 0.5
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.x, 50, 0.01)
|
|
end
|
|
|
|
-- Test per-keyframe easing
|
|
function TestKeyframeAnimation:test_per_keyframe_easing()
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0 }, easing = "easeInQuad" },
|
|
{ at = 0.5, values = { x = 50 }, easing = "linear" },
|
|
{ at = 1, values = { x = 100 } },
|
|
},
|
|
})
|
|
|
|
-- At t=0.25 (middle of first segment with easeInQuad)
|
|
anim.elapsed = 0.25
|
|
anim._resultDirty = true -- Mark dirty to force recalculation
|
|
local result1 = anim:interpolate()
|
|
-- easeInQuad at 0.5 should give 0.25, so x = 0 + (50-0) * 0.25 = 12.5
|
|
luaunit.assertTrue(result1.x < 25, "easeInQuad should slow start")
|
|
|
|
-- At t=0.75 (middle of second segment with linear)
|
|
anim.elapsed = 0.75
|
|
anim._resultDirty = true -- Mark dirty to force recalculation
|
|
local result2 = anim:interpolate()
|
|
-- linear at 0.5 should give 0.5, so x = 50 + (100-50) * 0.5 = 75
|
|
luaunit.assertAlmostEquals(result2.x, 75, 1)
|
|
end
|
|
|
|
-- Test findKeyframes method
|
|
function TestKeyframeAnimation:test_find_keyframes()
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0 } },
|
|
{ at = 0.25, values = { x = 25 } },
|
|
{ at = 0.75, values = { x = 75 } },
|
|
{ at = 1, values = { x = 100 } },
|
|
},
|
|
})
|
|
|
|
-- Test finding keyframes at different progress values
|
|
local prev1, next1 = anim:findKeyframes(0.1)
|
|
luaunit.assertEquals(prev1.at, 0)
|
|
luaunit.assertEquals(next1.at, 0.25)
|
|
|
|
local prev2, next2 = anim:findKeyframes(0.5)
|
|
luaunit.assertEquals(prev2.at, 0.25)
|
|
luaunit.assertEquals(next2.at, 0.75)
|
|
|
|
local prev3, next3 = anim:findKeyframes(0.9)
|
|
luaunit.assertEquals(prev3.at, 0.75)
|
|
luaunit.assertEquals(next3.at, 1)
|
|
end
|
|
|
|
-- Test keyframe animation with update
|
|
function TestKeyframeAnimation:test_keyframe_animation_update()
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0, values = { opacity = 0 } },
|
|
{ at = 1, values = { opacity = 1 } },
|
|
},
|
|
})
|
|
|
|
-- Update halfway through
|
|
anim:update(0.5)
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertAlmostEquals(result.opacity, 0.5, 0.01)
|
|
luaunit.assertFalse(anim:update(0)) -- Not complete yet
|
|
|
|
-- Update to completion
|
|
luaunit.assertTrue(anim:update(0.6)) -- Should complete
|
|
luaunit.assertEquals(anim:getState(), "completed")
|
|
end
|
|
|
|
-- Test keyframe animation with callbacks
|
|
function TestKeyframeAnimation:test_keyframe_animation_callbacks()
|
|
local startCalled = false
|
|
local updateCalled = false
|
|
local completeCalled = false
|
|
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0 } },
|
|
{ at = 1, values = { x = 100 } },
|
|
},
|
|
onStart = function()
|
|
startCalled = true
|
|
end,
|
|
onUpdate = function()
|
|
updateCalled = true
|
|
end,
|
|
onComplete = function()
|
|
completeCalled = true
|
|
end,
|
|
})
|
|
|
|
anim:update(0.5)
|
|
luaunit.assertTrue(startCalled)
|
|
luaunit.assertTrue(updateCalled)
|
|
luaunit.assertFalse(completeCalled)
|
|
|
|
anim:update(0.6)
|
|
luaunit.assertTrue(completeCalled)
|
|
end
|
|
|
|
-- Test missing keyframes (error handling)
|
|
function TestKeyframeAnimation:test_missing_keyframes()
|
|
-- Should create default keyframes with warning
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {},
|
|
})
|
|
|
|
luaunit.assertNotNil(anim)
|
|
luaunit.assertEquals(#anim.keyframes, 2) -- Should have default start and end
|
|
end
|
|
|
|
-- Test single keyframe (error handling)
|
|
function TestKeyframeAnimation:test_single_keyframe()
|
|
-- Should create default keyframes with warning
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0.5, values = { x = 50 } },
|
|
},
|
|
})
|
|
|
|
luaunit.assertNotNil(anim)
|
|
luaunit.assertTrue(#anim.keyframes >= 2) -- Should have at least 2 keyframes
|
|
end
|
|
|
|
-- Test keyframes without start (at=0)
|
|
function TestKeyframeAnimation:test_keyframes_without_start()
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0.5, values = { x = 50 } },
|
|
{ at = 1, values = { x = 100 } },
|
|
},
|
|
})
|
|
|
|
-- Should auto-add keyframe at 0
|
|
luaunit.assertEquals(anim.keyframes[1].at, 0)
|
|
luaunit.assertEquals(anim.keyframes[1].values.x, 50) -- Should copy first keyframe values
|
|
end
|
|
|
|
-- Test keyframes without end (at=1)
|
|
function TestKeyframeAnimation:test_keyframes_without_end()
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0 } },
|
|
{ at = 0.5, values = { x = 50 } },
|
|
},
|
|
})
|
|
|
|
-- Should auto-add keyframe at 1
|
|
luaunit.assertEquals(anim.keyframes[#anim.keyframes].at, 1)
|
|
luaunit.assertEquals(anim.keyframes[#anim.keyframes].values.x, 50) -- Should copy last keyframe values
|
|
end
|
|
|
|
-- Test keyframe with invalid props
|
|
function TestKeyframeAnimation:test_invalid_keyframe_props()
|
|
-- Should handle gracefully with warnings
|
|
local anim = Animation.keyframes({
|
|
duration = 0, -- Invalid
|
|
keyframes = "not a table", -- Invalid
|
|
})
|
|
|
|
luaunit.assertNotNil(anim)
|
|
luaunit.assertEquals(anim.duration, 1) -- Should use default
|
|
end
|
|
|
|
-- Test complex multi-property keyframes
|
|
function TestKeyframeAnimation:test_multi_property_keyframes()
|
|
local anim = Animation.keyframes({
|
|
duration = 2,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0, y = 0, opacity = 0, width = 50 } },
|
|
{ at = 0.33, values = { x = 100, y = 50, opacity = 1, width = 100 } },
|
|
{ at = 0.66, values = { x = 200, y = 100, opacity = 1, width = 150 } },
|
|
{ at = 1, values = { x = 300, y = 150, opacity = 0, width = 200 } },
|
|
},
|
|
})
|
|
|
|
-- Test interpolation at 0.5 (middle of second segment)
|
|
anim.elapsed = 1.0 -- t = 0.5
|
|
local result = anim:interpolate()
|
|
|
|
luaunit.assertNotNil(result.x)
|
|
luaunit.assertNotNil(result.y)
|
|
luaunit.assertNotNil(result.opacity)
|
|
luaunit.assertNotNil(result.width)
|
|
|
|
-- Should be interpolating between keyframes at 0.33 and 0.66
|
|
luaunit.assertTrue(result.x > 100 and result.x < 200)
|
|
luaunit.assertTrue(result.y > 50 and result.y < 100)
|
|
end
|
|
|
|
-- Test keyframe with easing function (not string)
|
|
function TestKeyframeAnimation:test_keyframe_with_easing_function()
|
|
local customEasing = function(t)
|
|
return t * t
|
|
end
|
|
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0 }, easing = customEasing },
|
|
{ at = 1, values = { x = 100 } },
|
|
},
|
|
})
|
|
|
|
anim.elapsed = 0.5
|
|
local result = anim:interpolate()
|
|
|
|
-- At t=0.5, easing(0.5) = 0.25, so x = 0 + 100 * 0.25 = 25
|
|
luaunit.assertAlmostEquals(result.x, 25, 1)
|
|
end
|
|
|
|
-- Test caching behavior with keyframes
|
|
function TestKeyframeAnimation:test_keyframe_caching()
|
|
local anim = Animation.keyframes({
|
|
duration = 1,
|
|
keyframes = {
|
|
{ at = 0, values = { x = 0 } },
|
|
{ at = 1, values = { x = 100 } },
|
|
},
|
|
})
|
|
|
|
anim.elapsed = 0.5
|
|
local result1 = anim:interpolate()
|
|
local result2 = anim:interpolate() -- Should return cached result
|
|
|
|
luaunit.assertEquals(result1, result2) -- Should be same table
|
|
end
|
|
|
|
-- Run all tests
|
|
if not _G.RUNNING_ALL_TESTS then
|
|
os.exit(luaunit.LuaUnit.run())
|
|
end
|