diff --git a/.luacov b/.luacov
index 758eba7..de5ce90 100644
--- a/.luacov
+++ b/.luacov
@@ -13,6 +13,7 @@ return {
"tasks",
"themes",
"luarocks",
+ "loveStub",
},
-- Run reporter by default
diff --git a/testing/__tests__/animation_coverage_test.lua b/testing/__tests__/animation_coverage_test.lua
new file mode 100644
index 0000000..5ddf2e8
--- /dev/null
+++ b/testing/__tests__/animation_coverage_test.lua
@@ -0,0 +1,354 @@
+-- Advanced test suite for Animation.lua to increase coverage
+-- Focuses on uncovered edge cases, error handling, and complex scenarios
+
+package.path = package.path .. ";./?.lua;./modules/?.lua"
+
+require("testing.loveStub")
+
+local luaunit = require("testing.luaunit")
+local ErrorHandler = require("modules.ErrorHandler")
+
+-- Initialize ErrorHandler
+ErrorHandler.init({})
+
+-- Load FlexLove which properly initializes all dependencies
+local FlexLove = require("FlexLove")
+
+-- Initialize FlexLove
+FlexLove.init()
+
+local Animation = FlexLove.Animation
+
+-- Test suite for Animation error handling and validation
+TestAnimationValidation = {}
+
+function TestAnimationValidation:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestAnimationValidation:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestAnimationValidation:test_new_with_invalid_props()
+ -- Should handle non-table props gracefully
+ local anim = Animation.new(nil)
+ luaunit.assertNotNil(anim)
+ luaunit.assertEquals(anim.duration, 1)
+
+ local anim2 = Animation.new("invalid")
+ luaunit.assertNotNil(anim2)
+ luaunit.assertEquals(anim2.duration, 1)
+end
+
+function TestAnimationValidation:test_new_with_invalid_duration()
+ -- Negative duration
+ local anim = Animation.new({
+ duration = -1,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+ luaunit.assertEquals(anim.duration, 1) -- Should default to 1
+
+ -- Zero duration
+ local anim2 = Animation.new({
+ duration = 0,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+ luaunit.assertEquals(anim2.duration, 1)
+
+ -- Non-number duration
+ local anim3 = Animation.new({
+ duration = "invalid",
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+ luaunit.assertEquals(anim3.duration, 1)
+end
+
+function TestAnimationValidation:test_new_with_invalid_start_final()
+ -- Invalid start table
+ local anim = Animation.new({
+ duration = 1,
+ start = "invalid",
+ final = { x = 100 },
+ })
+ luaunit.assertEquals(type(anim.start), "table")
+
+ -- Invalid final table
+ local anim2 = Animation.new({
+ duration = 1,
+ start = { x = 0 },
+ final = "invalid",
+ })
+ luaunit.assertEquals(type(anim2.final), "table")
+end
+
+function TestAnimationValidation:test_easing_string_and_function()
+ -- Valid easing string
+ local anim = Animation.new({
+ duration = 1,
+ easing = "easeInQuad",
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+ luaunit.assertEquals(type(anim.easing), "function")
+
+ -- Invalid easing string (should default to linear)
+ local anim2 = Animation.new({
+ duration = 1,
+ easing = "invalidEasing",
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+ luaunit.assertEquals(type(anim2.easing), "function")
+
+ -- Custom easing function
+ local customEasing = function(t) return t * t end
+ local anim3 = Animation.new({
+ duration = 1,
+ easing = customEasing,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+ luaunit.assertEquals(anim3.easing, customEasing)
+end
+
+-- Test suite for Animation update with edge cases
+TestAnimationUpdate = {}
+
+function TestAnimationUpdate:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestAnimationUpdate:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestAnimationUpdate:test_update_with_invalid_dt()
+ local anim = Animation.new({
+ duration = 1,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+
+ -- Negative dt
+ anim:update(-1)
+ luaunit.assertEquals(anim.elapsed, 0)
+
+ -- NaN dt
+ anim:update(0/0)
+ luaunit.assertEquals(anim.elapsed, 0)
+
+ -- Infinite dt
+ anim:update(math.huge)
+ luaunit.assertEquals(anim.elapsed, 0)
+
+ -- String dt (non-number)
+ anim:update("invalid")
+ luaunit.assertEquals(anim.elapsed, 0)
+end
+
+function TestAnimationUpdate:test_update_while_paused()
+ local anim = Animation.new({
+ duration = 1,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+
+ anim:pause()
+ local complete = anim:update(0.5)
+
+ luaunit.assertFalse(complete)
+ luaunit.assertEquals(anim.elapsed, 0)
+end
+
+function TestAnimationUpdate:test_callbacks()
+ local onStartCalled = false
+ local onUpdateCalled = false
+ local onCompleteCalled = false
+
+ local anim = Animation.new({
+ duration = 0.1,
+ start = { x = 0 },
+ final = { x = 100 },
+ onStart = function()
+ onStartCalled = true
+ end,
+ onUpdate = function()
+ onUpdateCalled = true
+ end,
+ onComplete = function()
+ onCompleteCalled = true
+ end,
+ })
+
+ -- First update should trigger onStart
+ anim:update(0.05)
+ luaunit.assertTrue(onStartCalled)
+ luaunit.assertTrue(onUpdateCalled)
+ luaunit.assertFalse(onCompleteCalled)
+
+ -- Complete the animation
+ anim:update(0.1)
+ luaunit.assertTrue(onCompleteCalled)
+end
+
+function TestAnimationUpdate:test_onCancel_callback()
+ local onCancelCalled = false
+
+ local anim = Animation.new({
+ duration = 1,
+ start = { x = 0 },
+ final = { x = 100 },
+ onCancel = function()
+ onCancelCalled = true
+ end,
+ })
+
+ anim:update(0.5)
+ anim:cancel()
+
+ luaunit.assertTrue(onCancelCalled)
+end
+
+-- Test suite for Animation state control
+TestAnimationStateControl = {}
+
+function TestAnimationStateControl:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestAnimationStateControl:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestAnimationStateControl:test_pause_resume()
+ local anim = Animation.new({
+ duration = 1,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+
+ anim:update(0.5)
+ local elapsed1 = anim.elapsed
+
+ anim:pause()
+ anim:update(0.5)
+ luaunit.assertEquals(anim.elapsed, elapsed1) -- Should not advance
+
+ anim:resume()
+ anim:update(0.1)
+ luaunit.assertTrue(anim.elapsed > elapsed1) -- Should advance
+end
+
+function TestAnimationStateControl:test_reverse()
+ local anim = Animation.new({
+ duration = 1,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+
+ anim:update(0.5)
+ anim:reverse()
+
+ luaunit.assertTrue(anim._reversed)
+
+ -- Continue updating - it should go backwards
+ anim:update(0.3)
+ luaunit.assertTrue(anim.elapsed < 0.5)
+end
+
+function TestAnimationStateControl:test_setSpeed()
+ local anim = Animation.new({
+ duration = 1,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+
+ anim:setSpeed(2.0)
+ luaunit.assertEquals(anim._speed, 2.0)
+
+ -- Update with 0.1 seconds at 2x speed should advance 0.2 seconds
+ anim:update(0.1)
+ luaunit.assertAlmostEquals(anim.elapsed, 0.2, 0.01)
+end
+
+function TestAnimationStateControl:test_reset()
+ local anim = Animation.new({
+ duration = 1,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+
+ anim:update(0.7)
+ luaunit.assertTrue(anim.elapsed > 0)
+
+ anim:reset()
+ luaunit.assertEquals(anim.elapsed, 0)
+ luaunit.assertFalse(anim._hasStarted)
+end
+
+function TestAnimationStateControl:test_isPaused_isComplete()
+ local anim = Animation.new({
+ duration = 0.5,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+
+ luaunit.assertFalse(anim:isPaused())
+
+ anim:pause()
+ luaunit.assertTrue(anim:isPaused())
+
+ anim:resume()
+ luaunit.assertFalse(anim:isPaused())
+
+ local complete = anim:update(1.0) -- Complete it
+ luaunit.assertTrue(complete)
+ luaunit.assertEquals(anim:getState(), "completed")
+end
+
+-- Test suite for delay functionality
+TestAnimationDelay = {}
+
+function TestAnimationDelay:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestAnimationDelay:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestAnimationDelay:test_delay()
+ local anim = Animation.new({
+ duration = 1,
+ start = { x = 0 },
+ final = { x = 100 },
+ })
+
+ anim:delay(0.5)
+
+ -- Update during delay - animation should not start yet
+ local result = anim:update(0.3)
+ luaunit.assertFalse(result)
+ luaunit.assertEquals(anim:getState(), "pending")
+
+ -- Update past delay - animation should be ready to start
+ anim:update(0.3) -- Now delay elapsed is > 0.5
+ luaunit.assertEquals(anim:getState(), "pending") -- Still pending until next update
+
+ -- One more update to actually start
+ anim:update(0.01)
+ luaunit.assertEquals(anim:getState(), "playing")
+end
+
+-- Run all tests
+if not _G.RUNNING_ALL_TESTS then
+ os.exit(luaunit.LuaUnit.run())
+end
diff --git a/testing/__tests__/element_coverage_test.lua b/testing/__tests__/element_coverage_test.lua
new file mode 100644
index 0000000..464244c
--- /dev/null
+++ b/testing/__tests__/element_coverage_test.lua
@@ -0,0 +1,612 @@
+-- Advanced test suite for Element.lua to increase coverage
+-- Focuses on uncovered edge cases and complex scenarios
+
+package.path = package.path .. ";./?.lua;./modules/?.lua"
+
+require("testing.loveStub")
+
+local luaunit = require("testing.luaunit")
+local FlexLove = require("FlexLove")
+local Color = require("modules.Color")
+
+-- Initialize FlexLove
+FlexLove.init()
+
+-- Test suite for resize behavior with different unit types
+TestElementResize = {}
+
+function TestElementResize:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestElementResize:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestElementResize:test_resize_with_percentage_units()
+ -- Test that percentage units calculate correctly initially
+ local parent = FlexLove.new({
+ id = "resize_parent",
+ x = 0,
+ y = 0,
+ width = 1000,
+ height = 500,
+ })
+
+ local child = FlexLove.new({
+ id = "resize_child",
+ width = "50%",
+ height = "50%",
+ parent = parent,
+ })
+
+ -- Initial calculation should be 50% of parent
+ luaunit.assertEquals(child.width, 500)
+ luaunit.assertEquals(child.height, 250)
+
+ -- Verify units are stored correctly
+ luaunit.assertEquals(child.units.width.unit, "%")
+ luaunit.assertEquals(child.units.height.unit, "%")
+end
+
+function TestElementResize:test_resize_with_viewport_units()
+ -- Test that viewport units calculate correctly
+ local element = FlexLove.new({
+ id = "vp_resize",
+ x = 0,
+ y = 0,
+ width = "50vw",
+ height = "50vh",
+ })
+
+ -- Should be 50% of viewport (1920x1080)
+ luaunit.assertEquals(element.width, 960)
+ luaunit.assertEquals(element.height, 540)
+
+ -- Verify units are stored correctly
+ luaunit.assertEquals(element.units.width.unit, "vw")
+ luaunit.assertEquals(element.units.height.unit, "vh")
+end
+
+function TestElementResize:test_resize_with_textSize_scaling()
+ -- Test that textSize with viewport units calculates correctly
+ local element = FlexLove.new({
+ id = "text_resize",
+ x = 0,
+ y = 0,
+ width = 200,
+ height = 100,
+ text = "Test",
+ textSize = "2vh",
+ autoScaleText = true,
+ })
+
+ -- 2vh of 1080 = 21.6
+ luaunit.assertAlmostEquals(element.textSize, 21.6, 0.1)
+
+ -- Verify unit is stored
+ luaunit.assertEquals(element.units.textSize.unit, "vh")
+end
+
+-- Test suite for positioning offset application (top/right/bottom/left)
+TestElementPositioningOffsets = {}
+
+function TestElementPositioningOffsets:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestElementPositioningOffsets:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestElementPositioningOffsets:test_applyPositioningOffsets_with_absolute()
+ local parent = FlexLove.new({
+ id = "offset_parent",
+ x = 0,
+ y = 0,
+ width = 500,
+ height = 500,
+ positioning = "absolute",
+ })
+
+ local child = FlexLove.new({
+ id = "offset_child",
+ width = 100,
+ height = 100,
+ positioning = "absolute",
+ top = 50,
+ left = 50,
+ parent = parent,
+ })
+
+ -- Apply positioning offsets
+ parent:applyPositioningOffsets(child)
+
+ -- Child should be offset from parent
+ luaunit.assertTrue(child.y >= parent.y + 50)
+ luaunit.assertTrue(child.x >= parent.x + 50)
+end
+
+function TestElementPositioningOffsets:test_applyPositioningOffsets_with_right_bottom()
+ local parent = FlexLove.new({
+ id = "rb_parent",
+ x = 0,
+ y = 0,
+ width = 500,
+ height = 500,
+ positioning = "relative",
+ })
+
+ local child = FlexLove.new({
+ id = "rb_child",
+ width = 100,
+ height = 100,
+ positioning = "absolute",
+ right = 50,
+ bottom = 50,
+ parent = parent,
+ })
+
+ parent:applyPositioningOffsets(child)
+
+ -- Child should be positioned from right/bottom
+ luaunit.assertNotNil(child.x)
+ luaunit.assertNotNil(child.y)
+end
+
+-- Test suite for scroll-related methods
+TestElementScrollMethods = {}
+
+function TestElementScrollMethods:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestElementScrollMethods:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestElementScrollMethods:test_scrollToTop()
+ local container = FlexLove.new({
+ id = "scroll_container",
+ x = 0,
+ y = 0,
+ width = 300,
+ height = 200,
+ overflow = "scroll",
+ positioning = "flex",
+ flexDirection = "vertical",
+ })
+
+ -- Add content that overflows
+ for i = 1, 10 do
+ FlexLove.new({
+ id = "item_" .. i,
+ width = 280,
+ height = 50,
+ parent = container,
+ })
+ end
+
+ -- Scroll down first
+ container:setScrollPosition(nil, 100)
+ local _, scrollY = container:getScrollPosition()
+ luaunit.assertEquals(scrollY, 100)
+
+ -- Scroll to top
+ container:scrollToTop()
+ _, scrollY = container:getScrollPosition()
+ luaunit.assertEquals(scrollY, 0)
+end
+
+function TestElementScrollMethods:test_scrollToBottom()
+ local container = FlexLove.new({
+ id = "scroll_bottom",
+ x = 0,
+ y = 0,
+ width = 300,
+ height = 200,
+ overflow = "scroll",
+ positioning = "flex",
+ flexDirection = "vertical",
+ })
+
+ -- Add overflowing content
+ for i = 1, 10 do
+ FlexLove.new({
+ id = "item_" .. i,
+ width = 280,
+ height = 50,
+ parent = container,
+ })
+ end
+
+ container:scrollToBottom()
+
+ local _, scrollY = container:getScrollPosition()
+ local _, maxScrollY = container:getMaxScroll()
+
+ luaunit.assertEquals(scrollY, maxScrollY)
+end
+
+function TestElementScrollMethods:test_scrollBy()
+ local container = FlexLove.new({
+ id = "scroll_by",
+ x = 0,
+ y = 0,
+ width = 300,
+ height = 200,
+ overflow = "scroll",
+ positioning = "flex",
+ flexDirection = "vertical",
+ })
+
+ for i = 1, 10 do
+ FlexLove.new({
+ id = "item_" .. i,
+ width = 280,
+ height = 50,
+ parent = container,
+ })
+ end
+
+ container:scrollBy(nil, 50)
+ local _, scrollY = container:getScrollPosition()
+ luaunit.assertEquals(scrollY, 50)
+
+ container:scrollBy(nil, 25)
+ _, scrollY = container:getScrollPosition()
+ luaunit.assertEquals(scrollY, 75)
+end
+
+function TestElementScrollMethods:test_getScrollPercentage()
+ local container = FlexLove.new({
+ id = "scroll_pct",
+ x = 0,
+ y = 0,
+ width = 300,
+ height = 200,
+ overflow = "scroll",
+ positioning = "flex",
+ flexDirection = "vertical",
+ })
+
+ for i = 1, 10 do
+ FlexLove.new({
+ id = "item_" .. i,
+ width = 280,
+ height = 50,
+ parent = container,
+ })
+ end
+
+ -- At top
+ local _, percentY = container:getScrollPercentage()
+ luaunit.assertEquals(percentY, 0)
+
+ -- Scroll halfway
+ local _, maxScrollY = container:getMaxScroll()
+ container:setScrollPosition(nil, maxScrollY / 2)
+ _, percentY = container:getScrollPercentage()
+ luaunit.assertAlmostEquals(percentY, 0.5, 0.01)
+end
+
+-- Test suite for auto-sizing with complex scenarios
+TestElementComplexAutoSizing = {}
+
+function TestElementComplexAutoSizing:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestElementComplexAutoSizing:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestElementComplexAutoSizing:test_autosize_with_nested_flex()
+ local root = FlexLove.new({
+ id = "root",
+ x = 0,
+ y = 0,
+ positioning = "flex",
+ flexDirection = "vertical",
+ })
+
+ local row1 = FlexLove.new({
+ id = "row1",
+ positioning = "flex",
+ flexDirection = "horizontal",
+ parent = root,
+ })
+
+ FlexLove.new({
+ id = "item1",
+ width = 100,
+ height = 50,
+ parent = row1,
+ })
+
+ FlexLove.new({
+ id = "item2",
+ width = 100,
+ height = 50,
+ parent = row1,
+ })
+
+ -- Root should auto-size to contain row
+ luaunit.assertTrue(root.width >= 200)
+ luaunit.assertTrue(root.height >= 50)
+end
+
+function TestElementComplexAutoSizing:test_autosize_with_absolutely_positioned_child()
+ local parent = FlexLove.new({
+ id = "abs_parent",
+ x = 0,
+ y = 0,
+ positioning = "flex",
+ })
+
+ -- Regular child affects size
+ FlexLove.new({
+ id = "regular",
+ width = 100,
+ height = 100,
+ parent = parent,
+ })
+
+ -- Absolutely positioned child should NOT affect parent size
+ FlexLove.new({
+ id = "absolute",
+ width = 200,
+ height = 200,
+ positioning = "absolute",
+ parent = parent,
+ })
+
+ -- Parent should only size to regular child
+ luaunit.assertTrue(parent.width < 150)
+ luaunit.assertTrue(parent.height < 150)
+end
+
+function TestElementComplexAutoSizing:test_autosize_with_margin()
+ local parent = FlexLove.new({
+ id = "margin_parent",
+ x = 0,
+ y = 0,
+ positioning = "flex",
+ flexDirection = "horizontal",
+ })
+
+ -- Add two children with margins to test margin collapsing
+ FlexLove.new({
+ id = "margin_child1",
+ width = 100,
+ height = 100,
+ margin = { right = 20 },
+ parent = parent,
+ })
+
+ FlexLove.new({
+ id = "margin_child2",
+ width = 100,
+ height = 100,
+ margin = { left = 20 },
+ parent = parent,
+ })
+
+ -- Parent should size to children (margins don't add to content size in flex layout)
+ luaunit.assertEquals(parent.width, 200)
+ luaunit.assertEquals(parent.height, 100)
+end
+
+-- Test suite for theme integration
+TestElementThemeIntegration = {}
+
+function TestElementThemeIntegration:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestElementThemeIntegration:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestElementThemeIntegration:test_getScaledContentPadding()
+ local element = FlexLove.new({
+ id = "themed",
+ x = 0,
+ y = 0,
+ width = 200,
+ height = 100,
+ })
+
+ local padding = element:getScaledContentPadding()
+ -- Should return nil if no theme component
+ luaunit.assertNil(padding)
+end
+
+function TestElementThemeIntegration:test_getAvailableContentWidth_with_padding()
+ local element = FlexLove.new({
+ id = "content_width",
+ x = 0,
+ y = 0,
+ width = 200,
+ height = 100,
+ padding = 10,
+ })
+
+ local availableWidth = element:getAvailableContentWidth()
+ -- Should be width minus padding
+ luaunit.assertEquals(availableWidth, 180) -- 200 - 10*2
+end
+
+function TestElementThemeIntegration:test_getAvailableContentHeight_with_padding()
+ local element = FlexLove.new({
+ id = "content_height",
+ x = 0,
+ y = 0,
+ width = 200,
+ height = 100,
+ padding = 10,
+ })
+
+ local availableHeight = element:getAvailableContentHeight()
+ luaunit.assertEquals(availableHeight, 80) -- 100 - 10*2
+end
+
+-- Test suite for child management edge cases
+TestElementChildManagementEdgeCases = {}
+
+function TestElementChildManagementEdgeCases:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestElementChildManagementEdgeCases:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestElementChildManagementEdgeCases:test_addChild_triggers_autosize_recalc()
+ local parent = FlexLove.new({
+ id = "dynamic_parent",
+ x = 0,
+ y = 0,
+ positioning = "flex",
+ })
+
+ local initialWidth = parent.width
+ local initialHeight = parent.height
+
+ -- Add child dynamically
+ local child = FlexLove.new({
+ id = "dynamic_child",
+ width = 150,
+ height = 150,
+ })
+
+ parent:addChild(child)
+
+ -- Parent should have resized
+ luaunit.assertTrue(parent.width >= initialWidth)
+ luaunit.assertTrue(parent.height >= initialHeight)
+end
+
+function TestElementChildManagementEdgeCases:test_removeChild_triggers_autosize_recalc()
+ local parent = FlexLove.new({
+ id = "shrink_parent",
+ x = 0,
+ y = 0,
+ positioning = "flex",
+ })
+
+ local child1 = FlexLove.new({
+ id = "child1",
+ width = 100,
+ height = 100,
+ parent = parent,
+ })
+
+ local child2 = FlexLove.new({
+ id = "child2",
+ width = 100,
+ height = 100,
+ parent = parent,
+ })
+
+ local widthWithTwo = parent.width
+
+ parent:removeChild(child2)
+
+ -- Parent should shrink
+ luaunit.assertTrue(parent.width < widthWithTwo)
+end
+
+function TestElementChildManagementEdgeCases:test_clearChildren_resets_autosize()
+ local parent = FlexLove.new({
+ id = "clear_parent",
+ x = 0,
+ y = 0,
+ positioning = "flex",
+ })
+
+ for i = 1, 5 do
+ FlexLove.new({
+ id = "child_" .. i,
+ width = 50,
+ height = 50,
+ parent = parent,
+ })
+ end
+
+ local widthWithChildren = parent.width
+
+ parent:clearChildren()
+
+ -- Parent should shrink to minimal size
+ luaunit.assertTrue(parent.width < widthWithChildren)
+ luaunit.assertEquals(#parent.children, 0)
+end
+
+-- Test suite for grid layout edge cases
+TestElementGridEdgeCases = {}
+
+function TestElementGridEdgeCases:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestElementGridEdgeCases:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestElementGridEdgeCases:test_grid_with_uneven_children()
+ local grid = FlexLove.new({
+ id = "uneven_grid",
+ x = 0,
+ y = 0,
+ width = 300,
+ height = 300,
+ positioning = "grid",
+ gridRows = 2,
+ gridColumns = 2,
+ })
+
+ -- Add only 3 children to a 2x2 grid
+ for i = 1, 3 do
+ FlexLove.new({
+ id = "grid_item_" .. i,
+ width = 50,
+ height = 50,
+ parent = grid,
+ })
+ end
+
+ luaunit.assertEquals(#grid.children, 3)
+end
+
+function TestElementGridEdgeCases:test_grid_with_percentage_gaps()
+ local grid = FlexLove.new({
+ id = "pct_gap_grid",
+ x = 0,
+ y = 0,
+ width = 400,
+ height = 400,
+ positioning = "grid",
+ gridRows = 2,
+ gridColumns = 2,
+ columnGap = "5%",
+ rowGap = "5%",
+ })
+
+ luaunit.assertNotNil(grid.columnGap)
+ luaunit.assertNotNil(grid.rowGap)
+ luaunit.assertTrue(grid.columnGap > 0)
+ luaunit.assertTrue(grid.rowGap > 0)
+end
+
+-- Run tests
+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 af6272b..08065b2 100644
--- a/testing/__tests__/element_test.lua
+++ b/testing/__tests__/element_test.lua
@@ -1512,7 +1512,6 @@ function TestElementUnhappyPaths:test_clear_children_twice()
luaunit.assertEquals(#parent.children, 0)
end
-
-- Test: Element contains with NaN coordinates
function TestElementUnhappyPaths:test_contains_nan_coordinates()
local element = FlexLove.new({
@@ -1542,7 +1541,6 @@ function TestElementUnhappyPaths:test_scroll_without_manager()
luaunit.assertTrue(true)
end
-
-- Test: Element scrollBy with nil values
function TestElementUnhappyPaths:test_scroll_by_nil()
local element = FlexLove.new({
@@ -1747,11 +1745,6 @@ function TestElementUnhappyPaths:test_invalid_gap()
gap = -10,
})
luaunit.assertNotNil(element)
-end
- gridRows = 0,
- gridColumns = 0,
- })
- luaunit.assertNotNil(element)
-- Negative rows/columns
element = FlexLove.new({
@@ -1790,7 +1783,6 @@ function TestElementUnhappyPaths:test_set_text_nil()
luaunit.assertNil(element.text)
end
-
-- Test: Element with conflicting size constraints
function TestElementUnhappyPaths:test_conflicting_size_constraints()
-- Width less than padding
diff --git a/testing/__tests__/renderer_texteditor_bugs_test.lua b/testing/__tests__/renderer_texteditor_bugs_test.lua
new file mode 100644
index 0000000..7d27ecf
--- /dev/null
+++ b/testing/__tests__/renderer_texteditor_bugs_test.lua
@@ -0,0 +1,773 @@
+-- Bug-finding and error handling tests for Renderer and TextEditor
+-- Tests edge cases, nil handling, division by zero, invalid inputs, etc.
+
+package.path = package.path .. ";./?.lua;./modules/?.lua"
+
+require("testing.loveStub")
+local luaunit = require("testing.luaunit")
+local ErrorHandler = require("modules.ErrorHandler")
+
+-- Initialize ErrorHandler
+ErrorHandler.init({})
+
+local FlexLove = require("FlexLove")
+FlexLove.init()
+
+-- ============================================================================
+-- Renderer Bug Tests
+-- ============================================================================
+
+TestRendererBugs = {}
+
+function TestRendererBugs:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestRendererBugs:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestRendererBugs:test_nil_background_color()
+ -- Should handle nil backgroundColor gracefully
+ local element = FlexLove.new({
+ id = "test",
+ width = 100,
+ height = 100,
+ backgroundColor = nil,
+ })
+
+ luaunit.assertNotNil(element)
+ luaunit.assertNotNil(element.backgroundColor)
+end
+
+function TestRendererBugs:test_invalid_opacity()
+ -- Opacity > 1
+ local element = FlexLove.new({
+ id = "test1",
+ width = 100,
+ height = 100,
+ opacity = 5,
+ })
+ luaunit.assertNotNil(element)
+
+ -- Negative opacity
+ local element2 = FlexLove.new({
+ id = "test2",
+ width = 100,
+ height = 100,
+ opacity = -1,
+ })
+ luaunit.assertNotNil(element2)
+
+ -- NaN opacity
+ local element3 = FlexLove.new({
+ id = "test3",
+ width = 100,
+ height = 100,
+ opacity = 0 / 0,
+ })
+ luaunit.assertNotNil(element3)
+end
+
+function TestRendererBugs:test_invalid_corner_radius()
+ -- Negative corner radius
+ local element = FlexLove.new({
+ id = "test",
+ width = 100,
+ height = 100,
+ cornerRadius = -10,
+ })
+ luaunit.assertNotNil(element)
+
+ -- Huge corner radius (larger than element)
+ local element2 = FlexLove.new({
+ id = "test2",
+ width = 100,
+ height = 100,
+ cornerRadius = 1000,
+ })
+ luaunit.assertNotNil(element2)
+end
+
+function TestRendererBugs:test_invalid_border_config()
+ -- Non-boolean border values
+ local element = FlexLove.new({
+ id = "test",
+ width = 100,
+ height = 100,
+ border = {
+ top = "yes",
+ right = 1,
+ bottom = nil,
+ left = {},
+ },
+ })
+ luaunit.assertNotNil(element)
+end
+
+function TestRendererBugs:test_missing_image_path()
+ -- Non-existent image path
+ local element = FlexLove.new({
+ id = "test",
+ width = 100,
+ height = 100,
+ imagePath = "/nonexistent/path/to/image.png",
+ })
+ luaunit.assertNotNil(element)
+end
+
+function TestRendererBugs:test_invalid_object_fit()
+ -- Invalid objectFit value
+ local element = FlexLove.new({
+ id = "test",
+ width = 100,
+ height = 100,
+ imagePath = "test.png",
+ objectFit = "invalid-value",
+ })
+ luaunit.assertNotNil(element)
+ luaunit.assertEquals(element.objectFit, "invalid-value") -- Should store but might break rendering
+end
+
+function TestRendererBugs:test_zero_dimensions()
+ -- Zero width
+ local element = FlexLove.new({
+ id = "test1",
+ width = 0,
+ height = 100,
+ })
+ luaunit.assertNotNil(element)
+
+ -- Zero height
+ local element2 = FlexLove.new({
+ id = "test2",
+ width = 100,
+ height = 0,
+ })
+ luaunit.assertNotNil(element2)
+
+ -- Both zero
+ local element3 = FlexLove.new({
+ id = "test3",
+ width = 0,
+ height = 0,
+ })
+ luaunit.assertNotNil(element3)
+end
+
+function TestRendererBugs:test_negative_dimensions()
+ -- Negative width
+ local element = FlexLove.new({
+ id = "test1",
+ width = -100,
+ height = 100,
+ })
+ luaunit.assertNotNil(element)
+
+ -- Negative height
+ local element2 = FlexLove.new({
+ id = "test2",
+ width = 100,
+ height = -100,
+ })
+ luaunit.assertNotNil(element2)
+end
+
+function TestRendererBugs:test_text_rendering_with_nil_text()
+ local element = FlexLove.new({
+ id = "test",
+ width = 100,
+ height = 100,
+ text = nil,
+ })
+ luaunit.assertNotNil(element)
+end
+
+function TestRendererBugs:test_text_rendering_with_empty_string()
+ local element = FlexLove.new({
+ id = "test",
+ width = 100,
+ height = 100,
+ text = "",
+ })
+ luaunit.assertNotNil(element)
+ luaunit.assertEquals(element.text, "")
+end
+
+function TestRendererBugs:test_text_rendering_with_very_long_text()
+ local longText = string.rep("A", 10000)
+ local element = FlexLove.new({
+ id = "test",
+ width = 100,
+ height = 100,
+ text = longText,
+ })
+ luaunit.assertNotNil(element)
+end
+
+function TestRendererBugs:test_text_rendering_with_special_characters()
+ -- Newlines
+ local element1 = FlexLove.new({
+ id = "test1",
+ width = 100,
+ height = 100,
+ text = "Line1\nLine2\nLine3",
+ })
+ luaunit.assertNotNil(element1)
+
+ -- Tabs
+ local element2 = FlexLove.new({
+ id = "test2",
+ width = 100,
+ height = 100,
+ text = "Col1\tCol2\tCol3",
+ })
+ luaunit.assertNotNil(element2)
+
+ -- Unicode
+ local element3 = FlexLove.new({
+ id = "test3",
+ width = 100,
+ height = 100,
+ text = "Hello δΈη π",
+ })
+ luaunit.assertNotNil(element3)
+end
+
+function TestRendererBugs:test_invalid_text_align()
+ local element = FlexLove.new({
+ id = "test",
+ width = 100,
+ height = 100,
+ text = "Test",
+ textAlign = "invalid-alignment",
+ })
+ luaunit.assertNotNil(element)
+end
+
+function TestRendererBugs:test_invalid_text_size()
+ -- Zero text size
+ local element1 = FlexLove.new({
+ id = "test1",
+ width = 100,
+ height = 100,
+ text = "Test",
+ textSize = 0,
+ })
+ luaunit.assertNotNil(element1)
+
+ -- Negative text size
+ local element2 = FlexLove.new({
+ id = "test2",
+ width = 100,
+ height = 100,
+ text = "Test",
+ textSize = -10,
+ })
+ luaunit.assertNotNil(element2)
+
+ -- Huge text size
+ local element3 = FlexLove.new({
+ id = "test3",
+ width = 100,
+ height = 100,
+ text = "Test",
+ textSize = 10000,
+ })
+ luaunit.assertNotNil(element3)
+end
+
+function TestRendererBugs:test_blur_with_invalid_intensity()
+ -- Negative intensity
+ local element1 = FlexLove.new({
+ id = "test1",
+ width = 100,
+ height = 100,
+ contentBlur = { intensity = -10, quality = 5 },
+ })
+ luaunit.assertNotNil(element1)
+
+ -- Intensity > 100
+ local element2 = FlexLove.new({
+ id = "test2",
+ width = 100,
+ height = 100,
+ backdropBlur = { intensity = 200, quality = 5 },
+ })
+ luaunit.assertNotNil(element2)
+end
+
+function TestRendererBugs:test_blur_with_invalid_quality()
+ -- Quality < 1
+ local element1 = FlexLove.new({
+ id = "test1",
+ width = 100,
+ height = 100,
+ contentBlur = { intensity = 10, quality = 0 },
+ })
+ luaunit.assertNotNil(element1)
+
+ -- Quality > 10
+ local element2 = FlexLove.new({
+ id = "test2",
+ width = 100,
+ height = 100,
+ contentBlur = { intensity = 10, quality = 100 },
+ })
+ luaunit.assertNotNil(element2)
+end
+
+function TestRendererBugs:test_theme_with_invalid_component()
+ local element = FlexLove.new({
+ id = "test",
+ width = 100,
+ height = 100,
+ theme = "nonexistent-theme",
+ themeComponent = "nonexistent-component",
+ })
+ luaunit.assertNotNil(element)
+end
+
+-- ============================================================================
+-- TextEditor Bug Tests
+-- ============================================================================
+
+TestTextEditorBugs = {}
+
+function TestTextEditorBugs:setUp()
+ love.window.setMode(1920, 1080)
+ FlexLove.beginFrame()
+end
+
+function TestTextEditorBugs:tearDown()
+ FlexLove.endFrame()
+end
+
+function TestTextEditorBugs:test_editable_without_text()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ })
+ luaunit.assertNotNil(element)
+ luaunit.assertEquals(element.text, "")
+end
+
+function TestTextEditorBugs:test_editable_with_nil_text()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = nil,
+ })
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_cursor_position_beyond_text_length()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "Hello",
+ })
+
+ -- Try to set cursor beyond text length
+ if element._textEditor then
+ element._textEditor:setCursorPosition(1000)
+ -- Should clamp to text length
+ luaunit.assertTrue(element._textEditor:getCursorPosition() <= 5)
+ end
+end
+
+function TestTextEditorBugs:test_cursor_position_negative()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "Hello",
+ })
+
+ if element._textEditor then
+ element._textEditor:setCursorPosition(-10)
+ -- Should clamp to 0
+ luaunit.assertEquals(element._textEditor:getCursorPosition(), 0)
+ end
+end
+
+function TestTextEditorBugs:test_selection_with_invalid_range()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "Hello World",
+ })
+
+ if element._textEditor then
+ -- Start > end
+ element._textEditor:setSelection(10, 2)
+ luaunit.assertNotNil(element._textEditor:getSelection())
+
+ -- Both beyond text length
+ element._textEditor:setSelection(100, 200)
+ luaunit.assertNotNil(element._textEditor:getSelection())
+
+ -- Negative values
+ element._textEditor:setSelection(-5, -1)
+ luaunit.assertNotNil(element._textEditor:getSelection())
+ end
+end
+
+function TestTextEditorBugs:test_insert_text_at_invalid_position()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "Hello",
+ })
+
+ if element._textEditor then
+ -- Insert beyond text length
+ element._textEditor:insertText(" World", 1000)
+ luaunit.assertNotNil(element._textEditor:getText())
+
+ -- Insert at negative position
+ element._textEditor:insertText("X", -10)
+ luaunit.assertNotNil(element._textEditor:getText())
+ end
+end
+
+function TestTextEditorBugs:test_delete_text_with_invalid_range()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "Hello World",
+ })
+
+ if element._textEditor then
+ local originalText = element._textEditor:getText()
+
+ -- Delete beyond text length
+ element._textEditor:deleteText(5, 1000)
+ luaunit.assertNotNil(element._textEditor:getText())
+
+ -- Delete with negative positions
+ element._textEditor:deleteText(-10, -5)
+ luaunit.assertNotNil(element._textEditor:getText())
+
+ -- Delete with start > end
+ element._textEditor:deleteText(10, 5)
+ luaunit.assertNotNil(element._textEditor:getText())
+ end
+end
+
+function TestTextEditorBugs:test_max_length_zero()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "",
+ maxLength = 0,
+ })
+
+ if element._textEditor then
+ element._textEditor:setText("Should not appear")
+ luaunit.assertEquals(element._textEditor:getText(), "")
+ end
+end
+
+function TestTextEditorBugs:test_max_length_negative()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "Test",
+ maxLength = -10,
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_max_lines_zero()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 100,
+ editable = true,
+ multiline = true,
+ text = "",
+ maxLines = 0,
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_multiline_with_very_long_lines()
+ local longLine = string.rep("A", 10000)
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 100,
+ editable = true,
+ multiline = true,
+ text = longLine,
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_text_wrap_with_zero_width()
+ local element = FlexLove.new({
+ id = "test",
+ width = 0,
+ height = 100,
+ editable = true,
+ multiline = true,
+ textWrap = "word",
+ text = "This should wrap",
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_password_mode_with_empty_text()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ passwordMode = true,
+ text = "",
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_input_type_number_with_non_numeric()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ inputType = "number",
+ text = "abc123def",
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_cursor_blink_rate_zero()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ cursorBlinkRate = 0,
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_cursor_blink_rate_negative()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ cursorBlinkRate = -1,
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_text_editor_update_with_invalid_dt()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "Test",
+ })
+
+ if element._textEditor then
+ -- Negative dt
+ element._textEditor:update(-1)
+
+ -- NaN dt
+ element._textEditor:update(0 / 0)
+
+ -- Infinite dt
+ element._textEditor:update(math.huge)
+
+ -- All should handle gracefully
+ luaunit.assertNotNil(element._textEditor)
+ end
+end
+
+function TestTextEditorBugs:test_placeholder_with_text()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "Actual text",
+ placeholder = "Placeholder",
+ })
+
+ luaunit.assertNotNil(element)
+ luaunit.assertEquals(element.text, "Actual text")
+end
+
+function TestTextEditorBugs:test_sanitization_with_malicious_input()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "",
+ sanitize = true,
+ })
+
+ luaunit.assertNotNil(element)
+ -- Text should be sanitized
+ luaunit.assertNotNil(element.text)
+end
+
+function TestTextEditorBugs:test_text_overflow_with_no_scrollable()
+ local element = FlexLove.new({
+ id = "test",
+ width = 50,
+ height = 30,
+ editable = true,
+ text = "This is a very long text that will overflow",
+ textOverflow = "ellipsis",
+ scrollable = false,
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_auto_grow_with_fixed_height()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ multiline = true,
+ autoGrow = true,
+ text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_select_on_focus_with_empty_text()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ selectOnFocus = true,
+ text = "",
+ })
+
+ luaunit.assertNotNil(element)
+
+ if element._textEditor then
+ element._textEditor:focus()
+ -- Should not crash with empty text
+ luaunit.assertNotNil(element._textEditor)
+ end
+end
+
+function TestTextEditorBugs:test_word_navigation_with_no_words()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = " ", -- Only spaces
+ })
+
+ if element._textEditor then
+ element._textEditor:moveCursorToNextWord()
+ luaunit.assertNotNil(element._textEditor:getCursorPosition())
+
+ element._textEditor:moveCursorToPreviousWord()
+ luaunit.assertNotNil(element._textEditor:getCursorPosition())
+ end
+end
+
+function TestTextEditorBugs:test_word_navigation_with_single_character()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "A",
+ })
+
+ if element._textEditor then
+ element._textEditor:moveCursorToNextWord()
+ luaunit.assertNotNil(element._textEditor:getCursorPosition())
+ end
+end
+
+function TestTextEditorBugs:test_multiline_with_only_newlines()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 100,
+ editable = true,
+ multiline = true,
+ text = "\n\n\n\n",
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_text_with_null_bytes()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "Hello\0World",
+ })
+
+ luaunit.assertNotNil(element)
+end
+
+function TestTextEditorBugs:test_concurrent_focus_blur()
+ local element = FlexLove.new({
+ id = "test",
+ width = 200,
+ height = 30,
+ editable = true,
+ text = "Test",
+ })
+
+ if element._textEditor then
+ element._textEditor:focus()
+ element._textEditor:blur()
+ element._textEditor:focus()
+ element._textEditor:blur()
+
+ luaunit.assertNotNil(element._textEditor)
+ end
+end
+
+-- Run tests
+if not _G.RUNNING_ALL_TESTS then
+ os.exit(luaunit.LuaUnit.run())
+end
diff --git a/testing/__tests__/text_editor_coverage_test.lua b/testing/__tests__/text_editor_coverage_test.lua
new file mode 100644
index 0000000..d0be8ba
--- /dev/null
+++ b/testing/__tests__/text_editor_coverage_test.lua
@@ -0,0 +1,693 @@
+-- Comprehensive coverage tests for TextEditor module
+-- Focuses on multiline, wrapping, keyboard/mouse interactions, and advanced features
+
+package.path = package.path .. ";./?.lua;./modules/?.lua"
+
+require("testing.loveStub")
+local luaunit = require("testing.luaunit")
+local ErrorHandler = require("modules.ErrorHandler")
+
+-- Initialize ErrorHandler
+ErrorHandler.init({})
+
+local FlexLove = require("FlexLove")
+FlexLove.init()
+
+local TextEditor = require("modules.TextEditor")
+local Color = require("modules.Color")
+local utils = require("modules.utils")
+
+-- Mock dependencies
+local MockContext = {
+ _immediateMode = false,
+ _focusedElement = nil,
+ setFocusedElement = function(self, element)
+ self._focusedElement = element
+ end,
+}
+
+local MockStateManager = {
+ getState = function(id) return nil end,
+ updateState = function(id, state) end,
+}
+
+-- Helper to create TextEditor
+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(width, height)
+ return {
+ _stateId = "test-element",
+ width = width or 200,
+ height = height or 100,
+ padding = {top = 5, right = 5, bottom = 5, left = 5},
+ getScaledContentPadding = function(self)
+ return self.padding
+ end,
+ _renderer = {
+ getFont = function()
+ return {
+ getWidth = function(text) return #text * 8 end,
+ getHeight = function() return 16 end,
+ }
+ end,
+ wrapLine = function(element, line, maxWidth)
+ -- Simple word wrapping simulation
+ local words = {}
+ for word in line:gmatch("%S+") do
+ table.insert(words, word)
+ end
+
+ local wrapped = {}
+ local currentLine = ""
+ local startIdx = 0
+
+ for i, word in ipairs(words) do
+ local testLine = currentLine == "" and word or (currentLine .. " " .. word)
+ if #testLine * 8 <= maxWidth then
+ currentLine = testLine
+ else
+ if currentLine ~= "" then
+ table.insert(wrapped, {text = currentLine, startIdx = startIdx, endIdx = startIdx + #currentLine})
+ startIdx = startIdx + #currentLine + 1
+ end
+ currentLine = word
+ end
+ end
+
+ if currentLine ~= "" then
+ table.insert(wrapped, {text = currentLine, startIdx = startIdx, endIdx = startIdx + #currentLine})
+ end
+
+ return #wrapped > 0 and wrapped or {{text = line, startIdx = 0, endIdx = #line}}
+ end,
+ },
+ }
+end
+
+-- ============================================================================
+-- Multiline Text Tests
+-- ============================================================================
+
+TestTextEditorMultiline = {}
+
+function TestTextEditorMultiline:test_multiline_split_lines()
+ local editor = createTextEditor({multiline = true, text = "Line 1\nLine 2\nLine 3"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:_splitLines()
+ luaunit.assertNotNil(editor._lines)
+ luaunit.assertEquals(#editor._lines, 3)
+ luaunit.assertEquals(editor._lines[1], "Line 1")
+ luaunit.assertEquals(editor._lines[2], "Line 2")
+ luaunit.assertEquals(editor._lines[3], "Line 3")
+end
+
+function TestTextEditorMultiline:test_multiline_cursor_movement()
+ local editor = createTextEditor({multiline = true, text = "Line 1\nLine 2"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ -- Move to end
+ editor:moveCursorToEnd()
+ luaunit.assertEquals(editor:getCursorPosition(), 13) -- "Line 1\nLine 2" = 13 chars
+
+ -- Move to start
+ editor:moveCursorToStart()
+ luaunit.assertEquals(editor:getCursorPosition(), 0)
+end
+
+function TestTextEditorMultiline:test_multiline_line_start_end()
+ local editor = createTextEditor({multiline = true, text = "Line 1\nLine 2"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ -- Position in middle of first line
+ editor:setCursorPosition(3)
+
+ -- Move to line start
+ editor:moveCursorToLineStart()
+ luaunit.assertEquals(editor:getCursorPosition(), 0)
+
+ -- Move to line end
+ editor:moveCursorToLineEnd()
+ luaunit.assertEquals(editor:getCursorPosition(), 6)
+end
+
+function TestTextEditorMultiline:test_multiline_insert_newline()
+ local editor = createTextEditor({multiline = true, text = "Hello"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setCursorPosition(5)
+ editor:insertText("\n", 5)
+ editor:insertText("World", 6)
+
+ luaunit.assertEquals(editor:getText(), "Hello\nWorld")
+end
+
+-- ============================================================================
+-- Text Wrapping Tests
+-- ============================================================================
+
+TestTextEditorWrapping = {}
+
+function TestTextEditorWrapping:test_word_wrapping()
+ local editor = createTextEditor({
+ multiline = true,
+ textWrap = "word",
+ text = "This is a long line that should wrap"
+ })
+ local element = createMockElement(100, 100) -- Narrow width to force wrapping
+ editor:initialize(element)
+
+ editor:_calculateWrapping()
+ luaunit.assertNotNil(editor._wrappedLines)
+ luaunit.assertTrue(#editor._wrappedLines > 1) -- Should wrap into multiple lines
+end
+
+function TestTextEditorWrapping:test_char_wrapping()
+ local editor = createTextEditor({
+ multiline = true,
+ textWrap = "char",
+ text = "Verylongwordwithoutspaces"
+ })
+ local element = createMockElement(100, 100)
+ editor:initialize(element)
+
+ editor:_calculateWrapping()
+ luaunit.assertNotNil(editor._wrappedLines)
+end
+
+function TestTextEditorWrapping:test_no_wrapping()
+ local editor = createTextEditor({
+ multiline = true,
+ textWrap = false,
+ text = "This is a long line that should not wrap"
+ })
+ local element = createMockElement(100, 100)
+ editor:initialize(element)
+
+ editor:_calculateWrapping()
+ luaunit.assertNotNil(editor._wrappedLines)
+end
+
+-- ============================================================================
+-- Selection Tests
+-- ============================================================================
+
+TestTextEditorSelection = {}
+
+function TestTextEditorSelection:test_select_all()
+ local editor = createTextEditor({text = "Hello World"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:selectAll()
+ luaunit.assertTrue(editor:hasSelection())
+ luaunit.assertEquals(editor:getSelectedText(), "Hello World")
+end
+
+function TestTextEditorSelection:test_get_selected_text()
+ local editor = createTextEditor({text = "Hello World"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setSelection(0, 5)
+ luaunit.assertEquals(editor:getSelectedText(), "Hello")
+end
+
+function TestTextEditorSelection:test_delete_selection()
+ local editor = createTextEditor({text = "Hello World", editable = true})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setSelection(0, 5)
+ editor:deleteSelection()
+ luaunit.assertEquals(editor:getText(), " World")
+end
+
+function TestTextEditorSelection:test_clear_selection()
+ local editor = createTextEditor({text = "Hello World"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setSelection(0, 5)
+ luaunit.assertTrue(editor:hasSelection())
+
+ editor:clearSelection()
+ luaunit.assertFalse(editor:hasSelection())
+end
+
+function TestTextEditorSelection:test_selection_reversed()
+ local editor = createTextEditor({text = "Hello World"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ -- Set selection in reverse order
+ editor:setSelection(5, 0)
+ local start, endPos = editor:getSelection()
+ luaunit.assertEquals(start, 0)
+ luaunit.assertEquals(endPos, 5)
+end
+
+-- ============================================================================
+-- Focus and Blur Tests
+-- ============================================================================
+
+TestTextEditorFocus = {}
+
+function TestTextEditorFocus:test_focus()
+ local focusCalled = false
+ local editor = createTextEditor({
+ text = "Test",
+ onFocus = function() focusCalled = true end
+ })
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ luaunit.assertTrue(editor:isFocused())
+ luaunit.assertTrue(focusCalled)
+end
+
+function TestTextEditorFocus:test_blur()
+ local blurCalled = false
+ local editor = createTextEditor({
+ text = "Test",
+ onBlur = function() blurCalled = true end
+ })
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ editor:blur()
+ luaunit.assertFalse(editor:isFocused())
+ luaunit.assertTrue(blurCalled)
+end
+
+function TestTextEditorFocus:test_select_on_focus()
+ local editor = createTextEditor({
+ text = "Hello World",
+ selectOnFocus = true
+ })
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ luaunit.assertTrue(editor:hasSelection())
+ luaunit.assertEquals(editor:getSelectedText(), "Hello World")
+end
+
+-- ============================================================================
+-- Keyboard Input Tests
+-- ============================================================================
+
+TestTextEditorKeyboard = {}
+
+function TestTextEditorKeyboard:test_handle_text_input()
+ local editor = createTextEditor({text = "", editable = true})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ editor:handleTextInput("H")
+ editor:handleTextInput("i")
+
+ luaunit.assertEquals(editor:getText(), "Hi")
+end
+
+function TestTextEditorKeyboard:test_handle_backspace()
+ local editor = createTextEditor({text = "Hello", editable = true})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ editor:setCursorPosition(5)
+ editor:handleKeyPress("backspace", "backspace", false)
+
+ luaunit.assertEquals(editor:getText(), "Hell")
+end
+
+function TestTextEditorKeyboard:test_handle_delete()
+ local editor = createTextEditor({text = "Hello", editable = true})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ editor:setCursorPosition(0)
+ editor:handleKeyPress("delete", "delete", false)
+
+ luaunit.assertEquals(editor:getText(), "ello")
+end
+
+function TestTextEditorKeyboard:test_handle_return_multiline()
+ local editor = createTextEditor({text = "Hello", editable = true, multiline = true})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ editor:setCursorPosition(5)
+ editor:handleKeyPress("return", "return", false)
+ editor:handleTextInput("World")
+
+ luaunit.assertEquals(editor:getText(), "Hello\nWorld")
+end
+
+function TestTextEditorKeyboard:test_handle_return_singleline()
+ local onEnterCalled = false
+ local editor = createTextEditor({
+ text = "Hello",
+ editable = true,
+ multiline = false,
+ onEnter = function() onEnterCalled = true end
+ })
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ editor:handleKeyPress("return", "return", false)
+
+ luaunit.assertTrue(onEnterCalled)
+ luaunit.assertEquals(editor:getText(), "Hello") -- Should not add newline
+end
+
+function TestTextEditorKeyboard:test_handle_tab()
+ local editor = createTextEditor({text = "Hello", editable = true, allowTabs = true})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ editor:setCursorPosition(5)
+ editor:handleKeyPress("tab", "tab", false)
+
+ luaunit.assertEquals(editor:getText(), "Hello\t")
+end
+
+function TestTextEditorKeyboard:test_handle_home_end()
+ local editor = createTextEditor({text = "Hello World"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setCursorPosition(5)
+
+ -- Home key
+ editor:handleKeyPress("home", "home", false)
+ luaunit.assertEquals(editor:getCursorPosition(), 0)
+
+ -- End key
+ editor:handleKeyPress("end", "end", false)
+ luaunit.assertEquals(editor:getCursorPosition(), 11)
+end
+
+function TestTextEditorKeyboard:test_handle_arrow_keys()
+ local editor = createTextEditor({text = "Hello"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setCursorPosition(2)
+
+ -- Right arrow
+ editor:handleKeyPress("right", "right", false)
+ luaunit.assertEquals(editor:getCursorPosition(), 3)
+
+ -- Left arrow
+ editor:handleKeyPress("left", "left", false)
+ luaunit.assertEquals(editor:getCursorPosition(), 2)
+end
+
+-- ============================================================================
+-- Mouse Interaction Tests
+-- ============================================================================
+
+TestTextEditorMouse = {}
+
+function TestTextEditorMouse:test_mouse_to_text_position()
+ local editor = createTextEditor({text = "Hello World"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ -- Click in middle of text (approximate)
+ local pos = editor:mouseToTextPosition(40, 10)
+ luaunit.assertNotNil(pos)
+ luaunit.assertTrue(pos >= 0 and pos <= 11)
+end
+
+function TestTextEditorMouse:test_handle_single_click()
+ local editor = createTextEditor({text = "Hello World"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:handleTextClick(40, 10, 1)
+ luaunit.assertTrue(editor:getCursorPosition() >= 0)
+end
+
+function TestTextEditorMouse:test_handle_double_click_selects_word()
+ local editor = createTextEditor({text = "Hello World"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ -- Double click on first word
+ editor:handleTextClick(20, 10, 2)
+ luaunit.assertTrue(editor:hasSelection())
+ local selected = editor:getSelectedText()
+ luaunit.assertTrue(selected == "Hello" or selected == "World")
+end
+
+function TestTextEditorMouse:test_handle_triple_click_selects_all()
+ local editor = createTextEditor({text = "Hello World"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:handleTextClick(20, 10, 3)
+ luaunit.assertTrue(editor:hasSelection())
+ luaunit.assertEquals(editor:getSelectedText(), "Hello World")
+end
+
+function TestTextEditorMouse:test_handle_text_drag()
+ local editor = createTextEditor({text = "Hello World"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ -- Start at position 0
+ editor:handleTextClick(0, 10, 1)
+
+ -- Drag to position further right
+ editor:handleTextDrag(40, 10)
+
+ luaunit.assertTrue(editor:hasSelection())
+end
+
+-- ============================================================================
+-- Password Mode Tests
+-- ============================================================================
+
+TestTextEditorPassword = {}
+
+function TestTextEditorPassword:test_password_mode_masks_text()
+ local editor = createTextEditor({text = "secret123", passwordMode = true})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ -- Password mode should be enabled
+ luaunit.assertTrue(editor.passwordMode)
+
+ -- The actual text should still be stored
+ luaunit.assertEquals(editor:getText(), "secret123")
+end
+
+-- ============================================================================
+-- Input Validation Tests
+-- ============================================================================
+
+TestTextEditorValidation = {}
+
+function TestTextEditorValidation:test_number_input_type()
+ local editor = createTextEditor({text = "", editable = true, inputType = "number"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ editor:handleTextInput("123")
+ luaunit.assertEquals(editor:getText(), "123")
+
+ -- Non-numeric input should be sanitized
+ editor:handleTextInput("abc")
+ -- Sanitization behavior depends on implementation
+end
+
+function TestTextEditorValidation:test_max_length()
+ local editor = createTextEditor({text = "", editable = true, maxLength = 5})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setText("12345")
+ luaunit.assertEquals(editor:getText(), "12345")
+
+ editor:setText("123456789")
+ luaunit.assertEquals(editor:getText(), "12345") -- Should be truncated
+end
+
+function TestTextEditorValidation:test_max_lines()
+ local editor = createTextEditor({
+ text = "",
+ editable = true,
+ multiline = true,
+ maxLines = 2
+ })
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setText("Line 1\nLine 2")
+ luaunit.assertEquals(editor:getText(), "Line 1\nLine 2")
+
+ editor:setText("Line 1\nLine 2\nLine 3")
+ -- Should be limited to 2 lines
+ local lines = {}
+ for line in editor:getText():gmatch("[^\n]+") do
+ table.insert(lines, line)
+ end
+ luaunit.assertTrue(#lines <= 2)
+end
+
+-- ============================================================================
+-- Cursor Blink and Update Tests
+-- ============================================================================
+
+TestTextEditorUpdate = {}
+
+function TestTextEditorUpdate:test_update_cursor_blink()
+ local editor = createTextEditor({text = "Test", cursorBlinkRate = 0.5})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+
+ -- Initial state
+ local initialVisible = editor._cursorVisible
+
+ -- Update for half the blink rate
+ editor:update(0.25)
+ luaunit.assertEquals(editor._cursorVisible, initialVisible)
+
+ -- Update to complete blink cycle
+ editor:update(0.26)
+ luaunit.assertNotEquals(editor._cursorVisible, initialVisible)
+end
+
+function TestTextEditorUpdate:test_cursor_blink_pause()
+ local editor = createTextEditor({text = "Test", cursorBlinkRate = 0.5})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:focus()
+ editor:_resetCursorBlink(true) -- Pause blink
+
+ luaunit.assertTrue(editor._cursorBlinkPaused)
+ luaunit.assertTrue(editor._cursorVisible)
+end
+
+-- ============================================================================
+-- Word Navigation Tests
+-- ============================================================================
+
+TestTextEditorWordNav = {}
+
+function TestTextEditorWordNav:test_move_to_next_word()
+ local editor = createTextEditor({text = "Hello World Test"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setCursorPosition(0)
+ editor:moveCursorToNextWord()
+
+ luaunit.assertTrue(editor:getCursorPosition() > 0)
+end
+
+function TestTextEditorWordNav:test_move_to_previous_word()
+ local editor = createTextEditor({text = "Hello World Test"})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setCursorPosition(16)
+ editor:moveCursorToPreviousWord()
+
+ luaunit.assertTrue(editor:getCursorPosition() < 16)
+end
+
+-- ============================================================================
+-- Sanitization Tests
+-- ============================================================================
+
+TestTextEditorSanitization = {}
+
+function TestTextEditorSanitization:test_sanitize_disabled()
+ local editor = createTextEditor({text = "", editable = true, sanitize = false})
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setText("", true) -- Skip sanitization
+ -- With sanitization disabled, text should be preserved
+ luaunit.assertNotNil(editor:getText())
+end
+
+function TestTextEditorSanitization:test_custom_sanitizer()
+ local customCalled = false
+ local editor = createTextEditor({
+ text = "",
+ editable = true,
+ customSanitizer = function(text)
+ customCalled = true
+ return text:upper()
+ end
+ })
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setText("hello")
+ luaunit.assertTrue(customCalled)
+ luaunit.assertEquals(editor:getText(), "HELLO")
+end
+
+function TestTextEditorSanitization:test_disallow_newlines()
+ local editor = createTextEditor({
+ text = "",
+ editable = true,
+ multiline = false,
+ allowNewlines = false
+ })
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setText("Hello\nWorld")
+ -- Newlines should be removed or replaced
+ luaunit.assertFalse(editor:getText():find("\n"))
+end
+
+function TestTextEditorSanitization:test_disallow_tabs()
+ local editor = createTextEditor({
+ text = "",
+ editable = true,
+ allowTabs = false
+ })
+ local element = createMockElement()
+ editor:initialize(element)
+
+ editor:setText("Hello\tWorld")
+ -- Tabs should be removed or replaced
+ luaunit.assertFalse(editor:getText():find("\t"))
+end
+
+-- Run tests
+if not _G.RUNNING_ALL_TESTS then
+ os.exit(luaunit.LuaUnit.run())
+end
diff --git a/testing/runAll.lua b/testing/runAll.lua
index 8f41fed..5d7f7ee 100644
--- a/testing/runAll.lua
+++ b/testing/runAll.lua
@@ -37,11 +37,13 @@ local luaunit = require("testing.luaunit")
-- Run all tests in the __tests__ directory
local testFiles = {
+ "testing/__tests__/animation_coverage_test.lua",
"testing/__tests__/animation_test.lua",
"testing/__tests__/animation_properties_test.lua",
"testing/__tests__/blur_test.lua",
"testing/__tests__/critical_failures_test.lua",
"testing/__tests__/easing_test.lua",
+ "testing/__tests__/element_coverage_test.lua",
"testing/__tests__/element_test.lua",
"testing/__tests__/event_handler_test.lua",
"testing/__tests__/flexlove_test.lua",
@@ -62,8 +64,10 @@ local testFiles = {
"testing/__tests__/performance_instrumentation_test.lua",
"testing/__tests__/performance_warnings_test.lua",
"testing/__tests__/renderer_test.lua",
+ "testing/__tests__/renderer_texteditor_bugs_test.lua",
"testing/__tests__/roundedrect_test.lua",
"testing/__tests__/sanitization_test.lua",
+ "testing/__tests__/text_editor_coverage_test.lua",
"testing/__tests__/text_editor_test.lua",
"testing/__tests__/theme_test.lua",
"testing/__tests__/touch_events_test.lua",