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",