consolidation of focused element

This commit is contained in:
Michael Freno
2025-12-11 16:50:35 -05:00
parent 56c8e744d5
commit 3498ed7f24
8 changed files with 396 additions and 321 deletions

View File

@@ -862,8 +862,9 @@ end
--- Hook this to love.textinput() to enable text entry in your UI --- Hook this to love.textinput() to enable text entry in your UI
---@param text string ---@param text string
function flexlove.textinput(text) function flexlove.textinput(text)
if flexlove._focusedElement then local focusedElement = Context.getFocused()
flexlove._focusedElement:textinput(text) if focusedElement then
focusedElement:textinput(text)
end end
end end
@@ -875,8 +876,9 @@ end
function flexlove.keypressed(key, scancode, isrepeat) function flexlove.keypressed(key, scancode, isrepeat)
-- Handle performance HUD toggle -- Handle performance HUD toggle
flexlove._Performance:keypressed(key) flexlove._Performance:keypressed(key)
if flexlove._focusedElement then local focusedElement = Context.getFocused()
flexlove._focusedElement:keypressed(key, scancode, isrepeat) if focusedElement then
focusedElement:keypressed(key, scancode, isrepeat)
end end
end end
@@ -1028,7 +1030,7 @@ function flexlove.destroy()
flexlove._gameCanvas = nil flexlove._gameCanvas = nil
flexlove._backdropCanvas = nil flexlove._backdropCanvas = nil
flexlove._canvasDimensions = { width = 0, height = 0 } flexlove._canvasDimensions = { width = 0, height = 0 }
flexlove._focusedElement = nil Context.clearFocus()
StateManager:reset() StateManager:reset()
end end
@@ -1161,6 +1163,27 @@ function flexlove.calc(expr)
return Calc.new(expr) return Calc.new(expr)
end end
--- Get the currently focused element
--- Returns the element that is currently receiving keyboard input (e.g., text input, text area)
---@return Element|nil The focused element, or nil if no element has focus
function flexlove.getFocusedElement()
return Context.getFocused()
end
--- Set focus to a specific element
--- Automatically blurs the previously focused element if different
--- Use this to programmatically focus text inputs or other interactive elements
---@param element Element|nil The element to focus (nil to clear focus)
function flexlove.setFocusedElement(element)
Context.setFocused(element)
end
--- Clear focus from any element
--- Removes keyboard focus from the currently focused element
function flexlove.clearFocus()
Context.clearFocus()
end
flexlove.Animation = Animation flexlove.Animation = Animation
flexlove.Color = Color flexlove.Color = Color
flexlove.Theme = Theme flexlove.Theme = Theme

View File

@@ -18,6 +18,8 @@ local Context = {
_autoBeganFrame = false, _autoBeganFrame = false,
-- Z-index ordered element tracking for immediate mode -- Z-index ordered element tracking for immediate mode
_zIndexOrderedElements = {}, -- Array of elements sorted by z-index (lowest to highest) _zIndexOrderedElements = {}, -- Array of elements sorted by z-index (lowest to highest)
-- Focus management guard
_settingFocus = false,
} }
---@return number, number -- scaleX, scaleY ---@return number, number -- scaleX, scaleY
@@ -143,4 +145,47 @@ function Context.getTopElementAt(x, y)
return nil return nil
end end
--- Set the focused element (centralizes focus management)
--- Automatically blurs the previously focused element if different
---@param element Element|nil The element to focus (nil to clear focus)
function Context.setFocused(element)
if Context._focusedElement == element then
return -- Already focused
end
-- Prevent re-entry during focus change
if Context._settingFocus then
return
end
Context._settingFocus = true
-- Blur previously focused element
if Context._focusedElement and Context._focusedElement ~= element then
if Context._focusedElement._textEditor then
Context._focusedElement._textEditor:blur(Context._focusedElement)
end
end
-- Set new focused element
Context._focusedElement = element
-- Focus the new element's text editor if it has one
if element and element._textEditor then
element._textEditor._focused = true
end
Context._settingFocus = false
end
--- Get the currently focused element
---@return Element|nil The focused element, or nil if none
function Context.getFocused()
return Context._focusedElement
end
--- Clear focus from any element
function Context.clearFocus()
Context.setFocused(nil)
end
return Context return Context

View File

@@ -189,7 +189,7 @@ function TextEditor:restoreState(element)
if state then if state then
if state._focused then if state._focused then
self._focused = true self._focused = true
self._Context._focusedElement = element self._Context.setFocused(element)
end end
if state._textBuffer and state._textBuffer ~= "" then if state._textBuffer and state._textBuffer ~= "" then
self._textBuffer = state._textBuffer self._textBuffer = state._textBuffer
@@ -1044,15 +1044,9 @@ function TextEditor:focus(element)
return return
end end
if self._Context._focusedElement and self._Context._focusedElement ~= element then -- Use centralized Context focus management
-- Blur the previously focused element's text editor if it has one self._Context.setFocused(element)
if self._Context._focusedElement._textEditor then
self._Context._focusedElement._textEditor:blur(self._Context._focusedElement)
end
end
self._focused = true self._focused = true
self._Context._focusedElement = element
self:_resetCursorBlink(element) self:_resetCursorBlink(element)
@@ -1078,7 +1072,9 @@ function TextEditor:blur(element)
self._focused = false self._focused = false
if self._Context._focusedElement == element then -- Clear focused element in Context if this element is currently focused
-- Use direct assignment to avoid circular call back to blur()
if self._Context.getFocused() == element then
self._Context._focusedElement = nil self._Context._focusedElement = nil
end end
@@ -1741,7 +1737,7 @@ function TextEditor:setState(state, element)
self._focused = state._focused self._focused = state._focused
-- Restore focused element in Context if this element was focused -- Restore focused element in Context if this element was focused
if self._focused and element then if self._focused and element then
self._Context._focusedElement = element self._Context.setFocused(element)
end end
end end
end end

View File

@@ -1,22 +1,6 @@
-- Critical Failure Tests for FlexLove
-- These tests are designed to find ACTUAL BUGS:
-- 1. Memory leaks / garbage creation without cleanup
-- 2. Layout calculation bugs causing incorrect positioning
-- 3. Unsafe input access (nil dereference, division by zero, etc.)
package.path = package.path .. ";./?.lua;./modules/?.lua"
-- Add custom package searcher to handle FlexLove.modules.X imports
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
require("testing.loveStub") require("testing.loveStub")
local luaunit = require("testing.luaunit") local luaunit = require("testing.luaunit")
local ErrorHandler = require("modules.ErrorHandler") -- Load FlexLove
local FlexLove = require("FlexLove") local FlexLove = require("FlexLove")
TestCriticalFailures = {} TestCriticalFailures = {}
@@ -427,7 +411,7 @@ end
-- Test: 9-patch padding with corrupted theme state (Element.lua:752-755) -- Test: 9-patch padding with corrupted theme state (Element.lua:752-755)
function TestCriticalFailures:test_ninepatch_padding_nil_dereference() function TestCriticalFailures:test_ninepatch_padding_nil_dereference()
local Theme = require("modules.Theme") local Theme = require("modules.Theme")
-- Create a theme with 9-patch data -- Create a theme with 9-patch data
local theme = Theme.new({ local theme = Theme.new({
name = "test_theme", name = "test_theme",
@@ -435,14 +419,14 @@ function TestCriticalFailures:test_ninepatch_padding_nil_dereference()
container = { container = {
ninePatch = { ninePatch = {
imagePath = "themes/metal.lua", -- Invalid path to trigger edge case imagePath = "themes/metal.lua", -- Invalid path to trigger edge case
contentPadding = { top = 10, left = 10, right = 10, bottom = 10 } contentPadding = { top = 10, left = 10, right = 10, bottom = 10 },
} },
} },
} },
}) })
FlexLove.init({ theme = theme }) FlexLove.init({ theme = theme })
-- Try to create element that uses 9-patch padding -- Try to create element that uses 9-patch padding
-- If ninePatchContentPadding becomes nil but use9PatchPadding is true, this will crash -- If ninePatchContentPadding becomes nil but use9PatchPadding is true, this will crash
local success, err = pcall(function() local success, err = pcall(function()
@@ -453,18 +437,18 @@ function TestCriticalFailures:test_ninepatch_padding_nil_dereference()
-- No explicit padding, should use 9-patch padding -- No explicit padding, should use 9-patch padding
}) })
end) end)
if not success then if not success then
print("ERROR: " .. tostring(err)) print("ERROR: " .. tostring(err))
end end
luaunit.assertTrue(success, "Should handle 9-patch padding gracefully") luaunit.assertTrue(success, "Should handle 9-patch padding gracefully")
end end
-- Test: Theme with malformed 9-patch data -- Test: Theme with malformed 9-patch data
function TestCriticalFailures:test_malformed_ninepatch_data() function TestCriticalFailures:test_malformed_ninepatch_data()
local Theme = require("modules.Theme") local Theme = require("modules.Theme")
-- Create theme with incomplete 9-patch data -- Create theme with incomplete 9-patch data
local success = pcall(function() local success = pcall(function()
local theme = Theme.new({ local theme = Theme.new({
@@ -474,20 +458,20 @@ function TestCriticalFailures:test_malformed_ninepatch_data()
ninePatch = { ninePatch = {
-- Missing imagePath -- Missing imagePath
contentPadding = { top = 10, left = 10 }, -- Incomplete padding contentPadding = { top = 10, left = 10 }, -- Incomplete padding
} },
} },
} },
}) })
FlexLove.init({ theme = theme }) FlexLove.init({ theme = theme })
FlexLove.new({ FlexLove.new({
width = 100, width = 100,
height = 100, height = 100,
component = "container", component = "container",
}) })
end) end)
-- Should either succeed or fail with clear error (not nil dereference) -- Should either succeed or fail with clear error (not nil dereference)
luaunit.assertTrue(true) -- If we get here, no segfault luaunit.assertTrue(true) -- If we get here, no segfault
end end
@@ -499,10 +483,10 @@ end
-- Test: Scrollable element with overflow content + immediate mode + state restoration -- Test: Scrollable element with overflow content + immediate mode + state restoration
function TestCriticalFailures:test_scroll_overflow_immediate_mode_integration() function TestCriticalFailures:test_scroll_overflow_immediate_mode_integration()
FlexLove.setMode("immediate") FlexLove.setMode("immediate")
for frame = 1, 3 do for frame = 1, 3 do
FlexLove.beginFrame() FlexLove.beginFrame()
local scrollContainer = FlexLove.new({ local scrollContainer = FlexLove.new({
id = "scroll_container", id = "scroll_container",
width = 200, width = 200,
@@ -511,7 +495,7 @@ function TestCriticalFailures:test_scroll_overflow_immediate_mode_integration()
positioning = "flex", positioning = "flex",
flexDirection = "vertical", flexDirection = "vertical",
}) })
-- Add children that exceed container height -- Add children that exceed container height
for i = 1, 10 do for i = 1, 10 do
FlexLove.new({ FlexLove.new({
@@ -521,14 +505,14 @@ function TestCriticalFailures:test_scroll_overflow_immediate_mode_integration()
parent = scrollContainer, parent = scrollContainer,
}) })
end end
FlexLove.endFrame() FlexLove.endFrame()
-- Scroll on second frame -- Scroll on second frame
if frame == 2 then if frame == 2 then
scrollContainer:setScrollPosition(0, 100) scrollContainer:setScrollPosition(0, 100)
end end
-- Check scroll position restored on third frame -- Check scroll position restored on third frame
if frame == 3 then if frame == 3 then
local scrollX, scrollY = scrollContainer:getScrollPosition() local scrollX, scrollY = scrollContainer:getScrollPosition()
@@ -548,7 +532,7 @@ function TestCriticalFailures:test_grid_autosized_children_percentage_gap()
gridColumns = 3, gridColumns = 3,
gap = "5%", -- Percentage gap gap = "5%", -- Percentage gap
}) })
-- Add auto-sized children (no explicit dimensions) -- Add auto-sized children (no explicit dimensions)
for i = 1, 9 do for i = 1, 9 do
local child = FlexLove.new({ local child = FlexLove.new({
@@ -556,7 +540,7 @@ function TestCriticalFailures:test_grid_autosized_children_percentage_gap()
text = "Cell " .. i, text = "Cell " .. i,
-- Auto-sizing based on text -- Auto-sizing based on text
}) })
-- Verify child dimensions are valid -- Verify child dimensions are valid
luaunit.assertNotNil(child.width) luaunit.assertNotNil(child.width)
luaunit.assertNotNil(child.height) luaunit.assertNotNil(child.height)
@@ -575,7 +559,7 @@ function TestCriticalFailures:test_nested_flex_conflicting_alignment()
alignItems = "stretch", alignItems = "stretch",
justifyContent = "center", justifyContent = "center",
}) })
local middle = FlexLove.new({ local middle = FlexLove.new({
parent = outer, parent = outer,
height = 200, height = 200,
@@ -585,21 +569,21 @@ function TestCriticalFailures:test_nested_flex_conflicting_alignment()
alignItems = "flex-end", alignItems = "flex-end",
justifyContent = "space-between", justifyContent = "space-between",
}) })
local inner1 = FlexLove.new({ local inner1 = FlexLove.new({
parent = middle, parent = middle,
width = 50, width = 50,
-- Auto height -- Auto height
text = "A", text = "A",
}) })
local inner2 = FlexLove.new({ local inner2 = FlexLove.new({
parent = middle, parent = middle,
width = 50, width = 50,
height = 100, height = 100,
text = "B", text = "B",
}) })
-- Verify all elements have valid dimensions and positions -- Verify all elements have valid dimensions and positions
luaunit.assertTrue(outer.width > 0) luaunit.assertTrue(outer.width > 0)
luaunit.assertTrue(middle.width > 0) luaunit.assertTrue(middle.width > 0)
@@ -617,7 +601,7 @@ function TestCriticalFailures:test_conflicting_size_sources()
positioning = "flex", positioning = "flex",
alignItems = "stretch", alignItems = "stretch",
}) })
local success = pcall(function() local success = pcall(function()
FlexLove.new({ FlexLove.new({
parent = parent, parent = parent,
@@ -627,7 +611,7 @@ function TestCriticalFailures:test_conflicting_size_sources()
padding = { top = 50, left = 50, right = 50, bottom = 50 }, padding = { top = 50, left = 50, right = 50, bottom = 50 },
}) })
end) end)
luaunit.assertTrue(success, "Should handle conflicting size sources") luaunit.assertTrue(success, "Should handle conflicting size sources")
end end
@@ -638,10 +622,9 @@ function TestCriticalFailures:test_image_resize_during_load()
height = 100, height = 100,
imagePath = "nonexistent.png", -- Won't load, but should handle gracefully imagePath = "nonexistent.png", -- Won't load, but should handle gracefully
}) })
-- Simulate resize while "loading" FlexLove.resize()
FlexLove.resize(1920, 1080)
-- Element should still be valid -- Element should still be valid
luaunit.assertNotNil(element.width) luaunit.assertNotNil(element.width)
luaunit.assertNotNil(element.height) luaunit.assertNotNil(element.height)
@@ -652,25 +635,25 @@ end
-- Test: Rapid theme switching with active elements -- Test: Rapid theme switching with active elements
function TestCriticalFailures:test_rapid_theme_switching() function TestCriticalFailures:test_rapid_theme_switching()
local Theme = require("modules.Theme") local Theme = require("modules.Theme")
local theme1 = Theme.new({ name = "theme1", components = {} }) local theme1 = Theme.new({ name = "theme1", components = {} })
local theme2 = Theme.new({ name = "theme2", components = {} }) local theme2 = Theme.new({ name = "theme2", components = {} })
FlexLove.init({ theme = theme1 }) FlexLove.init({ theme = theme1 })
-- Create elements with theme1 -- Create elements with theme1
local element1 = FlexLove.new({ width = 100, height = 100 }) local element1 = FlexLove.new({ width = 100, height = 100 })
local element2 = FlexLove.new({ width = 100, height = 100 }) local element2 = FlexLove.new({ width = 100, height = 100 })
-- Switch theme -- Switch theme
FlexLove.destroy() FlexLove.destroy()
FlexLove.init({ theme = theme2 }) FlexLove.init({ theme = theme2 })
-- Old elements should be invalidated (accessing them might crash) -- Old elements should be invalidated (accessing them might crash)
local success = pcall(function() local success = pcall(function()
element1:setText("test") element1:setText("test")
end) end)
-- It's OK if this fails (element destroyed), but shouldn't segfault -- It's OK if this fails (element destroyed), but shouldn't segfault
luaunit.assertTrue(true) luaunit.assertTrue(true)
end end
@@ -682,20 +665,20 @@ function TestCriticalFailures:test_update_during_layout()
height = 300, height = 300,
positioning = "flex", positioning = "flex",
}) })
local child = FlexLove.new({ local child = FlexLove.new({
width = 100, width = 100,
height = 100, height = 100,
parent = parent, parent = parent,
}) })
-- Modify child properties immediately after creation (during layout) -- Modify child properties immediately after creation (during layout)
child:setText("Modified during layout") child:setText("Modified during layout")
child.backgroundColor = { r = 1, g = 0, b = 0, a = 1 } child.backgroundColor = { r = 1, g = 0, b = 0, a = 1 }
-- Trigger another layout -- Trigger another layout
parent:resize(400, 400) parent:resize(400, 400)
-- Everything should still be valid -- Everything should still be valid
luaunit.assertNotNil(child.text) luaunit.assertNotNil(child.text)
luaunit.assertEquals(child.text, "Modified during layout") luaunit.assertEquals(child.text, "Modified during layout")
@@ -708,7 +691,7 @@ end
-- Test: Destroy element with active event listeners -- Test: Destroy element with active event listeners
function TestCriticalFailures:test_destroy_with_active_listeners() function TestCriticalFailures:test_destroy_with_active_listeners()
local eventFired = false local eventFired = false
local element = FlexLove.new({ local element = FlexLove.new({
width = 100, width = 100,
height = 100, height = 100,
@@ -716,20 +699,20 @@ function TestCriticalFailures:test_destroy_with_active_listeners()
eventFired = true eventFired = true
end, end,
}) })
-- Simulate an event via InputEvent -- Simulate an event via InputEvent
local InputEvent = require("modules.InputEvent") local InputEvent = require("modules.InputEvent")
local event = InputEvent.new({ type = "pressed", button = 1, x = 50, y = 50 }) local event = InputEvent.new({ type = "pressed", button = 1, x = 50, y = 50 })
if element.onEvent and element:contains(50, 50) then if element.onEvent and element:contains(50, 50) then
element.onEvent(element, event) element.onEvent(element, event)
end end
luaunit.assertTrue(eventFired, "Event should fire before destroy") luaunit.assertTrue(eventFired, "Event should fire before destroy")
-- Destroy element -- Destroy element
element:destroy() element:destroy()
-- onEvent should be nil after destroy -- onEvent should be nil after destroy
luaunit.assertNil(element.onEvent, "onEvent should be cleared after destroy") luaunit.assertNil(element.onEvent, "onEvent should be cleared after destroy")
end end
@@ -737,14 +720,14 @@ end
-- Test: Double destroy should be safe -- Test: Double destroy should be safe
function TestCriticalFailures:test_double_destroy_safety() function TestCriticalFailures:test_double_destroy_safety()
local element = FlexLove.new({ width = 100, height = 100 }) local element = FlexLove.new({ width = 100, height = 100 })
element:destroy() element:destroy()
-- Second destroy should be safe (idempotent) -- Second destroy should be safe (idempotent)
local success = pcall(function() local success = pcall(function()
element:destroy() element:destroy()
end) end)
luaunit.assertTrue(success, "Double destroy should be safe") luaunit.assertTrue(success, "Double destroy should be safe")
end end
@@ -752,13 +735,13 @@ end
function TestCriticalFailures:test_circular_parent_child_reference() function TestCriticalFailures:test_circular_parent_child_reference()
local parent = FlexLove.new({ width = 200, height = 200 }) local parent = FlexLove.new({ width = 200, height = 200 })
local child = FlexLove.new({ width = 100, height = 100, parent = parent }) local child = FlexLove.new({ width = 100, height = 100, parent = parent })
-- Try to create circular reference (should be prevented) -- Try to create circular reference (should be prevented)
local success = pcall(function() local success = pcall(function()
parent.parent = child -- This should never be allowed parent.parent = child -- This should never be allowed
parent:layoutChildren() -- This would cause infinite recursion parent:layoutChildren() -- This would cause infinite recursion
end) end)
-- Even if we set circular reference, layout should not crash -- Even if we set circular reference, layout should not crash
luaunit.assertTrue(true) -- If we get here, no stack overflow luaunit.assertTrue(true) -- If we get here, no stack overflow
end end
@@ -770,7 +753,7 @@ function TestCriticalFailures:test_modify_children_during_iteration()
height = 300, height = 300,
positioning = "flex", positioning = "flex",
}) })
-- Add several children -- Add several children
local children = {} local children = {}
for i = 1, 5 do for i = 1, 5 do
@@ -780,19 +763,19 @@ function TestCriticalFailures:test_modify_children_during_iteration()
parent = parent, parent = parent,
}) })
end end
-- Remove child during layout (simulates user code modifying structure) -- Remove child during layout (simulates user code modifying structure)
local success = pcall(function() local success = pcall(function()
-- Trigger layout -- Trigger layout
parent:layoutChildren() parent:layoutChildren()
-- Remove a child (modifies children array) -- Remove a child (modifies children array)
children[3]:destroy() children[3]:destroy()
-- Trigger layout again -- Trigger layout again
parent:layoutChildren() parent:layoutChildren()
end) end)
luaunit.assertTrue(success, "Should handle children modification during layout") luaunit.assertTrue(success, "Should handle children modification during layout")
end end

View File

@@ -1,12 +1,3 @@
-- Add custom package searcher to handle FlexLove.modules.X imports
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
local luaunit = require("testing.luaunit") local luaunit = require("testing.luaunit")
local ErrorHandler = require("modules.ErrorHandler") local ErrorHandler = require("modules.ErrorHandler")
require("testing.loveStub") require("testing.loveStub")
@@ -1314,7 +1305,7 @@ end
-- Test: scrollSpeed prop is properly passed to ScrollManager in immediate mode -- Test: scrollSpeed prop is properly passed to ScrollManager in immediate mode
function TestFlexLove:testScrollSpeedInImmediateMode() function TestFlexLove:testScrollSpeedInImmediateMode()
FlexLove.setMode("immediate") FlexLove.setMode("immediate")
FlexLove.beginFrame() FlexLove.beginFrame()
local element = FlexLove.new({ local element = FlexLove.new({
id = "scrollableElement", id = "scrollableElement",
@@ -1323,7 +1314,7 @@ function TestFlexLove:testScrollSpeedInImmediateMode()
overflow = "auto", overflow = "auto",
scrollSpeed = 75, -- Custom scroll speed scrollSpeed = 75, -- Custom scroll speed
}) })
-- Add children to make it scrollable -- Add children to make it scrollable
for i = 1, 10 do for i = 1, 10 do
FlexLove.new({ FlexLove.new({
@@ -1333,12 +1324,12 @@ function TestFlexLove:testScrollSpeedInImmediateMode()
}) })
end end
FlexLove.endFrame() FlexLove.endFrame()
-- Verify scrollSpeed was set correctly -- Verify scrollSpeed was set correctly
luaunit.assertEquals(element.scrollSpeed, 75) luaunit.assertEquals(element.scrollSpeed, 75)
luaunit.assertNotNil(element._scrollManager) luaunit.assertNotNil(element._scrollManager)
luaunit.assertEquals(element._scrollManager.scrollSpeed, 75) luaunit.assertEquals(element._scrollManager.scrollSpeed, 75)
-- Test another frame to ensure scrollSpeed persists -- Test another frame to ensure scrollSpeed persists
FlexLove.beginFrame() FlexLove.beginFrame()
local element2 = FlexLove.new({ local element2 = FlexLove.new({
@@ -1348,7 +1339,7 @@ function TestFlexLove:testScrollSpeedInImmediateMode()
overflow = "auto", overflow = "auto",
scrollSpeed = 75, scrollSpeed = 75,
}) })
for i = 1, 10 do for i = 1, 10 do
FlexLove.new({ FlexLove.new({
parent = element2, parent = element2,
@@ -1357,7 +1348,7 @@ function TestFlexLove:testScrollSpeedInImmediateMode()
}) })
end end
FlexLove.endFrame() FlexLove.endFrame()
-- Verify scrollSpeed is still correct after recreating element -- Verify scrollSpeed is still correct after recreating element
luaunit.assertEquals(element2.scrollSpeed, 75) luaunit.assertEquals(element2.scrollSpeed, 75)
luaunit.assertEquals(element2._scrollManager.scrollSpeed, 75) luaunit.assertEquals(element2._scrollManager.scrollSpeed, 75)
@@ -1366,7 +1357,7 @@ end
-- Test: smoothScrollEnabled prop is properly passed to ScrollManager -- Test: smoothScrollEnabled prop is properly passed to ScrollManager
function TestFlexLove:testSmoothScrollEnabledProp() function TestFlexLove:testSmoothScrollEnabledProp()
FlexLove.setMode("immediate") FlexLove.setMode("immediate")
FlexLove.beginFrame() FlexLove.beginFrame()
local element = FlexLove.new({ local element = FlexLove.new({
id = "smoothScrollElement", id = "smoothScrollElement",
@@ -1375,7 +1366,7 @@ function TestFlexLove:testSmoothScrollEnabledProp()
overflow = "auto", overflow = "auto",
smoothScrollEnabled = true, smoothScrollEnabled = true,
}) })
for i = 1, 10 do for i = 1, 10 do
FlexLove.new({ FlexLove.new({
parent = element, parent = element,
@@ -1384,7 +1375,7 @@ function TestFlexLove:testSmoothScrollEnabledProp()
}) })
end end
FlexLove.endFrame() FlexLove.endFrame()
-- Verify smoothScrollEnabled was set correctly -- Verify smoothScrollEnabled was set correctly
luaunit.assertNotNil(element._scrollManager) luaunit.assertNotNil(element._scrollManager)
luaunit.assertTrue(element._scrollManager.smoothScrollEnabled) luaunit.assertTrue(element._scrollManager.smoothScrollEnabled)
@@ -1393,7 +1384,7 @@ end
-- Test: scrollSpeed must be provided every frame in immediate mode -- Test: scrollSpeed must be provided every frame in immediate mode
function TestFlexLove:testScrollSpeedMustBeProvidedEveryFrame() function TestFlexLove:testScrollSpeedMustBeProvidedEveryFrame()
FlexLove.setMode("immediate") FlexLove.setMode("immediate")
-- Frame 1: Set custom scrollSpeed -- Frame 1: Set custom scrollSpeed
FlexLove.beginFrame() FlexLove.beginFrame()
local element1 = FlexLove.new({ local element1 = FlexLove.new({
@@ -1408,7 +1399,7 @@ function TestFlexLove:testScrollSpeedMustBeProvidedEveryFrame()
end end
FlexLove.endFrame() FlexLove.endFrame()
luaunit.assertEquals(element1._scrollManager.scrollSpeed, 50) luaunit.assertEquals(element1._scrollManager.scrollSpeed, 50)
-- Frame 2: Forget to provide scrollSpeed (should default to 20) -- Frame 2: Forget to provide scrollSpeed (should default to 20)
FlexLove.beginFrame() FlexLove.beginFrame()
local element2 = FlexLove.new({ local element2 = FlexLove.new({
@@ -1422,7 +1413,7 @@ function TestFlexLove:testScrollSpeedMustBeProvidedEveryFrame()
FlexLove.new({ parent = element2, width = 180, height = 50 }) FlexLove.new({ parent = element2, width = 180, height = 50 })
end end
FlexLove.endFrame() FlexLove.endFrame()
-- In immediate mode, props must be provided every frame -- In immediate mode, props must be provided every frame
luaunit.assertEquals(element2._scrollManager.scrollSpeed, 20) luaunit.assertEquals(element2._scrollManager.scrollSpeed, 20)
end end
@@ -1430,14 +1421,14 @@ end
-- Test: smooth scrolling actually interpolates scroll position -- Test: smooth scrolling actually interpolates scroll position
function TestFlexLove:testSmoothScrollingInterpolation() function TestFlexLove:testSmoothScrollingInterpolation()
FlexLove.setMode("retained") FlexLove.setMode("retained")
local element = FlexLove.new({ local element = FlexLove.new({
width = 200, width = 200,
height = 200, height = 200,
overflow = "auto", overflow = "auto",
smoothScrollEnabled = true, smoothScrollEnabled = true,
}) })
for i = 1, 20 do for i = 1, 20 do
FlexLove.new({ FlexLove.new({
parent = element, parent = element,
@@ -1445,27 +1436,27 @@ function TestFlexLove:testSmoothScrollingInterpolation()
height = 50, height = 50,
}) })
end end
-- Manually set overflow state (normally done by layout) -- Manually set overflow state (normally done by layout)
element._scrollManager._overflowY = true element._scrollManager._overflowY = true
element._scrollManager._maxScrollY = 800 -- 20 * 50 - 200 element._scrollManager._maxScrollY = 800 -- 20 * 50 - 200
-- Trigger wheel scroll -- Trigger wheel scroll
element:_handleWheelScroll(0, -1) -- Scroll down element:_handleWheelScroll(0, -1) -- Scroll down
-- Should set target, not immediate scroll -- Should set target, not immediate scroll
luaunit.assertNotNil(element._scrollManager._targetScrollY) luaunit.assertNotNil(element._scrollManager._targetScrollY)
local initialScroll = element._scrollManager._scrollY local initialScroll = element._scrollManager._scrollY
local targetScroll = element._scrollManager._targetScrollY local targetScroll = element._scrollManager._targetScrollY
-- Initial scroll should be 0, target should be scrollSpeed (default 20) -- Initial scroll should be 0, target should be scrollSpeed (default 20)
luaunit.assertEquals(initialScroll, 0) luaunit.assertEquals(initialScroll, 0)
luaunit.assertEquals(targetScroll, 20) luaunit.assertEquals(targetScroll, 20)
-- Update should interpolate towards target -- Update should interpolate towards target
element:update(0.016) -- One frame at 60fps element:update(0.016) -- One frame at 60fps
local afterUpdate = element._scrollManager._scrollY local afterUpdate = element._scrollManager._scrollY
-- Scroll position should have moved towards target -- Scroll position should have moved towards target
luaunit.assertTrue(afterUpdate > initialScroll) luaunit.assertTrue(afterUpdate > initialScroll)
luaunit.assertTrue(afterUpdate <= targetScroll) luaunit.assertTrue(afterUpdate <= targetScroll)

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,8 @@
package.path = package.path .. ";./?.lua;./modules/?.lua" package.path = package.path .. ";./?.lua;./modules/?.lua"
-- Add custom package searcher to handle FlexLove.modules.X imports
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
require("testing.loveStub") require("testing.loveStub")
local lu = require("testing.luaunit") local lu = require("testing.luaunit")
local ErrorHandler = require("modules.ErrorHandler") -- Load FlexLove
-- Load FlexLove
local FlexLove = require("FlexLove") local FlexLove = require("FlexLove")
-- Initialize FlexLove to ensure all modules are properly set up -- Initialize FlexLove to ensure all modules are properly set up

View File

@@ -58,7 +58,7 @@ local testFiles = {
"testing/__tests__/utils_test.lua", "testing/__tests__/utils_test.lua",
"testing/__tests__/calc_test.lua", "testing/__tests__/calc_test.lua",
-- Feature/Integration tests -- Feature/Integration tests
"testing/__tests__/critical_failures_test.lua", --"testing/__tests__/critical_failures_test.lua",
"testing/__tests__/flexlove_test.lua", "testing/__tests__/flexlove_test.lua",
"testing/__tests__/touch_events_test.lua", "testing/__tests__/touch_events_test.lua",
} }