diff --git a/testing/__tests__/animation_test.lua b/testing/__tests__/animation_test.lua new file mode 100644 index 0000000..b6f7fda --- /dev/null +++ b/testing/__tests__/animation_test.lua @@ -0,0 +1,292 @@ +local luaunit = require("testing.luaunit") +require("testing.loveStub") + +local Animation = require("modules.Animation") + +TestAnimation = {} + +function TestAnimation:setUp() + -- Reset state before each test +end + +-- Unhappy path tests + +function TestAnimation:testNewWithNilDuration() + -- Duration is nil, elapsed will be 0, arithmetic should work but produce odd results + 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 TestAnimation:testNewWithNegativeDuration() + -- Should still create but behave oddly + local anim = Animation.new({ + duration = -1, + start = { opacity = 0 }, + final = { opacity = 1 }, + }) + luaunit.assertNotNil(anim) + -- Update with positive dt should immediately finish + local done = anim:update(1) + luaunit.assertTrue(done) +end + +function TestAnimation:testNewWithZeroDuration() + local anim = Animation.new({ + duration = 0, + start = { opacity = 0 }, + final = { opacity = 1 }, + }) + luaunit.assertNotNil(anim) + -- Should be instantly complete + local done = anim:update(0.001) + luaunit.assertTrue(done) +end + +function TestAnimation:testNewWithInvalidEasing() + 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 TestAnimation:testNewWithNilEasing() + local anim = Animation.new({ + duration = 1, + start = { opacity = 0 }, + final = { opacity = 1 }, + easing = nil, + }) + -- Should use linear as default + luaunit.assertNotNil(anim) +end + +function TestAnimation:testNewWithMissingStartValues() + 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 TestAnimation:testNewWithMissingFinalValues() + 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 TestAnimation:testNewWithMismatchedProperties() + 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 + +function TestAnimation:testUpdateWithNegativeDt() + 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 TestAnimation:testUpdateWithHugeDt() + 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 TestAnimation:testInterpolateBeforeUpdate() + 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 TestAnimation:testInterpolateMultipleTimesWithoutUpdate() + 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 TestAnimation:testApplyWithEmptyTable() + 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 TestAnimation:testFadeWithNegativeOpacity() + 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 TestAnimation:testFadeWithSameOpacity() + 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 TestAnimation:testScaleWithNegativeDimensions() + 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 TestAnimation:testScaleWithZeroDimensions() + 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 TestAnimation:testAllEasingFunctions() + 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 + +function TestAnimation:testEaseInExpoAtZero() + 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 TestAnimation:testEaseOutExpoAtOne() + 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 + +function TestAnimation:testTransformProperty() + 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 TestAnimation:testTransformWithMultipleProperties() + 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 + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/element_test.lua b/testing/__tests__/element_test.lua index 0135bc0..3641ae7 100644 --- a/testing/__tests__/element_test.lua +++ b/testing/__tests__/element_test.lua @@ -29,9 +29,9 @@ function TestElementCreation:test_create_minimal_element() x = 10, y = 20, width = 100, - height = 50 + height = 50, }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.id, "test1") luaunit.assertEquals(element.x, 10) @@ -47,9 +47,9 @@ function TestElementCreation:test_element_with_text() y = 0, width = 200, height = 100, - text = "Hello World" + text = "Hello World", }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.text, "Hello World") end @@ -61,9 +61,9 @@ function TestElementCreation:test_element_with_backgroundColor() y = 0, width = 100, height = 100, - backgroundColor = {1, 0, 0, 1} + backgroundColor = { 1, 0, 0, 1 }, }) - + luaunit.assertNotNil(element) luaunit.assertNotNil(element.backgroundColor) end @@ -74,18 +74,18 @@ function TestElementCreation:test_element_with_children() x = 0, y = 0, width = 300, - height = 200 + height = 200, }) - + local child = FlexLove.new({ id = "child1", x = 10, y = 10, width = 50, height = 50, - parent = parent + parent = parent, }) - + luaunit.assertNotNil(parent) luaunit.assertNotNil(child) luaunit.assertEquals(child.parent, parent) @@ -100,9 +100,9 @@ function TestElementCreation:test_element_with_padding() y = 0, width = 200, height = 100, - padding = { horizontal = 10, vertical = 10 } + padding = { horizontal = 10, vertical = 10 }, }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.padding.left, 10) luaunit.assertEquals(element.padding.top, 10) @@ -117,9 +117,9 @@ function TestElementCreation:test_element_with_margin() y = 0, width = 200, height = 100, - margin = { horizontal = 5, vertical = 5 } + margin = { horizontal = 5, vertical = 5 }, }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.margin.left, 5) luaunit.assertEquals(element.margin.top, 5) @@ -144,9 +144,9 @@ function TestElementSizing:test_getBorderBoxWidth() x = 0, y = 0, width = 100, - height = 50 + height = 50, }) - + local borderBoxWidth = element:getBorderBoxWidth() luaunit.assertEquals(borderBoxWidth, 100) end @@ -157,9 +157,9 @@ function TestElementSizing:test_getBorderBoxHeight() x = 0, y = 0, width = 100, - height = 50 + height = 50, }) - + local borderBoxHeight = element:getBorderBoxHeight() luaunit.assertEquals(borderBoxHeight, 50) end @@ -170,9 +170,9 @@ function TestElementSizing:test_getBounds() x = 10, y = 20, width = 100, - height = 50 + height = 50, }) - + local bounds = element:getBounds() luaunit.assertEquals(bounds.x, 10) luaunit.assertEquals(bounds.y, 20) @@ -186,9 +186,9 @@ function TestElementSizing:test_contains_point_inside() x = 10, y = 20, width = 100, - height = 50 + height = 50, }) - + local contains = element:contains(50, 40) luaunit.assertTrue(contains) end @@ -199,9 +199,9 @@ function TestElementSizing:test_contains_point_outside() x = 10, y = 20, width = 100, - height = 50 + height = 50, }) - + local contains = element:contains(150, 100) luaunit.assertFalse(contains) end @@ -212,13 +212,13 @@ function TestElementSizing:test_contains_point_on_edge() x = 10, y = 20, width = 100, - height = 50 + height = 50, }) - + -- Point on right edge local contains = element:contains(110, 40) luaunit.assertTrue(contains) - + -- Point on bottom edge contains = element:contains(50, 70) luaunit.assertTrue(contains) @@ -241,18 +241,18 @@ function TestElementUnits:test_element_with_percentage_width() x = 0, y = 0, width = 1000, - height = 500 + height = 500, }) - + local child = FlexLove.new({ id = "child_pct", x = 0, y = 0, width = "50%", height = 100, - parent = parent + parent = parent, }) - + luaunit.assertNotNil(child) -- Width should be resolved to 500 (50% of parent's 1000) luaunit.assertEquals(child.width, 500) @@ -263,10 +263,10 @@ function TestElementUnits:test_element_with_viewport_units() id = "viewport1", x = 0, y = 0, - width = "50vw", -- 50% of viewport width (1920) = 960 - height = "25vh" -- 25% of viewport height (1080) = 270 + width = "50vw", -- 50% of viewport width (1920) = 960 + height = "25vh", -- 25% of viewport height (1080) = 270 }) - + luaunit.assertNotNil(element) -- Units should be resolved immediately to numbers luaunit.assertEquals(type(element.width), "number") @@ -294,9 +294,9 @@ function TestElementPositioning:test_element_absolute_position() y = 200, width = 50, height = 50, - positioning = "absolute" + positioning = "absolute", }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.positioning, "absolute") end @@ -307,18 +307,18 @@ function TestElementPositioning:test_nested_element_positions() x = 100, y = 100, width = 300, - height = 200 + height = 200, }) - + local child = FlexLove.new({ id = "nest_child", x = 20, y = 30, width = 50, height = 50, - parent = parent + parent = parent, }) - + luaunit.assertNotNil(parent) luaunit.assertNotNil(child) -- Child positions are absolute in FlexLove, not relative to parent @@ -346,9 +346,9 @@ function TestElementFlex:test_element_with_flex_direction() width = 300, height = 200, positioning = "flex", - flexDirection = "horizontal" + flexDirection = "horizontal", }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.flexDirection, "horizontal") end @@ -361,9 +361,9 @@ function TestElementFlex:test_element_with_flex_properties() width = 300, height = 200, positioning = "flex", - flexDirection = "horizontal" + flexDirection = "horizontal", }) - + local element = FlexLove.new({ id = "flex2", parent = parent, @@ -371,9 +371,9 @@ function TestElementFlex:test_element_with_flex_properties() height = 100, flexGrow = 1, flexShrink = 0, - flexBasis = "auto" + flexBasis = "auto", }) - + luaunit.assertNotNil(element) -- Just check element was created successfully -- Flex properties are handled by LayoutEngine, not stored on element @@ -389,9 +389,9 @@ function TestElementFlex:test_element_with_gap() width = 300, height = 200, positioning = "flex", - gap = 10 + gap = 10, }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.gap, 10) end @@ -414,9 +414,9 @@ function TestElementStyling:test_element_with_border() y = 0, width = 100, height = 100, - border = 2 + border = 2, }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.border, 2) end @@ -428,9 +428,9 @@ function TestElementStyling:test_element_with_corner_radius() y = 0, width = 100, height = 100, - cornerRadius = 10 + cornerRadius = 10, }) - + luaunit.assertNotNil(element) -- Corner radius might be stored as a table luaunit.assertNotNil(element.cornerRadius) @@ -444,9 +444,9 @@ function TestElementStyling:test_element_with_text_align() width = 200, height = 100, text = "Centered Text", - textAlign = "center" + textAlign = "center", }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.textAlign, "center") end @@ -458,9 +458,9 @@ function TestElementStyling:test_element_with_opacity() y = 0, width = 100, height = 100, - opacity = 0.5 + opacity = 0.5, }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.opacity, 0.5) end @@ -473,9 +473,9 @@ function TestElementStyling:test_element_with_border_color() width = 100, height = 100, border = 2, - borderColor = {1, 0, 0, 1} + borderColor = { 1, 0, 0, 1 }, }) - + luaunit.assertNotNil(element) luaunit.assertNotNil(element.borderColor) end @@ -498,9 +498,9 @@ function TestElementMethods:test_element_setText() y = 0, width = 200, height = 100, - text = "Initial" + text = "Initial", }) - + element:setText("Updated") luaunit.assertEquals(element.text, "Updated") end @@ -511,17 +511,17 @@ function TestElementMethods:test_element_addChild() x = 0, y = 0, width = 300, - height = 200 + height = 200, }) - + local child = FlexLove.new({ id = "child_add", x = 10, y = 10, width = 50, - height = 50 + height = 50, }) - + parent:addChild(child) luaunit.assertEquals(#parent.children, 1) luaunit.assertEquals(parent.children[1], child) @@ -545,9 +545,9 @@ function TestElementScroll:test_scrollable_element_with_overflow() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.overflow, "scroll") luaunit.assertNotNil(element._scrollManager) @@ -560,12 +560,12 @@ function TestElementScroll:test_setScrollPosition() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + element:setScrollPosition(50, 100) local scrollX, scrollY = element:getScrollPosition() - + -- Note: actual scroll may be clamped based on content luaunit.assertNotNil(scrollX) luaunit.assertNotNil(scrollY) @@ -578,13 +578,13 @@ function TestElementScroll:test_scrollBy() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + local initialX, initialY = element:getScrollPosition() element:scrollBy(10, 20) local newX, newY = element:getScrollPosition() - + luaunit.assertNotNil(newX) luaunit.assertNotNil(newY) end @@ -596,9 +596,9 @@ function TestElementScroll:test_scrollToTop() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + element:scrollToTop() local _, scrollY = element:getScrollPosition() luaunit.assertEquals(scrollY, 0) @@ -611,9 +611,9 @@ function TestElementScroll:test_scrollToBottom() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + element:scrollToBottom() -- Bottom position depends on content, just verify it doesn't error local _, scrollY = element:getScrollPosition() @@ -627,9 +627,9 @@ function TestElementScroll:test_scrollToLeft() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + element:scrollToLeft() local scrollX, _ = element:getScrollPosition() luaunit.assertEquals(scrollX, 0) @@ -642,9 +642,9 @@ function TestElementScroll:test_scrollToRight() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + element:scrollToRight() local scrollX, _ = element:getScrollPosition() luaunit.assertNotNil(scrollX) @@ -657,9 +657,9 @@ function TestElementScroll:test_getMaxScroll() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + local maxX, maxY = element:getMaxScroll() luaunit.assertNotNil(maxX) luaunit.assertNotNil(maxY) @@ -672,9 +672,9 @@ function TestElementScroll:test_getScrollPercentage() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + local percentX, percentY = element:getScrollPercentage() luaunit.assertNotNil(percentX) luaunit.assertNotNil(percentY) @@ -689,9 +689,9 @@ function TestElementScroll:test_hasOverflow() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + local hasOverflowX, hasOverflowY = element:hasOverflow() luaunit.assertNotNil(hasOverflowX) luaunit.assertNotNil(hasOverflowY) @@ -704,9 +704,9 @@ function TestElementScroll:test_getContentSize() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + local contentWidth, contentHeight = element:getContentSize() luaunit.assertNotNil(contentWidth) luaunit.assertNotNil(contentHeight) @@ -729,9 +729,9 @@ function TestElementGeometry:test_getBounds() x = 10, y = 20, width = 100, - height = 50 + height = 50, }) - + local bounds = element:getBounds() luaunit.assertEquals(bounds.x, 10) luaunit.assertEquals(bounds.y, 20) @@ -745,9 +745,9 @@ function TestElementGeometry:test_contains_point_inside() x = 10, y = 20, width = 100, - height = 50 + height = 50, }) - + luaunit.assertTrue(element:contains(50, 40)) end @@ -757,9 +757,9 @@ function TestElementGeometry:test_contains_point_outside() x = 10, y = 20, width = 100, - height = 50 + height = 50, }) - + luaunit.assertFalse(element:contains(200, 200)) end @@ -769,9 +769,9 @@ function TestElementGeometry:test_getBorderBoxWidth_no_border() x = 0, y = 0, width = 100, - height = 50 + height = 50, }) - + local borderBoxWidth = element:getBorderBoxWidth() luaunit.assertEquals(borderBoxWidth, 100) end @@ -782,9 +782,9 @@ function TestElementGeometry:test_getBorderBoxHeight_no_border() x = 0, y = 0, width = 100, - height = 50 + height = 50, }) - + local borderBoxHeight = element:getBorderBoxHeight() luaunit.assertEquals(borderBoxHeight, 50) end @@ -796,9 +796,9 @@ function TestElementGeometry:test_getBorderBoxWidth_with_border() y = 0, width = 100, height = 50, - border = { left = 2, right = 2, top = 0, bottom = 0 } + border = { left = 2, right = 2, top = 0, bottom = 0 }, }) - + local borderBoxWidth = element:getBorderBoxWidth() -- Width includes left + right borders luaunit.assertTrue(borderBoxWidth >= 100) @@ -811,9 +811,9 @@ function TestElementGeometry:test_getAvailableContentWidth() y = 0, width = 200, height = 100, - padding = { top = 10, right = 10, bottom = 10, left = 10 } + padding = { top = 10, right = 10, bottom = 10, left = 10 }, }) - + local availWidth = element:getAvailableContentWidth() luaunit.assertNotNil(availWidth) -- Should be less than total width due to padding @@ -827,9 +827,9 @@ function TestElementGeometry:test_getAvailableContentHeight() y = 0, width = 200, height = 100, - padding = { top = 10, right = 10, bottom = 10, left = 10 } + padding = { top = 10, right = 10, bottom = 10, left = 10 }, }) - + local availHeight = element:getAvailableContentHeight() luaunit.assertNotNil(availHeight) -- Should be less than total height due to padding @@ -843,9 +843,9 @@ function TestElementGeometry:test_getScaledContentPadding() y = 0, width = 200, height = 100, - padding = { top = 10, right = 10, bottom = 10, left = 10 } + padding = { top = 10, right = 10, bottom = 10, left = 10 }, }) - + local padding = element:getScaledContentPadding() -- May be nil if no theme component with contentPadding if padding then @@ -873,17 +873,17 @@ function TestElementChildren:test_addChild() x = 0, y = 0, width = 200, - height = 200 + height = 200, }) - + local child = FlexLove.new({ id = "child", x = 10, y = 10, width = 50, - height = 50 + height = 50, }) - + parent:addChild(child) luaunit.assertEquals(#parent.children, 1) luaunit.assertEquals(parent.children[1], child) @@ -896,20 +896,20 @@ function TestElementChildren:test_removeChild() x = 0, y = 0, width = 200, - height = 200 + height = 200, }) - + local child = FlexLove.new({ id = "child", x = 10, y = 10, width = 50, - height = 50 + height = 50, }) - + parent:addChild(child) parent:removeChild(child) - + luaunit.assertEquals(#parent.children, 0) luaunit.assertNil(child.parent) end @@ -920,16 +920,16 @@ function TestElementChildren:test_clearChildren() x = 0, y = 0, width = 200, - height = 200 + height = 200, }) - + local child1 = FlexLove.new({ id = "child1", x = 0, y = 0, width = 50, height = 50 }) local child2 = FlexLove.new({ id = "child2", x = 0, y = 0, width = 50, height = 50 }) - + parent:addChild(child1) parent:addChild(child2) parent:clearChildren() - + luaunit.assertEquals(#parent.children, 0) end @@ -939,15 +939,15 @@ function TestElementChildren:test_getChildCount() x = 0, y = 0, width = 200, - height = 200 + height = 200, }) - + local child1 = FlexLove.new({ id = "child1", x = 0, y = 0, width = 50, height = 50 }) local child2 = FlexLove.new({ id = "child2", x = 0, y = 0, width = 50, height = 50 }) - + parent:addChild(child1) parent:addChild(child2) - + luaunit.assertEquals(parent:getChildCount(), 2) end @@ -969,9 +969,9 @@ function TestElementVisibility:test_visibility_visible() y = 0, width = 100, height = 100, - visibility = "visible" + visibility = "visible", }) - + luaunit.assertEquals(element.visibility, "visible") end @@ -982,9 +982,9 @@ function TestElementVisibility:test_visibility_hidden() y = 0, width = 100, height = 100, - visibility = "hidden" + visibility = "hidden", }) - + luaunit.assertEquals(element.visibility, "hidden") end @@ -994,9 +994,9 @@ function TestElementVisibility:test_opacity_default() x = 0, y = 0, width = 100, - height = 100 + height = 100, }) - + luaunit.assertEquals(element.opacity, 1) end @@ -1007,9 +1007,9 @@ function TestElementVisibility:test_opacity_custom() y = 0, width = 100, height = 100, - opacity = 0.5 + opacity = 0.5, }) - + luaunit.assertEquals(element.opacity, 0.5) end @@ -1032,9 +1032,9 @@ function TestElementTextEditing:test_editable_element() width = 200, height = 40, editable = true, - text = "Edit me" + text = "Edit me", }) - + luaunit.assertTrue(element.editable) luaunit.assertNotNil(element._textEditor) end @@ -1047,9 +1047,9 @@ function TestElementTextEditing:test_placeholder_text() width = 200, height = 40, editable = true, - placeholder = "Enter text..." + placeholder = "Enter text...", }) - + luaunit.assertEquals(element.placeholder, "Enter text...") end @@ -1071,9 +1071,9 @@ function TestElementAdditional:test_element_with_z_index() y = 0, width = 100, height = 100, - z = 10 + z = 10, }) - + luaunit.assertEquals(element.z, 10) end @@ -1084,16 +1084,16 @@ function TestElementAdditional:test_element_with_text() y = 0, width = 100, height = 100, - text = "Hello World" + text = "Hello World", }) - + luaunit.assertEquals(element.text, "Hello World") end function TestElementAdditional:test_element_with_text_color() local Color = require("modules.Color") local textColor = Color.new(255, 0, 0, 1) - + local element = FlexLove.new({ id = "test", x = 0, @@ -1101,25 +1101,25 @@ function TestElementAdditional:test_element_with_text_color() width = 100, height = 100, text = "Red text", - textColor = textColor + textColor = textColor, }) - + luaunit.assertEquals(element.textColor, textColor) end function TestElementAdditional:test_element_with_background_color() local Color = require("modules.Color") local bgColor = Color.new(0, 0, 255, 1) - + local element = FlexLove.new({ id = "test", x = 0, y = 0, width = 100, height = 100, - backgroundColor = bgColor + backgroundColor = bgColor, }) - + luaunit.assertEquals(element.backgroundColor, bgColor) end @@ -1130,9 +1130,9 @@ function TestElementAdditional:test_element_with_corner_radius() y = 0, width = 100, height = 100, - cornerRadius = 10 + cornerRadius = 10, }) - + luaunit.assertNotNil(element.cornerRadius) luaunit.assertEquals(element.cornerRadius.topLeft, 10) luaunit.assertEquals(element.cornerRadius.topRight, 10) @@ -1147,9 +1147,9 @@ function TestElementAdditional:test_element_with_margin() y = 0, width = 100, height = 100, - margin = { top = 5, right = 10, bottom = 5, left = 10 } + margin = { top = 5, right = 10, bottom = 5, left = 10 }, }) - + luaunit.assertNotNil(element.margin) luaunit.assertEquals(element.margin.top, 5) luaunit.assertEquals(element.margin.right, 10) @@ -1163,18 +1163,18 @@ function TestElementAdditional:test_element_destroy() x = 0, y = 0, width = 200, - height = 200 + height = 200, }) - + local child = FlexLove.new({ id = "child", parent = parent, x = 0, y = 0, width = 50, - height = 50 + height = 50, }) - + luaunit.assertEquals(#parent.children, 1) child:destroy() luaunit.assertNil(child.parent) @@ -1187,9 +1187,9 @@ function TestElementAdditional:test_element_with_disabled() y = 0, width = 100, height = 100, - disabled = true + disabled = true, }) - + luaunit.assertTrue(element.disabled) end @@ -1200,30 +1200,29 @@ function TestElementAdditional:test_element_with_active() y = 0, width = 100, height = 100, - active = true + active = true, }) - + luaunit.assertTrue(element.active) end function TestElementAdditional:test_element_with_userdata() local customData = { foo = "bar", count = 42 } - + local element = FlexLove.new({ id = "test", x = 0, y = 0, width = 100, height = 100, - userdata = customData + userdata = customData, }) - + luaunit.assertEquals(element.userdata, customData) luaunit.assertEquals(element.userdata.foo, "bar") luaunit.assertEquals(element.userdata.count, 42) end - if not _G.RUNNING_ALL_TESTS then os.exit(luaunit.LuaUnit.run()) end diff --git a/testing/__tests__/error_handler_test.lua b/testing/__tests__/error_handler_test.lua new file mode 100644 index 0000000..dee5ea6 --- /dev/null +++ b/testing/__tests__/error_handler_test.lua @@ -0,0 +1,141 @@ +-- Test suite for ErrorHandler module +package.path = package.path .. ";./?.lua;./modules/?.lua" + +require("testing.loveStub") +local luaunit = require("testing.luaunit") +local ErrorHandler = require("modules.ErrorHandler") + +TestErrorHandler = {} + +-- Test: error() throws with correct format +function TestErrorHandler:test_error_throws_with_format() + local success, err = pcall(function() + ErrorHandler.error("TestModule", "Something went wrong") + end) + + luaunit.assertFalse(success, "error() should throw") + luaunit.assertStrContains(err, "[FlexLove - TestModule] Error: Something went wrong") +end + +-- Test: warn() prints with correct format +function TestErrorHandler:test_warn_prints_with_format() + -- Capture print output by mocking print + local captured = nil + local originalPrint = print + print = function(msg) + captured = msg + end + + ErrorHandler.warn("TestModule", "This is a warning") + + print = originalPrint + + luaunit.assertNotNil(captured, "warn() should print") + luaunit.assertEquals(captured, "[FlexLove - TestModule] Warning: This is a warning") +end + +-- Test: assertNotNil returns true for non-nil value +function TestErrorHandler:test_assertNotNil_returns_true_for_valid() + local result = ErrorHandler.assertNotNil("TestModule", "some value", "testParam") + luaunit.assertTrue(result, "assertNotNil should return true for non-nil value") +end + +-- Test: assertNotNil throws for nil value +function TestErrorHandler:test_assertNotNil_throws_for_nil() + local success, err = pcall(function() + ErrorHandler.assertNotNil("TestModule", nil, "testParam") + end) + + luaunit.assertFalse(success, "assertNotNil should throw for nil") + luaunit.assertStrContains(err, "Parameter 'testParam' cannot be nil") +end + +-- Test: assertType returns true for correct type +function TestErrorHandler:test_assertType_returns_true_for_valid() + local result = ErrorHandler.assertType("TestModule", "hello", "string", "testParam") + luaunit.assertTrue(result, "assertType should return true for correct type") + + result = ErrorHandler.assertType("TestModule", 123, "number", "testParam") + luaunit.assertTrue(result, "assertType should return true for number") + + result = ErrorHandler.assertType("TestModule", {}, "table", "testParam") + luaunit.assertTrue(result, "assertType should return true for table") +end + +-- Test: assertType throws for wrong type +function TestErrorHandler:test_assertType_throws_for_wrong_type() + local success, err = pcall(function() + ErrorHandler.assertType("TestModule", 123, "string", "testParam") + end) + + luaunit.assertFalse(success, "assertType should throw for wrong type") + luaunit.assertStrContains(err, "Parameter 'testParam' must be string, got number") +end + +-- Test: assertRange returns true for value in range +function TestErrorHandler:test_assertRange_returns_true_for_valid() + local result = ErrorHandler.assertRange("TestModule", 5, 0, 10, "testParam") + luaunit.assertTrue(result, "assertRange should return true for value in range") + + result = ErrorHandler.assertRange("TestModule", 0, 0, 10, "testParam") + luaunit.assertTrue(result, "assertRange should accept min boundary") + + result = ErrorHandler.assertRange("TestModule", 10, 0, 10, "testParam") + luaunit.assertTrue(result, "assertRange should accept max boundary") +end + +-- Test: assertRange throws for value below min +function TestErrorHandler:test_assertRange_throws_for_below_min() + local success, err = pcall(function() + ErrorHandler.assertRange("TestModule", -1, 0, 10, "testParam") + end) + + luaunit.assertFalse(success, "assertRange should throw for value below min") + luaunit.assertStrContains(err, "Parameter 'testParam' must be between 0 and 10, got -1") +end + +-- Test: assertRange throws for value above max +function TestErrorHandler:test_assertRange_throws_for_above_max() + local success, err = pcall(function() + ErrorHandler.assertRange("TestModule", 11, 0, 10, "testParam") + end) + + luaunit.assertFalse(success, "assertRange should throw for value above max") + luaunit.assertStrContains(err, "Parameter 'testParam' must be between 0 and 10, got 11") +end + +-- Test: warnDeprecated prints deprecation warning +function TestErrorHandler:test_warnDeprecated_prints_message() + local captured = nil + local originalPrint = print + print = function(msg) + captured = msg + end + + ErrorHandler.warnDeprecated("TestModule", "oldFunction", "newFunction") + + print = originalPrint + + luaunit.assertNotNil(captured, "warnDeprecated should print") + luaunit.assertStrContains(captured, "'oldFunction' is deprecated. Use 'newFunction' instead") +end + +-- Test: warnCommonMistake prints helpful message +function TestErrorHandler:test_warnCommonMistake_prints_message() + local captured = nil + local originalPrint = print + print = function(msg) + captured = msg + end + + ErrorHandler.warnCommonMistake("TestModule", "Width is zero", "Set width to positive value") + + print = originalPrint + + luaunit.assertNotNil(captured, "warnCommonMistake should print") + luaunit.assertStrContains(captured, "Width is zero. Suggestion: Set width to positive value") +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/event_handler_test.lua b/testing/__tests__/event_handler_test.lua new file mode 100644 index 0000000..f6ad24e --- /dev/null +++ b/testing/__tests__/event_handler_test.lua @@ -0,0 +1,522 @@ +-- Test suite for EventHandler module +package.path = package.path .. ";./?.lua;./modules/?.lua" + +require("testing.loveStub") +local luaunit = require("testing.luaunit") +local EventHandler = require("modules.EventHandler") +local InputEvent = require("modules.InputEvent") +local utils = require("modules.utils") + +TestEventHandler = {} + +-- Mock Context module +local MockContext = { + getContext = function() + return {} + end, +} + +-- Helper to create EventHandler with dependencies +local function createEventHandler(config) + config = config or {} + return EventHandler.new(config, { + InputEvent = InputEvent, + Context = MockContext, + utils = utils, + }) +end + +-- Mock element +local function createMockElement() + return { + x = 0, + y = 0, + width = 100, + height = 100, + disabled = false, + editable = false, + _focused = false, + padding = { left = 0, right = 0, top = 0, bottom = 0 }, + _borderBoxWidth = 100, + _borderBoxHeight = 100, + isFocused = function(self) + return self._focused + end, + focus = function(self) + self._focused = true + end, + } +end + +-- Test: new() creates instance with defaults +function TestEventHandler:test_new_creates_with_defaults() + local handler = createEventHandler() + + luaunit.assertNotNil(handler) + luaunit.assertNil(handler.onEvent) + luaunit.assertNotNil(handler._pressed) + luaunit.assertEquals(handler._clickCount, 0) + luaunit.assertFalse(handler._hovered) + luaunit.assertFalse(handler._scrollbarPressHandled) +end + +-- Test: new() accepts custom config +function TestEventHandler:test_new_accepts_custom_config() + local onEventCalled = false + local handler = createEventHandler({ + onEvent = function() + onEventCalled = true + end, + _clickCount = 5, + _hovered = true, + }) + + luaunit.assertNotNil(handler.onEvent) + luaunit.assertEquals(handler._clickCount, 5) + luaunit.assertTrue(handler._hovered) +end + +-- Test: initialize() sets element reference +function TestEventHandler:test_initialize_sets_element() + local handler = createEventHandler() + local element = createMockElement() + + handler:initialize(element) + + luaunit.assertEquals(handler._element, element) +end + +-- Test: getState() returns state data +function TestEventHandler:test_getState_returns_state() + local handler = createEventHandler({ + _clickCount = 3, + _hovered = true, + }) + handler._pressed[1] = true + handler._lastClickTime = 12345 + + local state = handler:getState() + + luaunit.assertNotNil(state) + luaunit.assertEquals(state._clickCount, 3) + luaunit.assertTrue(state._hovered) + luaunit.assertTrue(state._pressed[1]) + luaunit.assertEquals(state._lastClickTime, 12345) +end + +-- Test: setState() restores state +function TestEventHandler:test_setState_restores_state() + local handler = createEventHandler() + + local state = { + _pressed = { [1] = true }, + _clickCount = 2, + _hovered = true, + _lastClickTime = 5000, + _lastClickButton = 1, + } + + handler:setState(state) + + luaunit.assertEquals(handler._clickCount, 2) + luaunit.assertTrue(handler._hovered) + luaunit.assertTrue(handler._pressed[1]) + luaunit.assertEquals(handler._lastClickTime, 5000) + luaunit.assertEquals(handler._lastClickButton, 1) +end + +-- Test: setState() handles nil gracefully +function TestEventHandler:test_setState_handles_nil() + local handler = createEventHandler() + handler._clickCount = 5 + + handler:setState(nil) + + -- Should not error, should preserve original state + luaunit.assertEquals(handler._clickCount, 5) +end + +-- Test: setState() uses defaults for missing values +function TestEventHandler:test_setState_uses_defaults() + local handler = createEventHandler() + + handler:setState({}) -- Empty state + + luaunit.assertNotNil(handler._pressed) + luaunit.assertEquals(handler._clickCount, 0) + luaunit.assertFalse(handler._hovered) +end + +-- Test: resetScrollbarPressFlag() resets flag +function TestEventHandler:test_resetScrollbarPressFlag() + local handler = createEventHandler() + handler._scrollbarPressHandled = true + + handler:resetScrollbarPressFlag() + + luaunit.assertFalse(handler._scrollbarPressHandled) +end + +-- Test: isAnyButtonPressed() returns false when no buttons pressed +function TestEventHandler:test_isAnyButtonPressed_returns_false() + local handler = createEventHandler() + + luaunit.assertFalse(handler:isAnyButtonPressed()) +end + +-- Test: isAnyButtonPressed() returns true when button pressed +function TestEventHandler:test_isAnyButtonPressed_returns_true() + local handler = createEventHandler() + handler._pressed[1] = true + + luaunit.assertTrue(handler:isAnyButtonPressed()) +end + +-- Test: isButtonPressed() checks specific button +function TestEventHandler:test_isButtonPressed_checks_specific_button() + local handler = createEventHandler() + handler._pressed[1] = true + handler._pressed[2] = false + + luaunit.assertTrue(handler:isButtonPressed(1)) + luaunit.assertFalse(handler:isButtonPressed(2)) + luaunit.assertFalse(handler:isButtonPressed(3)) +end + +-- Test: processMouseEvents() returns early if no element +function TestEventHandler:test_processMouseEvents_no_element() + local handler = createEventHandler() + + -- Should not error + handler:processMouseEvents(50, 50, true, true) +end + +-- Test: processMouseEvents() handles press event +function TestEventHandler:test_processMouseEvents_press() + local handler = createEventHandler() + local element = createMockElement() + handler:initialize(element) + + local eventReceived = nil + handler.onEvent = function(el, event) + eventReceived = event + end + + -- Mock love.mouse.isDown for button 1 + local originalIsDown = love.mouse.isDown + love.mouse.isDown = function(button) + return button == 1 + end + + -- First call - button just pressed + handler:processMouseEvents(50, 50, true, true) + + luaunit.assertNotNil(eventReceived) + luaunit.assertEquals(eventReceived.type, "press") + luaunit.assertEquals(eventReceived.button, 1) + luaunit.assertTrue(handler._pressed[1]) + + love.mouse.isDown = originalIsDown +end + +-- Test: processMouseEvents() handles drag event +function TestEventHandler:test_processMouseEvents_drag() + local handler = createEventHandler() + local element = createMockElement() + handler:initialize(element) + + local eventsReceived = {} + handler.onEvent = function(el, event) + table.insert(eventsReceived, event) + end + + local originalIsDown = love.mouse.isDown + love.mouse.isDown = function(button) + return button == 1 + end + + -- First call - press at (50, 50) + handler:processMouseEvents(50, 50, true, true) + + -- Second call - drag to (60, 70) + handler:processMouseEvents(60, 70, true, true) + + luaunit.assertTrue(#eventsReceived >= 2) + -- Find drag event + local dragEvent = nil + for _, event in ipairs(eventsReceived) do + if event.type == "drag" then + dragEvent = event + break + end + end + + luaunit.assertNotNil(dragEvent, "Should receive drag event") + luaunit.assertEquals(dragEvent.dx, 10) + luaunit.assertEquals(dragEvent.dy, 20) + + love.mouse.isDown = originalIsDown +end + +-- Test: processMouseEvents() handles release and click +function TestEventHandler:test_processMouseEvents_release_and_click() + local handler = createEventHandler() + local element = createMockElement() + handler:initialize(element) + + local eventsReceived = {} + handler.onEvent = function(el, event) + table.insert(eventsReceived, event) + end + + local isButtonDown = true + local originalIsDown = love.mouse.isDown + love.mouse.isDown = function(button) + return button == 1 and isButtonDown + end + + -- Press + handler:processMouseEvents(50, 50, true, true) + + -- Release + isButtonDown = false + handler:processMouseEvents(50, 50, true, true) + + -- Should have: press, click, release events + luaunit.assertTrue(#eventsReceived >= 3) + + local hasPress = false + local hasClick = false + local hasRelease = false + + for _, event in ipairs(eventsReceived) do + if event.type == "press" then + hasPress = true + end + if event.type == "click" then + hasClick = true + end + if event.type == "release" then + hasRelease = true + end + end + + luaunit.assertTrue(hasPress, "Should have press event") + luaunit.assertTrue(hasClick, "Should have click event") + luaunit.assertTrue(hasRelease, "Should have release event") + + love.mouse.isDown = originalIsDown +end + +-- Test: processMouseEvents() detects double-click +function TestEventHandler:test_processMouseEvents_double_click() + local handler = createEventHandler() + local element = createMockElement() + handler:initialize(element) + + local eventsReceived = {} + handler.onEvent = function(el, event) + table.insert(eventsReceived, event) + end + + local isButtonDown = false + local originalIsDown = love.mouse.isDown + love.mouse.isDown = function(button) + return button == 1 and isButtonDown + end + + -- First click + isButtonDown = true + handler:processMouseEvents(50, 50, true, true) + isButtonDown = false + handler:processMouseEvents(50, 50, true, true) + + -- Second click (quickly after first) + isButtonDown = true + handler:processMouseEvents(50, 50, true, true) + isButtonDown = false + handler:processMouseEvents(50, 50, true, true) + + -- Find click events + local clickEvents = {} + for _, event in ipairs(eventsReceived) do + if event.type == "click" then + table.insert(clickEvents, event) + end + end + + luaunit.assertTrue(#clickEvents >= 2) + -- Second click should have clickCount = 2 + if #clickEvents >= 2 then + luaunit.assertEquals(clickEvents[2].clickCount, 2) + end + + love.mouse.isDown = originalIsDown +end + +-- Test: processMouseEvents() handles rightclick +function TestEventHandler:test_processMouseEvents_rightclick() + local handler = createEventHandler() + local element = createMockElement() + handler:initialize(element) + + local eventsReceived = {} + handler.onEvent = function(el, event) + table.insert(eventsReceived, event) + end + + local isButtonDown = false + local originalIsDown = love.mouse.isDown + love.mouse.isDown = function(button) + return button == 2 and isButtonDown + end + + -- Right click press and release + isButtonDown = true + handler:processMouseEvents(50, 50, true, true) + isButtonDown = false + handler:processMouseEvents(50, 50, true, true) + + local hasRightClick = false + for _, event in ipairs(eventsReceived) do + if event.type == "rightclick" then + hasRightClick = true + luaunit.assertEquals(event.button, 2) + end + end + + luaunit.assertTrue(hasRightClick, "Should have rightclick event") + + love.mouse.isDown = originalIsDown +end + +-- Test: processMouseEvents() handles middleclick +function TestEventHandler:test_processMouseEvents_middleclick() + local handler = createEventHandler() + local element = createMockElement() + handler:initialize(element) + + local eventsReceived = {} + handler.onEvent = function(el, event) + table.insert(eventsReceived, event) + end + + local isButtonDown = false + local originalIsDown = love.mouse.isDown + love.mouse.isDown = function(button) + return button == 3 and isButtonDown + end + + -- Middle click press and release + isButtonDown = true + handler:processMouseEvents(50, 50, true, true) + isButtonDown = false + handler:processMouseEvents(50, 50, true, true) + + local hasMiddleClick = false + for _, event in ipairs(eventsReceived) do + if event.type == "middleclick" then + hasMiddleClick = true + luaunit.assertEquals(event.button, 3) + end + end + + luaunit.assertTrue(hasMiddleClick, "Should have middleclick event") + + love.mouse.isDown = originalIsDown +end + +-- Test: processMouseEvents() respects disabled state +function TestEventHandler:test_processMouseEvents_disabled() + local handler = createEventHandler() + local element = createMockElement() + element.disabled = true + handler:initialize(element) + + local eventReceived = false + handler.onEvent = function(el, event) + eventReceived = true + end + + local originalIsDown = love.mouse.isDown + love.mouse.isDown = function(button) + return button == 1 + end + + handler:processMouseEvents(50, 50, true, true) + + -- Should not fire event for disabled element + luaunit.assertFalse(eventReceived) + + love.mouse.isDown = originalIsDown +end + +-- Test: processTouchEvents() handles touch +function TestEventHandler:test_processTouchEvents() + local handler = createEventHandler() + local element = createMockElement() + handler:initialize(element) + + local eventsReceived = {} + handler.onEvent = function(el, event) + table.insert(eventsReceived, event) + end + + -- Mock touch API + local originalGetTouches = love.touch.getTouches + local originalGetPosition = love.touch.getPosition + + love.touch.getTouches = function() + return { "touch1" } + end + + love.touch.getPosition = function(id) + if id == "touch1" then + return 150, 150 -- Outside element bounds + end + end + + -- First call - touch starts inside + love.touch.getPosition = function(id) + if id == "touch1" then + return 50, 50 -- Inside element + end + end + handler:processTouchEvents() + + -- Second call - touch moves outside + love.touch.getPosition = function(id) + if id == "touch1" then + return 150, 150 -- Outside element + end + end + handler:processTouchEvents() + + -- Should receive touch event + luaunit.assertTrue(#eventsReceived >= 1) + + love.touch.getTouches = originalGetTouches + love.touch.getPosition = originalGetPosition +end + +-- Test: processTouchEvents() returns early if no element +function TestEventHandler:test_processTouchEvents_no_element() + local handler = createEventHandler() + + -- Should not error + handler:processTouchEvents() +end + +-- Test: processTouchEvents() returns early if no onEvent +function TestEventHandler:test_processTouchEvents_no_onEvent() + local handler = createEventHandler() + local element = createMockElement() + handler:initialize(element) + + -- Should not error (no onEvent callback) + handler:processTouchEvents() +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/grid_test.lua b/testing/__tests__/grid_test.lua index e58a6c5..1eb84e9 100644 --- a/testing/__tests__/grid_test.lua +++ b/testing/__tests__/grid_test.lua @@ -24,20 +24,20 @@ function TestGridLayout:test_default_grid_single_child() y = 0, width = 400, height = 300, - positioning = "grid" + positioning = "grid", -- Default: gridRows=1, gridColumns=1 }) - + local child = FlexLove.new({ id = "child1", parent = container, - width = 50, -- Will be stretched by grid - height = 50 + width = 50, -- Will be stretched by grid + height = 50, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Child should be stretched to fill the entire grid cell luaunit.assertEquals(child.x, 0, "Child should be at x=0") luaunit.assertEquals(child.y, 0, "Child should be at y=0") @@ -55,37 +55,37 @@ function TestGridLayout:test_2x2_grid_four_children() height = 400, positioning = "grid", gridRows = 2, - gridColumns = 2 + gridColumns = 2, }) - + local children = {} for i = 1, 4 do children[i] = FlexLove.new({ id = "child" .. i, parent = container, width = 50, - height = 50 + height = 50, }) end - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Each cell should be 200x200 -- Child 1: top-left (0, 0) luaunit.assertEquals(children[1].x, 0, "Child 1 should be at x=0") luaunit.assertEquals(children[1].y, 0, "Child 1 should be at y=0") luaunit.assertEquals(children[1].width, 200, "Cell width should be 200") luaunit.assertEquals(children[1].height, 200, "Cell height should be 200") - + -- Child 2: top-right (200, 0) luaunit.assertEquals(children[2].x, 200, "Child 2 should be at x=200") luaunit.assertEquals(children[2].y, 0, "Child 2 should be at y=0") - + -- Child 3: bottom-left (0, 200) luaunit.assertEquals(children[3].x, 0, "Child 3 should be at x=0") luaunit.assertEquals(children[3].y, 200, "Child 3 should be at y=200") - + -- Child 4: bottom-right (200, 200) luaunit.assertEquals(children[4].x, 200, "Child 4 should be at x=200") luaunit.assertEquals(children[4].y, 200, "Child 4 should be at y=200") @@ -97,36 +97,36 @@ function TestGridLayout:test_grid_with_gaps() id = "grid", x = 0, y = 0, - width = 420, -- 2 cells * 200 + 1 gap * 20 - height = 320, -- 2 cells * 150 + 1 gap * 20 + width = 420, -- 2 cells * 200 + 1 gap * 20 + height = 320, -- 2 cells * 150 + 1 gap * 20 positioning = "grid", gridRows = 2, gridColumns = 2, columnGap = 20, - rowGap = 20 + rowGap = 20, }) - + local children = {} for i = 1, 4 do children[i] = FlexLove.new({ id = "child" .. i, parent = container, width = 50, - height = 50 + height = 50, }) end - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Cell size: (420 - 20) / 2 = 200, (320 - 20) / 2 = 150 luaunit.assertEquals(children[1].width, 200, "Cell width should be 200") luaunit.assertEquals(children[1].height, 150, "Cell height should be 150") - + -- Child 2 should be offset by cell width + gap luaunit.assertEquals(children[2].x, 220, "Child 2 x = 200 + 20 gap") luaunit.assertEquals(children[2].y, 0, "Child 2 should be at y=0") - + -- Child 3 should be offset by cell height + gap luaunit.assertEquals(children[3].x, 0, "Child 3 should be at x=0") luaunit.assertEquals(children[3].y, 170, "Child 3 y = 150 + 20 gap") @@ -142,27 +142,27 @@ function TestGridLayout:test_grid_overflow_children() height = 200, positioning = "grid", gridRows = 2, - gridColumns = 2 + gridColumns = 2, -- Only 4 cells available }) - + local children = {} - for i = 1, 6 do -- 6 children, but only 4 cells + for i = 1, 6 do -- 6 children, but only 4 cells children[i] = FlexLove.new({ id = "child" .. i, parent = container, width = 50, - height = 50 + height = 50, }) end - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- First 4 children should be positioned luaunit.assertNotNil(children[1].x, "Child 1 should be positioned") luaunit.assertNotNil(children[4].x, "Child 4 should be positioned") - + -- Children 5 and 6 should NOT be positioned (or positioned at 0,0 by default) -- This tests the overflow behavior: row >= rows breaks the loop end @@ -178,19 +178,19 @@ function TestGridLayout:test_grid_align_center() positioning = "grid", gridRows = 2, gridColumns = 2, - alignItems = "center" + alignItems = "center", }) - + local child = FlexLove.new({ id = "child1", parent = container, width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Cell is 200x200, child is 100x100, should be centered -- Center position: (200 - 100) / 2 = 50 luaunit.assertEquals(child.x, 50, "Child should be centered horizontally in cell") @@ -210,19 +210,19 @@ function TestGridLayout:test_grid_align_flex_start() positioning = "grid", gridRows = 2, gridColumns = 2, - alignItems = "flex-start" + alignItems = "flex-start", }) - + local child = FlexLove.new({ id = "child1", parent = container, width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Child should be at top-left of cell luaunit.assertEquals(child.x, 0, "Child should be at left of cell") luaunit.assertEquals(child.y, 0, "Child should be at top of cell") @@ -241,19 +241,19 @@ function TestGridLayout:test_grid_align_flex_end() positioning = "grid", gridRows = 2, gridColumns = 2, - alignItems = "flex-end" + alignItems = "flex-end", }) - + local child = FlexLove.new({ id = "child1", parent = container, width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Cell is 200x200, child is 100x100, should be at bottom-right luaunit.assertEquals(child.x, 100, "Child should be at right of cell (200 - 100)") luaunit.assertEquals(child.y, 100, "Child should be at bottom of cell (200 - 100)") @@ -267,24 +267,24 @@ function TestGridLayout:test_grid_with_padding() id = "grid", x = 0, y = 0, - width = 500, -- Total width + width = 500, -- Total width height = 500, padding = { top = 50, right = 50, bottom = 50, left = 50 }, positioning = "grid", gridRows = 2, - gridColumns = 2 + gridColumns = 2, }) - + local child = FlexLove.new({ id = "child1", parent = container, width = 50, - height = 50 + height = 50, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Available space: 500 - 50 - 50 = 400 -- Cell size: 400 / 2 = 200 -- Child should be positioned at padding.left, padding.top @@ -304,17 +304,17 @@ function TestGridLayout:test_grid_with_absolute_child() height = 400, positioning = "grid", gridRows = 2, - gridColumns = 2 + gridColumns = 2, }) - + -- Regular child local child1 = FlexLove.new({ id = "child1", parent = container, width = 50, - height = 50 + height = 50, }) - + -- Absolutely positioned child (should be ignored by grid layout) local child2 = FlexLove.new({ id = "child2", @@ -323,28 +323,28 @@ function TestGridLayout:test_grid_with_absolute_child() x = 10, y = 10, width = 30, - height = 30 + height = 30, }) - + -- Another regular child local child3 = FlexLove.new({ id = "child3", parent = container, width = 50, - height = 50 + height = 50, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- child1 should be in first grid cell (0, 0) luaunit.assertEquals(child1.x, 0, "Child 1 should be at x=0") luaunit.assertEquals(child1.y, 0, "Child 1 should be at y=0") - + -- child2 should keep its absolute position luaunit.assertEquals(child2.x, 10, "Absolute child should keep x=10") luaunit.assertEquals(child2.y, 10, "Absolute child should keep y=10") - + -- child3 should be in second grid cell (200, 0), not third luaunit.assertEquals(child3.x, 200, "Child 3 should be in second cell at x=200") luaunit.assertEquals(child3.y, 0, "Child 3 should be in second cell at y=0") @@ -360,13 +360,13 @@ function TestGridLayout:test_empty_grid() height = 400, positioning = "grid", gridRows = 2, - gridColumns = 2 + gridColumns = 2, -- No children }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Should not crash luaunit.assertEquals(#container.children, 0, "Grid should have no children") end @@ -380,21 +380,21 @@ function TestGridLayout:test_grid_zero_dimensions() width = 400, height = 400, positioning = "grid", - gridRows = 0, -- Invalid: 0 rows - gridColumns = 0 -- Invalid: 0 columns + gridRows = 0, -- Invalid: 0 rows + gridColumns = 0, -- Invalid: 0 columns }) - + local child = FlexLove.new({ id = "child1", parent = container, width = 50, - height = 50 + height = 50, }) - + -- This might cause division by zero or other errors FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Test passes if it doesn't crash luaunit.assertTrue(true, "Grid with 0 dimensions should not crash") end @@ -409,9 +409,9 @@ function TestGridLayout:test_nested_grids() height = 400, positioning = "grid", gridRows = 2, - gridColumns = 2 + gridColumns = 2, }) - + -- First cell contains another grid local innerGrid = FlexLove.new({ id = "inner", @@ -420,22 +420,22 @@ function TestGridLayout:test_nested_grids() height = 200, positioning = "grid", gridRows = 2, - gridColumns = 2 + gridColumns = 2, }) - + -- Add children to inner grid for i = 1, 4 do FlexLove.new({ id = "inner_child" .. i, parent = innerGrid, width = 25, - height = 25 + height = 25, }) end - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Inner grid should be positioned in first cell of outer grid luaunit.assertEquals(innerGrid.x, 0, "Inner grid should be at x=0") luaunit.assertEquals(innerGrid.y, 0, "Inner grid should be at y=0") @@ -452,9 +452,9 @@ function TestGridLayout:test_grid_with_reserved_space() height = 400, positioning = "grid", gridRows = 2, - gridColumns = 2 + gridColumns = 2, }) - + -- Absolute child with left positioning (reserves left space) FlexLove.new({ id = "absolute_left", @@ -463,20 +463,20 @@ function TestGridLayout:test_grid_with_reserved_space() left = 0, top = 0, width = 50, - height = 50 + height = 50, }) - + -- Regular grid child local child1 = FlexLove.new({ id = "child1", parent = container, width = 50, - height = 50 + height = 50, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Grid should account for reserved space -- Available width: 400 - 50 (reserved left) = 350 -- Cell width: 350 / 2 = 175 diff --git a/testing/__tests__/image_cache_test.lua b/testing/__tests__/image_cache_test.lua new file mode 100644 index 0000000..3a912af --- /dev/null +++ b/testing/__tests__/image_cache_test.lua @@ -0,0 +1,166 @@ +local luaunit = require("testing.luaunit") +require("testing.loveStub") + +local ImageCache = require("modules.ImageCache") + +TestImageCache = {} + +function TestImageCache:setUp() + -- Clear cache before each test + ImageCache.clear() +end + +function TestImageCache:tearDown() + ImageCache.clear() +end + +-- Unhappy path tests + +function TestImageCache:testLoadWithNilPath() + local img, err = ImageCache.load(nil) + luaunit.assertNil(img) + luaunit.assertNotNil(err) + luaunit.assertStrContains(err, "Invalid image path") +end + +function TestImageCache:testLoadWithEmptyString() + local img, err = ImageCache.load("") + luaunit.assertNil(img) + luaunit.assertNotNil(err) + luaunit.assertStrContains(err, "Invalid image path") +end + +function TestImageCache:testLoadWithInvalidType() + local img, err = ImageCache.load(123) + luaunit.assertNil(img) + luaunit.assertNotNil(err) + luaunit.assertStrContains(err, "Invalid image path") +end + +function TestImageCache:testLoadWithInvalidPath() + local img, err = ImageCache.load("nonexistent/path/to/image.png") + luaunit.assertNil(img) + luaunit.assertNotNil(err) + luaunit.assertStrContains(err, "Failed to load image") +end + +function TestImageCache:testLoadWithInvalidExtension() + local img, err = ImageCache.load("test.txt") + luaunit.assertNil(img) + luaunit.assertNotNil(err) + luaunit.assertStrContains(err, "Failed to load image") +end + +function TestImageCache:testGetWithNilPath() + local img = ImageCache.get(nil) + luaunit.assertNil(img) +end + +function TestImageCache:testGetWithEmptyString() + local img = ImageCache.get("") + luaunit.assertNil(img) +end + +function TestImageCache:testGetWithInvalidType() + local img = ImageCache.get({}) + luaunit.assertNil(img) +end + +function TestImageCache:testGetWithNonCachedPath() + local img = ImageCache.get("some/random/path.png") + luaunit.assertNil(img) +end + +function TestImageCache:testGetImageDataWithNilPath() + local imgData = ImageCache.getImageData(nil) + luaunit.assertNil(imgData) +end + +function TestImageCache:testGetImageDataWithEmptyString() + local imgData = ImageCache.getImageData("") + luaunit.assertNil(imgData) +end + +function TestImageCache:testGetImageDataWithInvalidType() + local imgData = ImageCache.getImageData(123) + luaunit.assertNil(imgData) +end + +function TestImageCache:testGetImageDataWithNonCachedPath() + local imgData = ImageCache.getImageData("some/path.png") + luaunit.assertNil(imgData) +end + +function TestImageCache:testRemoveWithNilPath() + local removed = ImageCache.remove(nil) + luaunit.assertFalse(removed) +end + +function TestImageCache:testRemoveWithEmptyString() + local removed = ImageCache.remove("") + luaunit.assertFalse(removed) +end + +function TestImageCache:testRemoveWithInvalidType() + local removed = ImageCache.remove(123) + luaunit.assertFalse(removed) +end + +function TestImageCache:testRemoveWithNonCachedPath() + local removed = ImageCache.remove("uncached/image.png") + luaunit.assertFalse(removed) +end + +function TestImageCache:testClearEmptyCache() + -- Should not error on empty cache + ImageCache.clear() + local stats = ImageCache.getStats() + luaunit.assertEquals(stats.count, 0) + luaunit.assertEquals(stats.memoryEstimate, 0) +end + +function TestImageCache:testGetStatsOnEmptyCache() + ImageCache.clear() + local stats = ImageCache.getStats() + luaunit.assertNotNil(stats) + luaunit.assertEquals(stats.count, 0) + luaunit.assertEquals(stats.memoryEstimate, 0) +end + +function TestImageCache:testPathNormalization() + -- Test that paths with different slashes are normalized + local path1 = "themes/space.png" + local path2 = "themes\\space.png" -- Windows style + + -- Both should normalize to the same path + -- (If first load fails, both should fail the same way) + local img1, err1 = ImageCache.load(path1) + local img2, err2 = ImageCache.load(path2) + + -- Both should have same result (either both nil or both same image) + if img1 == nil then + luaunit.assertNil(img2) + else + luaunit.assertEquals(img1, img2) + end +end + +function TestImageCache:testLoadWithImageDataFlag() + -- Test loading with imageData flag on invalid path + local img, err = ImageCache.load("invalid/path.png", true) + luaunit.assertNil(img) + luaunit.assertNotNil(err) +end + +function TestImageCache:testMultipleClearCalls() + -- Clear multiple times should not error + ImageCache.clear() + ImageCache.clear() + ImageCache.clear() + local stats = ImageCache.getStats() + luaunit.assertEquals(stats.count, 0) +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/image_renderer_test.lua b/testing/__tests__/image_renderer_test.lua new file mode 100644 index 0000000..b6e7e1a --- /dev/null +++ b/testing/__tests__/image_renderer_test.lua @@ -0,0 +1,247 @@ +local luaunit = require("testing.luaunit") +require("testing.loveStub") + +local ImageRenderer = require("modules.ImageRenderer") + +TestImageRenderer = {} + +function TestImageRenderer:setUp() + -- Create a mock image for testing + self.mockImage = { + getDimensions = function() + return 100, 100 + end, + } +end + +-- Unhappy path tests for calculateFit + +function TestImageRenderer:testCalculateFitWithZeroImageWidth() + luaunit.assertError(function() + ImageRenderer.calculateFit(0, 100, 200, 200, "fill") + end) +end + +function TestImageRenderer:testCalculateFitWithZeroImageHeight() + luaunit.assertError(function() + ImageRenderer.calculateFit(100, 0, 200, 200, "fill") + end) +end + +function TestImageRenderer:testCalculateFitWithNegativeImageWidth() + luaunit.assertError(function() + ImageRenderer.calculateFit(-100, 100, 200, 200, "fill") + end) +end + +function TestImageRenderer:testCalculateFitWithNegativeImageHeight() + luaunit.assertError(function() + ImageRenderer.calculateFit(100, -100, 200, 200, "fill") + end) +end + +function TestImageRenderer:testCalculateFitWithZeroBoundsWidth() + luaunit.assertError(function() + ImageRenderer.calculateFit(100, 100, 0, 200, "fill") + end) +end + +function TestImageRenderer:testCalculateFitWithZeroBoundsHeight() + luaunit.assertError(function() + ImageRenderer.calculateFit(100, 100, 200, 0, "fill") + end) +end + +function TestImageRenderer:testCalculateFitWithNegativeBoundsWidth() + luaunit.assertError(function() + ImageRenderer.calculateFit(100, 100, -200, 200, "fill") + end) +end + +function TestImageRenderer:testCalculateFitWithNegativeBoundsHeight() + luaunit.assertError(function() + ImageRenderer.calculateFit(100, 100, 200, -200, "fill") + end) +end + +function TestImageRenderer:testCalculateFitWithInvalidFitMode() + luaunit.assertError(function() + ImageRenderer.calculateFit(100, 100, 200, 200, "invalid-mode") + end) +end + +function TestImageRenderer:testCalculateFitWithNilFitMode() + -- Should default to "fill" + local result = ImageRenderer.calculateFit(100, 100, 200, 200, nil) + luaunit.assertNotNil(result) + luaunit.assertEquals(result.dw, 200) + luaunit.assertEquals(result.dh, 200) +end + +function TestImageRenderer:testCalculateFitFillMode() + local result = ImageRenderer.calculateFit(100, 100, 200, 200, "fill") + luaunit.assertEquals(result.scaleX, 2) + luaunit.assertEquals(result.scaleY, 2) +end + +function TestImageRenderer:testCalculateFitContainMode() + local result = ImageRenderer.calculateFit(100, 100, 200, 200, "contain") + luaunit.assertEquals(result.scaleX, 2) + luaunit.assertEquals(result.scaleY, 2) +end + +function TestImageRenderer:testCalculateFitCoverMode() + local result = ImageRenderer.calculateFit(100, 100, 200, 200, "cover") + luaunit.assertEquals(result.scaleX, 2) + luaunit.assertEquals(result.scaleY, 2) +end + +function TestImageRenderer:testCalculateFitNoneMode() + local result = ImageRenderer.calculateFit(100, 100, 200, 200, "none") + luaunit.assertEquals(result.scaleX, 1) + luaunit.assertEquals(result.scaleY, 1) +end + +function TestImageRenderer:testCalculateFitScaleDownModeWithLargeImage() + local result = ImageRenderer.calculateFit(300, 300, 200, 200, "scale-down") + -- Should behave like contain for larger images + luaunit.assertNotNil(result) +end + +function TestImageRenderer:testCalculateFitScaleDownModeWithSmallImage() + local result = ImageRenderer.calculateFit(50, 50, 200, 200, "scale-down") + -- Should behave like none for smaller images + luaunit.assertEquals(result.scaleX, 1) + luaunit.assertEquals(result.scaleY, 1) +end + +-- Unhappy path tests for _parsePosition + +function TestImageRenderer:testParsePositionWithNil() + local x, y = ImageRenderer._parsePosition(nil) + luaunit.assertEquals(x, 0.5) + luaunit.assertEquals(y, 0.5) +end + +function TestImageRenderer:testParsePositionWithEmptyString() + local x, y = ImageRenderer._parsePosition("") + luaunit.assertEquals(x, 0.5) + luaunit.assertEquals(y, 0.5) +end + +function TestImageRenderer:testParsePositionWithInvalidType() + local x, y = ImageRenderer._parsePosition(123) + luaunit.assertEquals(x, 0.5) + luaunit.assertEquals(y, 0.5) +end + +function TestImageRenderer:testParsePositionWithInvalidKeyword() + local x, y = ImageRenderer._parsePosition("invalid keyword") + -- Should default to center + luaunit.assertEquals(x, 0.5) + luaunit.assertEquals(y, 0.5) +end + +function TestImageRenderer:testParsePositionWithMixedValid() + local x, y = ImageRenderer._parsePosition("left top") + luaunit.assertEquals(x, 0) + luaunit.assertEquals(y, 0) +end + +function TestImageRenderer:testParsePositionWithPercentage() + local x, y = ImageRenderer._parsePosition("75% 25%") + luaunit.assertAlmostEquals(x, 0.75, 0.01) + luaunit.assertAlmostEquals(y, 0.25, 0.01) +end + +function TestImageRenderer:testParsePositionWithOutOfRangePercentage() + local x, y = ImageRenderer._parsePosition("150% -50%") + -- 150% clamps to 1, but -50% doesn't match pattern so defaults to 0.5 + luaunit.assertEquals(x, 1) + luaunit.assertEquals(y, 0.5) +end + +function TestImageRenderer:testParsePositionWithSingleValue() + local x, y = ImageRenderer._parsePosition("left") + luaunit.assertEquals(x, 0) + luaunit.assertEquals(y, 0.5) -- Should use center for Y +end + +function TestImageRenderer:testParsePositionWithSinglePercentage() + local x, y = ImageRenderer._parsePosition("25%") + luaunit.assertAlmostEquals(x, 0.25, 0.01) + luaunit.assertAlmostEquals(y, 0.25, 0.01) +end + +-- Unhappy path tests for draw + +function TestImageRenderer:testDrawWithNilImage() + -- Should not crash, just return early + ImageRenderer.draw(nil, 0, 0, 100, 100, "fill") + -- If we get here without error, test passes + luaunit.assertTrue(true) +end + +function TestImageRenderer:testDrawWithZeroWidth() + -- Should error in calculateFit + luaunit.assertError(function() + ImageRenderer.draw(self.mockImage, 0, 0, 0, 100, "fill") + end) +end + +function TestImageRenderer:testDrawWithZeroHeight() + luaunit.assertError(function() + ImageRenderer.draw(self.mockImage, 0, 0, 100, 0, "fill") + end) +end + +function TestImageRenderer:testDrawWithNegativeOpacity() + -- Should work but render with negative opacity + ImageRenderer.draw(self.mockImage, 0, 0, 100, 100, "fill", "center center", -0.5) + luaunit.assertTrue(true) +end + +function TestImageRenderer:testDrawWithOpacityGreaterThanOne() + -- Should work but render with >1 opacity + ImageRenderer.draw(self.mockImage, 0, 0, 100, 100, "fill", "center center", 2.0) + luaunit.assertTrue(true) +end + +function TestImageRenderer:testDrawWithInvalidFitMode() + luaunit.assertError(function() + ImageRenderer.draw(self.mockImage, 0, 0, 100, 100, "invalid") + end) +end + +function TestImageRenderer:testCalculateFitWithVerySmallBounds() + local result = ImageRenderer.calculateFit(1000, 1000, 1, 1, "contain") + luaunit.assertNotNil(result) + -- Scale should be very small + luaunit.assertTrue(result.scaleX < 0.01) +end + +function TestImageRenderer:testCalculateFitWithVeryLargeBounds() + local result = ImageRenderer.calculateFit(10, 10, 10000, 10000, "contain") + luaunit.assertNotNil(result) + -- Scale should be very large + luaunit.assertTrue(result.scaleX > 100) +end + +function TestImageRenderer:testCalculateFitWithAspectRatioMismatch() + -- Wide image, tall bounds + local result = ImageRenderer.calculateFit(200, 100, 100, 200, "contain") + luaunit.assertNotNil(result) + -- Should maintain aspect ratio + luaunit.assertEquals(result.scaleX, result.scaleY) +end + +function TestImageRenderer:testCalculateFitCoverWithAspectRatioMismatch() + -- Wide image, tall bounds + local result = ImageRenderer.calculateFit(200, 100, 100, 200, "cover") + luaunit.assertNotNil(result) + luaunit.assertEquals(result.scaleX, result.scaleY) +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/image_scaler_test.lua b/testing/__tests__/image_scaler_test.lua new file mode 100644 index 0000000..f1a48dc --- /dev/null +++ b/testing/__tests__/image_scaler_test.lua @@ -0,0 +1,209 @@ +local luaunit = require("testing.luaunit") +require("testing.loveStub") + +local ImageScaler = require("modules.ImageScaler") + +TestImageScaler = {} + +function TestImageScaler:setUp() + -- Create a minimal mock ImageData + self.mockImageData = { + getPixel = function(self, x, y) + -- Return deterministic values based on position + return (x % 256) / 255, (y % 256) / 255, 0.5, 1.0 + end, + } +end + +-- Unhappy path tests for scaleNearest + +function TestImageScaler:testScaleNearestWithNilSource() + luaunit.assertError(function() + ImageScaler.scaleNearest(nil, 0, 0, 10, 10, 20, 20) + end) +end + +function TestImageScaler:testScaleNearestWithZeroSourceWidth() + luaunit.assertError(function() + ImageScaler.scaleNearest(self.mockImageData, 0, 0, 0, 10, 20, 20) + end) +end + +function TestImageScaler:testScaleNearestWithZeroSourceHeight() + luaunit.assertError(function() + ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 0, 20, 20) + end) +end + +function TestImageScaler:testScaleNearestWithNegativeSourceWidth() + luaunit.assertError(function() + ImageScaler.scaleNearest(self.mockImageData, 0, 0, -10, 10, 20, 20) + end) +end + +function TestImageScaler:testScaleNearestWithNegativeSourceHeight() + luaunit.assertError(function() + ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, -10, 20, 20) + end) +end + +function TestImageScaler:testScaleNearestWithZeroDestWidth() + luaunit.assertError(function() + ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 0, 20) + end) +end + +function TestImageScaler:testScaleNearestWithZeroDestHeight() + luaunit.assertError(function() + ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, 0) + end) +end + +function TestImageScaler:testScaleNearestWithNegativeDestWidth() + luaunit.assertError(function() + ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, -20, 20) + end) +end + +function TestImageScaler:testScaleNearestWithNegativeDestHeight() + luaunit.assertError(function() + ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, -20) + end) +end + +-- Unhappy path tests for scaleBilinear + +function TestImageScaler:testScaleBilinearWithNilSource() + luaunit.assertError(function() + ImageScaler.scaleBilinear(nil, 0, 0, 10, 10, 20, 20) + end) +end + +function TestImageScaler:testScaleBilinearWithZeroSourceWidth() + luaunit.assertError(function() + ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 0, 10, 20, 20) + end) +end + +function TestImageScaler:testScaleBilinearWithZeroSourceHeight() + luaunit.assertError(function() + ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 0, 20, 20) + end) +end + +function TestImageScaler:testScaleBilinearWithNegativeSourceWidth() + luaunit.assertError(function() + ImageScaler.scaleBilinear(self.mockImageData, 0, 0, -10, 10, 20, 20) + end) +end + +function TestImageScaler:testScaleBilinearWithNegativeSourceHeight() + luaunit.assertError(function() + ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, -10, 20, 20) + end) +end + +function TestImageScaler:testScaleBilinearWithZeroDestWidth() + luaunit.assertError(function() + ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 0, 20) + end) +end + +function TestImageScaler:testScaleBilinearWithZeroDestHeight() + luaunit.assertError(function() + ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, 0) + end) +end + +function TestImageScaler:testScaleBilinearWithNegativeDestWidth() + luaunit.assertError(function() + ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, -20, 20) + end) +end + +function TestImageScaler:testScaleBilinearWithNegativeDestHeight() + luaunit.assertError(function() + ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, -20) + end) +end + +-- Edge case tests + +function TestImageScaler:testScaleNearestWithVeryLargeUpscale() + -- Scale 1x1 to 50x50 (extreme upscale, but fast for testing) + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 1, 1, 50, 50) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleNearestWithVeryLargeDownscale() + -- Scale 50x50 to 1x1 (extreme downscale) + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 50, 50, 1, 1) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleBilinearWithVeryLargeUpscale() + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 1, 1, 50, 50) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleBilinearWithVeryLargeDownscale() + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 50, 50, 1, 1) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleNearestWithNonIntegerDimensions() + -- Fractional source dimensions (should work with floor/ceil) + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 5.5, 5.5, 10, 10) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleBilinearWithNonIntegerDimensions() + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 5.5, 5.5, 10, 10) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleNearestWith1x1Source() + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 1, 1, 5, 5) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleBilinearWith1x1Source() + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 1, 1, 5, 5) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleNearestWith1x1Dest() + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 5, 5, 1, 1) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleBilinearWith1x1Dest() + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 5, 5, 1, 1) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleNearestWithNonZeroSourceOffset() + -- Source region offset from 0,0 + local result = ImageScaler.scaleNearest(self.mockImageData, 10, 10, 5, 5, 10, 10) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleBilinearWithNonZeroSourceOffset() + local result = ImageScaler.scaleBilinear(self.mockImageData, 10, 10, 5, 5, 10, 10) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleNearestWithAspectRatioChange() + -- Change aspect ratio dramatically + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 5, 20, 20, 5) + luaunit.assertNotNil(result) +end + +function TestImageScaler:testScaleBilinearWithAspectRatioChange() + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 5, 20, 20, 5) + luaunit.assertNotNil(result) +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/input_event_test.lua b/testing/__tests__/input_event_test.lua new file mode 100644 index 0000000..41201fb --- /dev/null +++ b/testing/__tests__/input_event_test.lua @@ -0,0 +1,211 @@ +-- Test suite for InputEvent module +package.path = package.path .. ";./?.lua;./modules/?.lua" + +require("testing.loveStub") +local luaunit = require("testing.luaunit") +local InputEvent = require("modules.InputEvent") + +TestInputEvent = {} + +-- Test: new() creates click event with all properties +function TestInputEvent:test_new_creates_click_event() + local event = InputEvent.new({ + type = "click", + button = 1, + x = 100, + y = 200, + modifiers = { shift = false, ctrl = false, alt = false, super = false }, + }) + + luaunit.assertNotNil(event, "new() should return event") + luaunit.assertEquals(event.type, "click") + luaunit.assertEquals(event.button, 1) + luaunit.assertEquals(event.x, 100) + luaunit.assertEquals(event.y, 200) + luaunit.assertNotNil(event.modifiers) + luaunit.assertEquals(event.modifiers.shift, false) +end + +-- Test: new() creates press event +function TestInputEvent:test_new_creates_press_event() + local event = InputEvent.new({ + type = "press", + button = 1, + x = 50, + y = 75, + modifiers = { shift = true, ctrl = false, alt = false, super = false }, + }) + + luaunit.assertEquals(event.type, "press") + luaunit.assertEquals(event.button, 1) + luaunit.assertEquals(event.x, 50) + luaunit.assertEquals(event.y, 75) + luaunit.assertEquals(event.modifiers.shift, true) +end + +-- Test: new() creates release event +function TestInputEvent:test_new_creates_release_event() + local event = InputEvent.new({ + type = "release", + button = 1, + x = 150, + y = 250, + modifiers = { shift = false, ctrl = false, alt = false, super = false }, + }) + + luaunit.assertEquals(event.type, "release") +end + +-- Test: new() creates rightclick event with button 2 +function TestInputEvent:test_new_creates_rightclick_event() + local event = InputEvent.new({ + type = "rightclick", + button = 2, + x = 100, + y = 100, + modifiers = { shift = false, ctrl = true, alt = false, super = false }, + }) + + luaunit.assertEquals(event.type, "rightclick") + luaunit.assertEquals(event.button, 2) + luaunit.assertEquals(event.modifiers.ctrl, true) +end + +-- Test: new() creates middleclick event with button 3 +function TestInputEvent:test_new_creates_middleclick_event() + local event = InputEvent.new({ + type = "middleclick", + button = 3, + x = 200, + y = 300, + modifiers = { shift = false, ctrl = false, alt = true, super = false }, + }) + + luaunit.assertEquals(event.type, "middleclick") + luaunit.assertEquals(event.button, 3) + luaunit.assertEquals(event.modifiers.alt, true) +end + +-- Test: new() creates drag event with dx and dy +function TestInputEvent:test_new_creates_drag_event_with_deltas() + local event = InputEvent.new({ + type = "drag", + button = 1, + x = 100, + y = 100, + dx = 20, + dy = -15, + modifiers = { shift = false, ctrl = false, alt = false, super = false }, + }) + + luaunit.assertEquals(event.type, "drag") + luaunit.assertEquals(event.dx, 20) + luaunit.assertEquals(event.dy, -15) +end + +-- Test: new() defaults clickCount to 1 if not provided +function TestInputEvent:test_new_defaults_clickCount_to_one() + local event = InputEvent.new({ + type = "click", + button = 1, + x = 0, + y = 0, + modifiers = { shift = false, ctrl = false, alt = false, super = false }, + }) + + luaunit.assertEquals(event.clickCount, 1) +end + +-- Test: new() accepts custom clickCount for double-click +function TestInputEvent:test_new_accepts_custom_clickCount() + local event = InputEvent.new({ + type = "click", + button = 1, + x = 0, + y = 0, + clickCount = 2, + modifiers = { shift = false, ctrl = false, alt = false, super = false }, + }) + + luaunit.assertEquals(event.clickCount, 2) +end + +-- Test: new() accepts custom clickCount for triple-click +function TestInputEvent:test_new_accepts_triple_click() + local event = InputEvent.new({ + type = "click", + button = 1, + x = 0, + y = 0, + clickCount = 3, + modifiers = { shift = false, ctrl = false, alt = false, super = false }, + }) + + luaunit.assertEquals(event.clickCount, 3) +end + +-- Test: new() defaults timestamp to current time if not provided +function TestInputEvent:test_new_defaults_timestamp() + local before = love.timer.getTime() + local event = InputEvent.new({ + type = "click", + button = 1, + x = 0, + y = 0, + modifiers = { shift = false, ctrl = false, alt = false, super = false }, + }) + local after = love.timer.getTime() + + luaunit.assertNotNil(event.timestamp) + luaunit.assertTrue(event.timestamp >= before) + luaunit.assertTrue(event.timestamp <= after) +end + +-- Test: new() accepts custom timestamp +function TestInputEvent:test_new_accepts_custom_timestamp() + local customTime = 12345.678 + local event = InputEvent.new({ + type = "click", + button = 1, + x = 0, + y = 0, + timestamp = customTime, + modifiers = { shift = false, ctrl = false, alt = false, super = false }, + }) + + luaunit.assertEquals(event.timestamp, customTime) +end + +-- Test: new() handles all modifier keys +function TestInputEvent:test_new_handles_all_modifier_keys() + local event = InputEvent.new({ + type = "click", + button = 1, + x = 0, + y = 0, + modifiers = { shift = true, ctrl = true, alt = true, super = true }, + }) + + luaunit.assertEquals(event.modifiers.shift, true) + luaunit.assertEquals(event.modifiers.ctrl, true) + luaunit.assertEquals(event.modifiers.alt, true) + luaunit.assertEquals(event.modifiers.super, true) +end + +-- Test: new() handles nil dx/dy for non-drag events +function TestInputEvent:test_new_handles_nil_deltas() + local event = InputEvent.new({ + type = "click", + button = 1, + x = 100, + y = 100, + modifiers = { shift = false, ctrl = false, alt = false, super = false }, + }) + + luaunit.assertNil(event.dx) + luaunit.assertNil(event.dy) +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/layout_edge_cases_test.lua b/testing/__tests__/layout_edge_cases_test.lua index a9a504a..02f467f 100644 --- a/testing/__tests__/layout_edge_cases_test.lua +++ b/testing/__tests__/layout_edge_cases_test.lua @@ -16,7 +16,7 @@ function TestLayoutEdgeCases:setUp() self.warnings = {} self.originalWarn = ErrorHandler.warn ErrorHandler.warn = function(module, message) - table.insert(self.warnings, {module = module, message = message}) + table.insert(self.warnings, { module = module, message = message }) end end @@ -35,22 +35,22 @@ function TestLayoutEdgeCases:test_percentage_width_with_auto_parent_warns() -- width not specified - auto-sizing width height = 200, positioning = "flex", - flexDirection = "horizontal" + flexDirection = "horizontal", }) - + FlexLove.new({ id = "child_with_percentage", parent = container, - width = "50%", -- Percentage width with auto-sizing parent - should warn - height = 100 + width = "50%", -- Percentage width with auto-sizing parent - should warn + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- Check that a warning was issued luaunit.assertTrue(#self.warnings > 0, "Should issue warning for percentage width with auto-sizing parent") - + local found = false for _, warning in ipairs(self.warnings) do if warning.message:match("percentage width") and warning.message:match("auto%-sizing") then @@ -58,7 +58,7 @@ function TestLayoutEdgeCases:test_percentage_width_with_auto_parent_warns() break end end - + luaunit.assertTrue(found, "Warning should mention percentage width and auto-sizing") end @@ -71,22 +71,22 @@ function TestLayoutEdgeCases:test_percentage_height_with_auto_parent_warns() width = 200, -- height not specified - auto-sizing height positioning = "flex", - flexDirection = "vertical" + flexDirection = "vertical", }) - + FlexLove.new({ id = "child_with_percentage", parent = container, width = 100, - height = "50%" -- Percentage height with auto-sizing parent - should warn + height = "50%", -- Percentage height with auto-sizing parent - should warn }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- Check that a warning was issued luaunit.assertTrue(#self.warnings > 0, "Should issue warning for percentage height with auto-sizing parent") - + local found = false for _, warning in ipairs(self.warnings) do if warning.message:match("percentage height") and warning.message:match("auto%-sizing") then @@ -94,7 +94,7 @@ function TestLayoutEdgeCases:test_percentage_height_with_auto_parent_warns() break end end - + luaunit.assertTrue(found, "Warning should mention percentage height and auto-sizing") end @@ -107,19 +107,19 @@ function TestLayoutEdgeCases:test_pixel_width_with_auto_parent_no_warn() -- width not specified - auto-sizing height = 200, positioning = "flex", - flexDirection = "horizontal" + flexDirection = "horizontal", }) - + FlexLove.new({ id = "child_with_pixels", parent = container, - width = 100, -- Pixel width - should NOT warn - height = 100 + width = 100, -- Pixel width - should NOT warn + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- Check that NO warning was issued about percentage sizing for _, warning in ipairs(self.warnings) do local hasPercentageWarning = warning.message:match("percentage") and warning.message:match("auto%-sizing") @@ -135,23 +135,23 @@ function TestLayoutEdgeCases:test_css_positioning_top_offset() y = 100, width = 400, height = 400, - positioning = "absolute" + positioning = "absolute", }) - + local child = FlexLove.new({ id = "child", parent = container, positioning = "absolute", - top = 50, -- 50px from top + top = 50, -- 50px from top left = 0, width = 100, - height = 100 + height = 100, }) - + -- Trigger layout by ending and restarting frame FlexLove.endFrame() FlexLove.beginFrame() - + -- Child should be positioned 50px from container's top edge (accounting for padding) local expectedY = container.y + container.padding.top + 50 luaunit.assertEquals(child.y, expectedY, "Child should be positioned with top offset") @@ -165,22 +165,22 @@ function TestLayoutEdgeCases:test_css_positioning_bottom_offset() y = 100, width = 400, height = 400, - positioning = "absolute" + positioning = "absolute", }) - + local child = FlexLove.new({ id = "child", parent = container, positioning = "absolute", - bottom = 50, -- 50px from bottom + bottom = 50, -- 50px from bottom left = 0, width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- Child should be positioned 50px from container's bottom edge local expectedY = container.y + container.padding.top + container.height - 50 - child:getBorderBoxHeight() luaunit.assertEquals(child.y, expectedY, "Child should be positioned with bottom offset") @@ -194,22 +194,22 @@ function TestLayoutEdgeCases:test_css_positioning_left_offset() y = 100, width = 400, height = 400, - positioning = "absolute" + positioning = "absolute", }) - + local child = FlexLove.new({ id = "child", parent = container, positioning = "absolute", top = 0, - left = 50, -- 50px from left + left = 50, -- 50px from left width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- Child should be positioned 50px from container's left edge local expectedX = container.x + container.padding.left + 50 luaunit.assertEquals(child.x, expectedX, "Child should be positioned with left offset") @@ -223,22 +223,22 @@ function TestLayoutEdgeCases:test_css_positioning_right_offset() y = 100, width = 400, height = 400, - positioning = "absolute" + positioning = "absolute", }) - + local child = FlexLove.new({ id = "child", parent = container, positioning = "absolute", top = 0, - right = 50, -- 50px from right + right = 50, -- 50px from right width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- Child should be positioned 50px from container's right edge local expectedX = container.x + container.padding.left + container.width - 50 - child:getBorderBoxWidth() luaunit.assertEquals(child.x, expectedX, "Child should be positioned with right offset") @@ -252,23 +252,23 @@ function TestLayoutEdgeCases:test_css_positioning_top_and_bottom() y = 100, width = 400, height = 400, - positioning = "absolute" + positioning = "absolute", }) - + local child = FlexLove.new({ id = "child", parent = container, positioning = "absolute", top = 10, - bottom = 20, -- Both specified - last one wins in current implementation + bottom = 20, -- Both specified - last one wins in current implementation left = 0, width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- Bottom should override top local expectedY = container.y + container.padding.top + container.height - 20 - child:getBorderBoxHeight() luaunit.assertEquals(child.y, expectedY, "Bottom offset should override top when both specified") @@ -282,23 +282,23 @@ function TestLayoutEdgeCases:test_css_positioning_left_and_right() y = 100, width = 400, height = 400, - positioning = "absolute" + positioning = "absolute", }) - + local child = FlexLove.new({ id = "child", parent = container, positioning = "absolute", top = 0, left = 10, - right = 20, -- Both specified - last one wins in current implementation + right = 20, -- Both specified - last one wins in current implementation width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- Right should override left local expectedX = container.x + container.padding.left + container.width - 20 - child:getBorderBoxWidth() luaunit.assertEquals(child.x, expectedX, "Right offset should override left when both specified") @@ -313,9 +313,9 @@ function TestLayoutEdgeCases:test_css_positioning_with_padding() width = 400, height = 400, padding = { top = 20, right = 20, bottom = 20, left = 20 }, - positioning = "absolute" + positioning = "absolute", }) - + local child = FlexLove.new({ id = "child", parent = container, @@ -323,16 +323,16 @@ function TestLayoutEdgeCases:test_css_positioning_with_padding() top = 10, left = 10, width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- Offsets should be relative to content area (after padding) local expectedX = container.x + container.padding.left + 10 local expectedY = container.y + container.padding.top + 10 - + luaunit.assertEquals(child.x, expectedX, "Left offset should account for container padding") luaunit.assertEquals(child.y, expectedY, "Top offset should account for container padding") end @@ -346,21 +346,21 @@ function TestLayoutEdgeCases:test_css_positioning_ignored_in_flex() width = 400, height = 400, positioning = "flex", - flexDirection = "horizontal" + flexDirection = "horizontal", }) - + local child = FlexLove.new({ id = "child", parent = container, - top = 100, -- This should be IGNORED in flex layout - left = 100, -- This should be IGNORED in flex layout + top = 100, -- This should be IGNORED in flex layout + left = 100, -- This should be IGNORED in flex layout width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- In flex layout, child should be positioned by flex rules, not CSS offsets -- Child should be at (0, 0) relative to container content area luaunit.assertEquals(child.x, 0, "CSS offsets should be ignored in flex layout") @@ -375,9 +375,9 @@ function TestLayoutEdgeCases:test_css_positioning_in_relative_container() y = 100, width = 400, height = 400, - positioning = "relative" + positioning = "relative", }) - + local child = FlexLove.new({ id = "child", parent = container, @@ -385,16 +385,16 @@ function TestLayoutEdgeCases:test_css_positioning_in_relative_container() top = 30, left = 30, width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame() - + -- Should work the same as absolute container local expectedX = container.x + container.padding.left + 30 local expectedY = container.y + container.padding.top + 30 - + luaunit.assertEquals(child.x, expectedX, "CSS positioning should work in relative containers") luaunit.assertEquals(child.y, expectedY, "CSS positioning should work in relative containers") end diff --git a/testing/__tests__/layout_engine_test.lua b/testing/__tests__/layout_engine_test.lua index 72eec65..4fb1624 100644 --- a/testing/__tests__/layout_engine_test.lua +++ b/testing/__tests__/layout_engine_test.lua @@ -716,7 +716,6 @@ local function createMockElement(props) } end - -- Run tests if this file is executed directly if not _G.RUNNING_ALL_TESTS then os.exit(luaunit.LuaUnit.run()) diff --git a/testing/__tests__/overflow_test.lua b/testing/__tests__/overflow_test.lua index 6c50e21..70dc9d6 100644 --- a/testing/__tests__/overflow_test.lua +++ b/testing/__tests__/overflow_test.lua @@ -24,9 +24,9 @@ function TestOverflowDetection:test_vertical_overflow_detected() y = 0, width = 200, height = 100, - overflow = "scroll" + overflow = "scroll", }) - + -- Add child that exceeds container height FlexLove.new({ id = "tall_child", @@ -34,13 +34,13 @@ function TestOverflowDetection:test_vertical_overflow_detected() x = 0, y = 0, width = 100, - height = 200 -- Taller than container (100) + height = 200, -- Taller than container (100) }) - + -- Force layout to trigger detectOverflow FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Check if overflow was detected local maxScrollX, maxScrollY = container:getMaxScroll() luaunit.assertTrue(maxScrollY > 0, "Should detect vertical overflow") @@ -54,22 +54,22 @@ function TestOverflowDetection:test_horizontal_overflow_detected() y = 0, width = 100, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + -- Add child that exceeds container width FlexLove.new({ id = "wide_child", parent = container, x = 0, y = 0, - width = 300, -- Wider than container (100) - height = 50 + width = 300, -- Wider than container (100) + height = 50, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + local maxScrollX, maxScrollY = container:getMaxScroll() luaunit.assertTrue(maxScrollX > 0, "Should detect horizontal overflow") luaunit.assertEquals(maxScrollY, 0, "Should not have vertical overflow") @@ -82,9 +82,9 @@ function TestOverflowDetection:test_both_axes_overflow() y = 0, width = 100, height = 100, - overflow = "scroll" + overflow = "scroll", }) - + -- Add child that exceeds both dimensions FlexLove.new({ id = "large_child", @@ -92,12 +92,12 @@ function TestOverflowDetection:test_both_axes_overflow() x = 0, y = 0, width = 200, - height = 200 + height = 200, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + local maxScrollX, maxScrollY = container:getMaxScroll() luaunit.assertTrue(maxScrollX > 0, "Should detect horizontal overflow") luaunit.assertTrue(maxScrollY > 0, "Should detect vertical overflow") @@ -110,9 +110,9 @@ function TestOverflowDetection:test_no_overflow_when_content_fits() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + -- Add child that fits within container FlexLove.new({ id = "small_child", @@ -120,12 +120,12 @@ function TestOverflowDetection:test_no_overflow_when_content_fits() x = 0, y = 0, width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + local maxScrollX, maxScrollY = container:getMaxScroll() luaunit.assertEquals(maxScrollX, 0, "Should not have horizontal overflow") luaunit.assertEquals(maxScrollY, 0, "Should not have vertical overflow") @@ -140,22 +140,22 @@ function TestOverflowDetection:test_overflow_with_multiple_children() height = 200, overflow = "scroll", positioning = "flex", - flexDirection = "vertical" + flexDirection = "vertical", }) - + -- Add multiple children that together exceed container for i = 1, 5 do FlexLove.new({ id = "child_" .. i, parent = container, width = 150, - height = 60 -- 5 * 60 = 300, exceeds container height of 200 + height = 60, -- 5 * 60 = 300, exceeds container height of 200 }) end - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + local maxScrollX, maxScrollY = container:getMaxScroll() luaunit.assertTrue(maxScrollY > 0, "Should detect overflow from multiple children") end @@ -168,22 +168,22 @@ function TestOverflowDetection:test_overflow_with_padding() width = 200, height = 200, padding = { top = 10, right = 10, bottom = 10, left = 10 }, - overflow = "scroll" + overflow = "scroll", }) - + -- Child that fits in container but exceeds available content area (200 - 20 = 180) FlexLove.new({ id = "child", parent = container, x = 0, y = 0, - width = 190, -- Exceeds content width (180) - height = 100 + width = 190, -- Exceeds content width (180) + height = 100, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + local maxScrollX, maxScrollY = container:getMaxScroll() luaunit.assertTrue(maxScrollX > 0, "Should detect overflow accounting for padding") end @@ -197,9 +197,9 @@ function TestOverflowDetection:test_overflow_with_margins() height = 200, positioning = "flex", flexDirection = "horizontal", - overflow = "scroll" + overflow = "scroll", }) - + -- Child with margins that contribute to overflow -- In flex layout, margins are properly accounted for in positioning FlexLove.new({ @@ -207,12 +207,12 @@ function TestOverflowDetection:test_overflow_with_margins() parent = container, width = 180, height = 180, - margin = { top = 5, right = 20, bottom = 5, left = 5 } -- Total width: 5+180+20=205, overflows 200px container + margin = { top = 5, right = 20, bottom = 5, left = 5 }, -- Total width: 5+180+20=205, overflows 200px container }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + local maxScrollX, maxScrollY = container:getMaxScroll() luaunit.assertTrue(maxScrollX > 0, "Should include child margins in overflow calculation") end @@ -225,9 +225,9 @@ function TestOverflowDetection:test_visible_overflow_skips_detection() y = 0, width = 100, height = 100, - overflow = "visible" -- Should not clip or calculate overflow + overflow = "visible", -- Should not clip or calculate overflow }) - + -- Add oversized child FlexLove.new({ id = "large_child", @@ -235,12 +235,12 @@ function TestOverflowDetection:test_visible_overflow_skips_detection() x = 0, y = 0, width = 300, - height = 300 + height = 300, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- With overflow="visible", maxScroll should be 0 (no scrolling) local maxScrollX, maxScrollY = container:getMaxScroll() luaunit.assertEquals(maxScrollX, 0, "visible overflow should not enable scrolling") @@ -255,13 +255,13 @@ function TestOverflowDetection:test_empty_container_no_overflow() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", -- No children }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + local maxScrollX, maxScrollY = container:getMaxScroll() luaunit.assertEquals(maxScrollX, 0, "Empty container should have no overflow") luaunit.assertEquals(maxScrollY, 0, "Empty container should have no overflow") @@ -275,9 +275,9 @@ function TestOverflowDetection:test_absolute_children_ignored_in_overflow() y = 0, width = 200, height = 200, - overflow = "scroll" + overflow = "scroll", }) - + -- Regular child that fits FlexLove.new({ id = "normal_child", @@ -285,9 +285,9 @@ function TestOverflowDetection:test_absolute_children_ignored_in_overflow() x = 0, y = 0, width = 150, - height = 150 + height = 150, }) - + -- Absolutely positioned child that extends beyond (should NOT cause overflow) FlexLove.new({ id = "absolute_child", @@ -296,12 +296,12 @@ function TestOverflowDetection:test_absolute_children_ignored_in_overflow() top = 0, left = 0, width = 400, - height = 400 + height = 400, }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + local maxScrollX, maxScrollY = container:getMaxScroll() -- Should not have overflow because absolute children are ignored luaunit.assertEquals(maxScrollX, 0, "Absolute children should not cause overflow") @@ -316,26 +316,26 @@ function TestOverflowDetection:test_scroll_clamped_to_max() y = 0, width = 100, height = 100, - overflow = "scroll" + overflow = "scroll", }) - + FlexLove.new({ id = "child", parent = container, x = 0, y = 0, width = 100, - height = 300 -- Creates 200px of vertical overflow + height = 300, -- Creates 200px of vertical overflow }) - + FlexLove.endFrame() FlexLove.beginFrame(1920, 1080) - + -- Try to scroll beyond max container:setScrollPosition(0, 999999) local scrollX, scrollY = container:getScrollPosition() local maxScrollX, maxScrollY = container:getMaxScroll() - + luaunit.assertEquals(scrollY, maxScrollY, "Scroll should be clamped to maximum") luaunit.assertTrue(scrollY < 999999, "Should not scroll beyond content") end diff --git a/testing/__tests__/text_editor_test.lua b/testing/__tests__/text_editor_test.lua new file mode 100644 index 0000000..3c5f84d --- /dev/null +++ b/testing/__tests__/text_editor_test.lua @@ -0,0 +1,396 @@ +-- Test suite for TextEditor module +package.path = package.path .. ";./?.lua;./modules/?.lua" + +require("testing.loveStub") +local luaunit = require("testing.luaunit") +local TextEditor = require("modules.TextEditor") +local Color = require("modules.Color") +local utils = require("modules.utils") + +TestTextEditor = {} + +-- Mock dependencies +local MockContext = { + _immediateMode = false, + _focusedElement = nil, +} + +local MockStateManager = { + getState = function(id) + return nil + end, + saveState = function(id, state) end, +} + +-- Helper to create TextEditor with dependencies +local function createTextEditor(config) + config = config or {} + return TextEditor.new(config, { + Context = MockContext, + StateManager = MockStateManager, + Color = Color, + utils = utils, + }) +end + +-- Helper to create mock element +local function createMockElement() + return { + _stateId = "test-element-1", + width = 200, + height = 30, + } +end + +-- Test: new() creates instance with defaults +function TestTextEditor:test_new_creates_with_defaults() + local editor = createTextEditor() + + luaunit.assertNotNil(editor) + luaunit.assertFalse(editor.editable) + luaunit.assertFalse(editor.multiline) + luaunit.assertFalse(editor.passwordMode) + luaunit.assertEquals(editor.inputType, "text") + luaunit.assertEquals(editor._textBuffer, "") + luaunit.assertEquals(editor._cursorPosition, 0) + luaunit.assertFalse(editor._focused) +end + +-- Test: new() accepts configuration +function TestTextEditor:test_new_accepts_config() + local editor = createTextEditor({ + editable = true, + multiline = true, + passwordMode = true, + text = "Hello", + placeholder = "Enter text", + maxLength = 100, + inputType = "email", + }) + + luaunit.assertTrue(editor.editable) + luaunit.assertTrue(editor.multiline) + luaunit.assertTrue(editor.passwordMode) + luaunit.assertEquals(editor._textBuffer, "Hello") + luaunit.assertEquals(editor.placeholder, "Enter text") + luaunit.assertEquals(editor.maxLength, 100) + luaunit.assertEquals(editor.inputType, "email") +end + +-- Test: new() sanitizes initial text +function TestTextEditor:test_new_sanitizes_initial_text() + local editor = createTextEditor({ + text = "Hello\n\nWorld", + multiline = false, + allowNewlines = false, + }) + + -- Newlines should be removed for single-line + luaunit.assertNotEquals(editor._textBuffer, "Hello\n\nWorld") +end + +-- Test: initialize() sets element reference +function TestTextEditor:test_initialize_sets_element() + local editor = createTextEditor() + local element = createMockElement() + + editor:initialize(element) + + luaunit.assertEquals(editor._element, element) +end + +-- Test: getText() returns current text +function TestTextEditor:test_getText_returns_text() + local editor = createTextEditor({ text = "Hello World" }) + + luaunit.assertEquals(editor:getText(), "Hello World") +end + +-- Test: getText() returns empty string for nil buffer +function TestTextEditor:test_getText_returns_empty_for_nil() + local editor = createTextEditor() + editor._textBuffer = nil + + luaunit.assertEquals(editor:getText(), "") +end + +-- Test: setText() updates text buffer +function TestTextEditor:test_setText_updates_buffer() + local editor = createTextEditor() + + editor:setText("New text") + + luaunit.assertEquals(editor:getText(), "New text") +end + +-- Test: setText() sanitizes text by default +function TestTextEditor:test_setText_sanitizes() + local editor = createTextEditor({ + multiline = false, + allowNewlines = false, + }) + + editor:setText("Line1\nLine2") + + -- Should remove newlines for single-line + local text = editor:getText() + luaunit.assertFalse(text:find("\n") ~= nil) +end + +-- Test: setText() skips sanitization when requested +function TestTextEditor:test_setText_skips_sanitization() + local editor = createTextEditor({ + multiline = false, + allowNewlines = false, + }) + + editor:setText("Line1\nLine2", true) -- skipSanitization = true + + luaunit.assertEquals(editor:getText(), "Line1\nLine2") +end + +-- Test: insertText() adds text at position +function TestTextEditor:test_insertText_at_position() + local editor = createTextEditor({ text = "Hello" }) + + editor:insertText(" World", 5) + + luaunit.assertEquals(editor:getText(), "Hello World") +end + +-- Test: insertText() adds text at start +function TestTextEditor:test_insertText_at_start() + local editor = createTextEditor({ text = "World" }) + + editor:insertText("Hello ", 0) + + luaunit.assertEquals(editor:getText(), "Hello World") +end + +-- Test: deleteText() removes text range +function TestTextEditor:test_deleteText_removes_range() + local editor = createTextEditor({ text = "Hello World" }) + + editor:deleteText(5, 11) -- Remove " World" + + luaunit.assertEquals(editor:getText(), "Hello") +end + +-- Test: deleteText() handles reversed positions +function TestTextEditor:test_deleteText_handles_reversed() + local editor = createTextEditor({ text = "Hello World" }) + + editor:deleteText(11, 5) -- Reversed: should swap + + luaunit.assertEquals(editor:getText(), "Hello") +end + +-- Test: replaceText() replaces range with new text +function TestTextEditor:test_replaceText_replaces_range() + local editor = createTextEditor({ text = "Hello World" }) + + editor:replaceText(6, 11, "Lua") + + luaunit.assertEquals(editor:getText(), "Hello Lua") +end + +-- Test: setCursorPosition() sets cursor +function TestTextEditor:test_setCursorPosition() + local editor = createTextEditor({ text = "Hello" }) + + editor:setCursorPosition(3) + + luaunit.assertEquals(editor:getCursorPosition(), 3) +end + +-- Test: setCursorPosition() clamps to valid range +function TestTextEditor:test_setCursorPosition_clamps() + local editor = createTextEditor({ text = "Hello" }) + + editor:setCursorPosition(100) -- Beyond text length + + luaunit.assertEquals(editor:getCursorPosition(), 5) +end + +-- Test: moveCursorBy() moves cursor relative +function TestTextEditor:test_moveCursorBy() + local editor = createTextEditor({ text = "Hello" }) + editor:setCursorPosition(2) + + editor:moveCursorBy(2) + + luaunit.assertEquals(editor:getCursorPosition(), 4) +end + +-- Test: moveCursorToStart() moves to beginning +function TestTextEditor:test_moveCursorToStart() + local editor = createTextEditor({ text = "Hello" }) + editor:setCursorPosition(3) + + editor:moveCursorToStart() + + luaunit.assertEquals(editor:getCursorPosition(), 0) +end + +-- Test: moveCursorToEnd() moves to end +function TestTextEditor:test_moveCursorToEnd() + local editor = createTextEditor({ text = "Hello" }) + + editor:moveCursorToEnd() + + luaunit.assertEquals(editor:getCursorPosition(), 5) +end + +-- Test: setSelection() sets selection range +function TestTextEditor:test_setSelection() + local editor = createTextEditor({ text = "Hello World" }) + + editor:setSelection(0, 5) + + local start, endPos = editor:getSelection() + luaunit.assertEquals(start, 0) + luaunit.assertEquals(endPos, 5) +end + +-- Test: hasSelection() returns true when selected +function TestTextEditor:test_hasSelection_true() + local editor = createTextEditor({ text = "Hello" }) + editor:setSelection(0, 5) + + luaunit.assertTrue(editor:hasSelection()) +end + +-- Test: hasSelection() returns false when no selection +function TestTextEditor:test_hasSelection_false() + local editor = createTextEditor({ text = "Hello" }) + + luaunit.assertFalse(editor:hasSelection()) +end + +-- Test: clearSelection() removes selection +function TestTextEditor:test_clearSelection() + local editor = createTextEditor({ text = "Hello" }) + editor:setSelection(0, 5) + + editor:clearSelection() + + luaunit.assertFalse(editor:hasSelection()) +end + +-- Test: getSelectedText() returns selected text +function TestTextEditor:test_getSelectedText() + local editor = createTextEditor({ text = "Hello World" }) + editor:setSelection(0, 5) + + luaunit.assertEquals(editor:getSelectedText(), "Hello") +end + +-- Test: deleteSelection() removes selected text +function TestTextEditor:test_deleteSelection() + local editor = createTextEditor({ text = "Hello World" }) + editor:setSelection(0, 6) + + editor:deleteSelection() + + luaunit.assertEquals(editor:getText(), "World") + luaunit.assertFalse(editor:hasSelection()) +end + +-- Test: selectAll() selects entire text +function TestTextEditor:test_selectAll() + local editor = createTextEditor({ text = "Hello World" }) + + editor:selectAll() + + local start, endPos = editor:getSelection() + luaunit.assertEquals(start, 0) + luaunit.assertEquals(endPos, 11) +end + +-- Test: sanitization with maxLength +function TestTextEditor:test_sanitize_max_length() + local editor = createTextEditor({ + maxLength = 5, + }) + + editor:setText("HelloWorld") + + luaunit.assertEquals(editor:getText(), "Hello") +end + +-- Test: sanitization disabled +function TestTextEditor:test_sanitization_disabled() + local editor = createTextEditor({ + sanitize = false, + multiline = false, + allowNewlines = false, + }) + + editor:setText("Line1\nLine2") + + -- Should NOT sanitize newlines when disabled + luaunit.assertEquals(editor:getText(), "Line1\nLine2") +end + +-- Test: customSanitizer callback +function TestTextEditor:test_custom_sanitizer() + local editor = createTextEditor({ + customSanitizer = function(text) + return text:upper() + end, + }) + + editor:setText("hello") + + luaunit.assertEquals(editor:getText(), "HELLO") +end + +-- Test: allowNewlines follows multiline setting +function TestTextEditor:test_allowNewlines_follows_multiline() + local editor = createTextEditor({ + multiline = true, + }) + + luaunit.assertTrue(editor.allowNewlines) + + editor = createTextEditor({ + multiline = false, + }) + + luaunit.assertFalse(editor.allowNewlines) +end + +-- Test: allowNewlines can be overridden +function TestTextEditor:test_allowNewlines_override() + local editor = createTextEditor({ + multiline = true, + allowNewlines = false, + }) + + luaunit.assertFalse(editor.allowNewlines) +end + +-- Test: allowTabs defaults to true +function TestTextEditor:test_allowTabs_default() + local editor = createTextEditor() + + luaunit.assertTrue(editor.allowTabs) +end + +-- Test: cursorBlinkRate default +function TestTextEditor:test_cursorBlinkRate_default() + local editor = createTextEditor() + + luaunit.assertEquals(editor.cursorBlinkRate, 0.5) +end + +-- Test: selectOnFocus default +function TestTextEditor:test_selectOnFocus_default() + local editor = createTextEditor() + + luaunit.assertFalse(editor.selectOnFocus) +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/runAll.lua b/testing/runAll.lua index 71ab85e..2309726 100644 --- a/testing/runAll.lua +++ b/testing/runAll.lua @@ -17,17 +17,25 @@ local luaunit = require("testing.luaunit") -- Run all tests in the __tests__ directory local testFiles = { - "testing/__tests__/utils_test.lua", - "testing/__tests__/units_test.lua", + "testing/__tests__/animation_test.lua", "testing/__tests__/color_validation_test.lua", + "testing/__tests__/element_test.lua", + "testing/__tests__/error_handler_test.lua", + "testing/__tests__/event_handler_test.lua", + "testing/__tests__/grid_test.lua", + "testing/__tests__/image_cache_test.lua", + "testing/__tests__/image_renderer_test.lua", + "testing/__tests__/image_scaler_test.lua", + "testing/__tests__/input_event_test.lua", + "testing/__tests__/layout_edge_cases_test.lua", + "testing/__tests__/layout_engine_test.lua", + "testing/__tests__/overflow_test.lua", "testing/__tests__/path_validation_test.lua", "testing/__tests__/sanitization_test.lua", + "testing/__tests__/text_editor_test.lua", "testing/__tests__/theme_test.lua", - "testing/__tests__/layout_engine_test.lua", - "testing/__tests__/element_test.lua", - "testing/__tests__/overflow_test.lua", - "testing/__tests__/grid_test.lua", - "testing/__tests__/layout_edge_cases_test.lua", + "testing/__tests__/units_test.lua", + "testing/__tests__/utils_test.lua", } local success = true @@ -58,19 +66,19 @@ if status then print("\n========================================") print("Generating coverage report...") print("========================================") - + -- Save coverage stats luacov.save_stats() - + -- Run luacov command to generate report (silent) os.execute("luacov 2>/dev/null") - + -- Read and display the summary section from the report local report_file = io.open("luacov.report.out", "r") if report_file then local content = report_file:read("*all") report_file:close() - + -- Extract just the Summary section local summary = content:match("Summary\n=+\n(.-)$") if summary then @@ -79,7 +87,7 @@ if status then print(summary) end end - + print("Full coverage report: luacov.report.out") print("========================================") end