almost
This commit is contained in:
44
FlexLove.lua
44
FlexLove.lua
@@ -21,6 +21,7 @@ local utils = req("utils")
|
|||||||
local Units = req("Units")
|
local Units = req("Units")
|
||||||
local GuiState = req("GuiState")
|
local GuiState = req("GuiState")
|
||||||
local ImmediateModeState = req("ImmediateModeState")
|
local ImmediateModeState = req("ImmediateModeState")
|
||||||
|
local StateManager = req("StateManager")
|
||||||
|
|
||||||
-- externals
|
-- externals
|
||||||
---@type Theme
|
---@type Theme
|
||||||
@@ -131,6 +132,7 @@ function Gui.beginFrame()
|
|||||||
-- Increment frame counter
|
-- Increment frame counter
|
||||||
Gui._frameNumber = Gui._frameNumber + 1
|
Gui._frameNumber = Gui._frameNumber + 1
|
||||||
ImmediateModeState.incrementFrame()
|
ImmediateModeState.incrementFrame()
|
||||||
|
StateManager.incrementFrame()
|
||||||
|
|
||||||
-- Clear current frame elements
|
-- Clear current frame elements
|
||||||
Gui._currentFrameElements = {}
|
Gui._currentFrameElements = {}
|
||||||
@@ -167,6 +169,9 @@ function Gui.endFrame()
|
|||||||
state._textBuffer = element._textBuffer
|
state._textBuffer = element._textBuffer
|
||||||
state._scrollX = element._scrollX
|
state._scrollX = element._scrollX
|
||||||
state._scrollY = element._scrollY
|
state._scrollY = element._scrollY
|
||||||
|
state._scrollbarDragging = element._scrollbarDragging
|
||||||
|
state._hoveredScrollbar = element._hoveredScrollbar
|
||||||
|
state._scrollbarDragOffset = element._scrollbarDragOffset
|
||||||
|
|
||||||
ImmediateModeState.setState(element.id, state)
|
ImmediateModeState.setState(element.id, state)
|
||||||
end
|
end
|
||||||
@@ -174,9 +179,11 @@ function Gui.endFrame()
|
|||||||
|
|
||||||
-- Cleanup stale states
|
-- Cleanup stale states
|
||||||
ImmediateModeState.cleanup()
|
ImmediateModeState.cleanup()
|
||||||
|
StateManager.cleanup()
|
||||||
|
|
||||||
-- Force cleanup if we have too many states
|
-- Force cleanup if we have too many states
|
||||||
ImmediateModeState.forceCleanupIfNeeded()
|
ImmediateModeState.forceCleanupIfNeeded()
|
||||||
|
StateManager.forceCleanupIfNeeded()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Canvas cache for game rendering
|
-- Canvas cache for game rendering
|
||||||
@@ -442,7 +449,9 @@ function Gui.wheelmoved(x, y)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local scrollableElement = findScrollableAtPosition(Gui.topElements, mx, my)
|
-- In immediate mode, use current frame elements; in retained mode, use topElements
|
||||||
|
local elements = Gui._immediateMode and Gui._currentFrameElements or Gui.topElements
|
||||||
|
local scrollableElement = findScrollableAtPosition(elements, mx, my)
|
||||||
if scrollableElement then
|
if scrollableElement then
|
||||||
scrollableElement:_handleWheelScroll(x, y)
|
scrollableElement:_handleWheelScroll(x, y)
|
||||||
end
|
end
|
||||||
@@ -492,7 +501,7 @@ function Gui.new(props)
|
|||||||
-- Create the element
|
-- Create the element
|
||||||
local element = Element.new(props)
|
local element = Element.new(props)
|
||||||
|
|
||||||
-- Bind persistent state to element
|
-- Bind persistent state to element (ImmediateModeState)
|
||||||
-- Copy stateful properties from persistent state
|
-- Copy stateful properties from persistent state
|
||||||
element._pressed = state._pressed or {}
|
element._pressed = state._pressed or {}
|
||||||
element._lastClickTime = state._lastClickTime
|
element._lastClickTime = state._lastClickTime
|
||||||
@@ -510,6 +519,37 @@ function Gui.new(props)
|
|||||||
element._textBuffer = state._textBuffer or element.text or ""
|
element._textBuffer = state._textBuffer or element.text or ""
|
||||||
element._scrollX = state._scrollX or element._scrollX or 0
|
element._scrollX = state._scrollX or element._scrollX or 0
|
||||||
element._scrollY = state._scrollY or element._scrollY or 0
|
element._scrollY = state._scrollY or element._scrollY or 0
|
||||||
|
element._scrollbarDragging = state._scrollbarDragging or false
|
||||||
|
element._hoveredScrollbar = state._hoveredScrollbar
|
||||||
|
element._scrollbarDragOffset = state._scrollbarDragOffset or 0
|
||||||
|
|
||||||
|
-- Bind element to StateManager for interactive states
|
||||||
|
-- Use the same ID for StateManager so state persists across frames
|
||||||
|
element._stateId = props.id
|
||||||
|
|
||||||
|
-- Load interactive state from StateManager
|
||||||
|
local interactiveState = StateManager.getState(props.id)
|
||||||
|
element._scrollbarHoveredVertical = interactiveState.scrollbarHoveredVertical
|
||||||
|
element._scrollbarHoveredHorizontal = interactiveState.scrollbarHoveredHorizontal
|
||||||
|
element._scrollbarDragging = interactiveState.scrollbarDragging
|
||||||
|
element._hoveredScrollbar = interactiveState.hoveredScrollbar
|
||||||
|
element._scrollbarDragOffset = interactiveState.scrollbarDragOffset or 0
|
||||||
|
|
||||||
|
-- Set initial theme state based on StateManager state
|
||||||
|
-- This will be updated in Element:update() but we need an initial value
|
||||||
|
if element.themeComponent then
|
||||||
|
if element.disabled or interactiveState.disabled then
|
||||||
|
element._themeState = "disabled"
|
||||||
|
elseif element.active or interactiveState.active then
|
||||||
|
element._themeState = "active"
|
||||||
|
elseif interactiveState.pressed then
|
||||||
|
element._themeState = "pressed"
|
||||||
|
elseif interactiveState.hover then
|
||||||
|
element._themeState = "hover"
|
||||||
|
else
|
||||||
|
element._themeState = "normal"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Store element in current frame tracking
|
-- Store element in current frame tracking
|
||||||
table.insert(Gui._currentFrameElements, element)
|
table.insert(Gui._currentFrameElements, element)
|
||||||
|
|||||||
@@ -2,22 +2,27 @@
|
|||||||
-- Element Object
|
-- Element Object
|
||||||
-- ====================
|
-- ====================
|
||||||
|
|
||||||
|
-- Setup module path for relative requires
|
||||||
|
local modulePath = (...):match("(.-)[^%.]+$")
|
||||||
|
local function req(name)
|
||||||
|
return require(modulePath .. name)
|
||||||
|
end
|
||||||
|
|
||||||
-- Module dependencies
|
-- Module dependencies
|
||||||
local GuiState = require("GuiState")
|
local GuiState = req("GuiState")
|
||||||
local Theme = require("Theme")
|
local Theme = req("Theme")
|
||||||
local Color = require("Color")
|
local Color = req("Color")
|
||||||
local Units = require("Units")
|
local Units = req("Units")
|
||||||
local Blur = require("Blur")
|
local Blur = req("Blur")
|
||||||
local ImageRenderer = require("ImageRenderer")
|
local ImageRenderer = req("ImageRenderer")
|
||||||
local NineSlice = require("NineSlice")
|
local NineSlice = req("NineSlice")
|
||||||
local RoundedRect = require("RoundedRect")
|
local RoundedRect = req("RoundedRect")
|
||||||
--local Animation = require("Animation")
|
--local Animation = req("Animation")
|
||||||
local ImageCache = require("ImageCache")
|
local ImageCache = req("ImageCache")
|
||||||
local utils = require("utils")
|
local utils = req("utils")
|
||||||
local Grid = require("Grid")
|
local Grid = req("Grid")
|
||||||
local InputEvent = require("InputEvent")
|
local InputEvent = req("InputEvent")
|
||||||
local StateManager = require("StateManager")
|
local ImmediateModeState = req("ImmediateModeState")
|
||||||
local StateManager = req("StateManager")
|
|
||||||
local StateManager = req("StateManager")
|
local StateManager = req("StateManager")
|
||||||
|
|
||||||
-- Extract utilities
|
-- Extract utilities
|
||||||
@@ -204,8 +209,11 @@ function Element.new(props)
|
|||||||
self._lastMouseX = {} -- Track last mouse X position per button
|
self._lastMouseX = {} -- Track last mouse X position per button
|
||||||
self._lastMouseY = {} -- Track last mouse Y position per button
|
self._lastMouseY = {} -- Track last mouse Y position per button
|
||||||
|
|
||||||
-- Initialize theme
|
-- Initialize theme state (will be managed by StateManager in immediate mode)
|
||||||
self._themeState = "normal"
|
self._themeState = "normal"
|
||||||
|
|
||||||
|
-- Initialize state manager ID for immediate mode
|
||||||
|
self._stateId = nil -- Will be set during GUI initialization if in immediate mode
|
||||||
|
|
||||||
-- Handle theme property:
|
-- Handle theme property:
|
||||||
-- - theme: which theme to use (defaults to Gui.defaultTheme if not specified)
|
-- - theme: which theme to use (defaults to Gui.defaultTheme if not specified)
|
||||||
@@ -1513,6 +1521,15 @@ function Element:_handleScrollbarPress(mouseX, mouseY, button)
|
|||||||
local thumbX = trackX + dims.horizontal.thumbX
|
local thumbX = trackX + dims.horizontal.thumbX
|
||||||
self._scrollbarDragOffset = mouseX - thumbX
|
self._scrollbarDragOffset = mouseX - thumbX
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Update StateManager if in immediate mode
|
||||||
|
if self._stateId and Gui._immediateMode then
|
||||||
|
StateManager.updateState(self._stateId, {
|
||||||
|
scrollbarDragging = self._scrollbarDragging,
|
||||||
|
hoveredScrollbar = self._hoveredScrollbar,
|
||||||
|
scrollbarDragOffset = self._scrollbarDragOffset,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
return true -- Event consumed
|
return true -- Event consumed
|
||||||
elseif scrollbar.region == "track" then
|
elseif scrollbar.region == "track" then
|
||||||
@@ -1582,6 +1599,14 @@ function Element:_handleScrollbarRelease(button)
|
|||||||
|
|
||||||
if self._scrollbarDragging then
|
if self._scrollbarDragging then
|
||||||
self._scrollbarDragging = false
|
self._scrollbarDragging = false
|
||||||
|
|
||||||
|
-- Update StateManager if in immediate mode
|
||||||
|
if self._stateId and Gui._immediateMode then
|
||||||
|
StateManager.updateState(self._stateId, {
|
||||||
|
scrollbarDragging = false,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2814,6 +2839,16 @@ function Element:update(dt)
|
|||||||
if not scrollbar and not self._scrollbarDragging then
|
if not scrollbar and not self._scrollbarDragging then
|
||||||
self._hoveredScrollbar = nil
|
self._hoveredScrollbar = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Update scrollbar state in StateManager if in immediate mode
|
||||||
|
if self._stateId and Gui._immediateMode then
|
||||||
|
StateManager.updateState(self._stateId, {
|
||||||
|
scrollbarHoveredVertical = self._scrollbarHoveredVertical,
|
||||||
|
scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal,
|
||||||
|
scrollbarDragging = self._scrollbarDragging,
|
||||||
|
hoveredScrollbar = self._hoveredScrollbar,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
-- Handle scrollbar dragging
|
-- Handle scrollbar dragging
|
||||||
if self._scrollbarDragging and love.mouse.isDown(1) then
|
if self._scrollbarDragging and love.mouse.isDown(1) then
|
||||||
@@ -2821,6 +2856,13 @@ function Element:update(dt)
|
|||||||
elseif self._scrollbarDragging then
|
elseif self._scrollbarDragging then
|
||||||
-- Mouse button released
|
-- Mouse button released
|
||||||
self._scrollbarDragging = false
|
self._scrollbarDragging = false
|
||||||
|
|
||||||
|
-- Update StateManager if in immediate mode
|
||||||
|
if self._stateId and Gui._immediateMode then
|
||||||
|
StateManager.updateState(self._stateId, {
|
||||||
|
scrollbarDragging = false,
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle scrollbar click/press (independent of callback)
|
-- Handle scrollbar click/press (independent of callback)
|
||||||
@@ -2884,12 +2926,14 @@ function Element:update(dt)
|
|||||||
|
|
||||||
-- Update theme state based on interaction
|
-- Update theme state based on interaction
|
||||||
if self.themeComponent then
|
if self.themeComponent then
|
||||||
|
local newThemeState = "normal"
|
||||||
|
|
||||||
-- Disabled state takes priority
|
-- Disabled state takes priority
|
||||||
if self.disabled then
|
if self.disabled then
|
||||||
self._themeState = "disabled"
|
newThemeState = "disabled"
|
||||||
-- Active state (for inputs when focused/typing)
|
-- Active state (for inputs when focused/typing)
|
||||||
elseif self.active then
|
elseif self.active then
|
||||||
self._themeState = "active"
|
newThemeState = "active"
|
||||||
-- Only show hover/pressed states if this element is active (not blocked)
|
-- Only show hover/pressed states if this element is active (not blocked)
|
||||||
elseif isHovering and isActiveElement then
|
elseif isHovering and isActiveElement then
|
||||||
-- Check if any button is pressed
|
-- Check if any button is pressed
|
||||||
@@ -2902,13 +2946,30 @@ function Element:update(dt)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if anyPressed then
|
if anyPressed then
|
||||||
self._themeState = "pressed"
|
newThemeState = "pressed"
|
||||||
else
|
else
|
||||||
self._themeState = "hover"
|
newThemeState = "hover"
|
||||||
end
|
end
|
||||||
else
|
|
||||||
self._themeState = "normal"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Update state (in StateManager if in immediate mode, otherwise locally)
|
||||||
|
if self._stateId and Gui._immediateMode then
|
||||||
|
-- Update in StateManager for immediate mode
|
||||||
|
local hover = newThemeState == "hover"
|
||||||
|
local pressed = newThemeState == "pressed"
|
||||||
|
local focused = newThemeState == "active" or self._focused
|
||||||
|
|
||||||
|
StateManager.updateState(self._stateId, {
|
||||||
|
hover = hover,
|
||||||
|
pressed = pressed,
|
||||||
|
focused = focused,
|
||||||
|
disabled = self.disabled,
|
||||||
|
active = self.active,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Always update local state for backward compatibility
|
||||||
|
self._themeState = newThemeState
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Only process button events if callback exists, element is not disabled,
|
-- Only process button events if callback exists, element is not disabled,
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ local function hashProps(props, visited, depth)
|
|||||||
return table.concat(parts, ";")
|
return table.concat(parts, ";")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Counter to track multiple elements created at the same source location (e.g., in loops)
|
||||||
|
local callSiteCounters = {} -- {source_line -> counter}
|
||||||
|
|
||||||
--- Generate a unique ID from call site and properties
|
--- Generate a unique ID from call site and properties
|
||||||
---@param props table|nil Optional properties to include in ID generation
|
---@param props table|nil Optional properties to include in ID generation
|
||||||
---@return string
|
---@return string
|
||||||
@@ -102,9 +105,20 @@ function ImmediateModeState.generateID(props)
|
|||||||
-- Create ID from source file and line number
|
-- Create ID from source file and line number
|
||||||
local baseID = source:match("([^/\\]+)$") or source -- Get filename
|
local baseID = source:match("([^/\\]+)$") or source -- Get filename
|
||||||
baseID = baseID:gsub("%.lua$", "") -- Remove .lua extension
|
baseID = baseID:gsub("%.lua$", "") -- Remove .lua extension
|
||||||
baseID = baseID .. "_L" .. line
|
local locationKey = baseID .. "_L" .. line
|
||||||
|
|
||||||
|
-- Track how many elements have been created at this location
|
||||||
|
callSiteCounters[locationKey] = (callSiteCounters[locationKey] or 0) + 1
|
||||||
|
local instanceNum = callSiteCounters[locationKey]
|
||||||
|
|
||||||
|
baseID = locationKey
|
||||||
|
|
||||||
|
-- Add instance number if multiple elements created at same location (e.g., in loops)
|
||||||
|
if instanceNum > 1 then
|
||||||
|
baseID = baseID .. "_" .. instanceNum
|
||||||
|
end
|
||||||
|
|
||||||
-- Add property hash if provided
|
-- Add property hash if provided (for additional differentiation)
|
||||||
if props then
|
if props then
|
||||||
local propHash = hashProps(props)
|
local propHash = hashProps(props)
|
||||||
if propHash ~= "" then
|
if propHash ~= "" then
|
||||||
@@ -197,6 +211,8 @@ end
|
|||||||
--- Increment frame counter (called at frame start)
|
--- Increment frame counter (called at frame start)
|
||||||
function ImmediateModeState.incrementFrame()
|
function ImmediateModeState.incrementFrame()
|
||||||
frameNumber = frameNumber + 1
|
frameNumber = frameNumber + 1
|
||||||
|
-- Reset call site counters for new frame
|
||||||
|
callSiteCounters = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Get current frame number
|
--- Get current frame number
|
||||||
@@ -320,6 +336,7 @@ function ImmediateModeState.reset()
|
|||||||
stateStore = {}
|
stateStore = {}
|
||||||
stateMetadata = {}
|
stateMetadata = {}
|
||||||
frameNumber = 0
|
frameNumber = 0
|
||||||
|
callSiteCounters = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
return ImmediateModeState
|
return ImmediateModeState
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ function StateManager.getState(id)
|
|||||||
focused = false,
|
focused = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
active = false,
|
active = false,
|
||||||
|
-- Scrollbar-specific states
|
||||||
|
scrollbarHoveredVertical = false,
|
||||||
|
scrollbarHoveredHorizontal = false,
|
||||||
|
scrollbarDragging = false,
|
||||||
|
hoveredScrollbar = nil, -- "vertical" or "horizontal"
|
||||||
|
scrollbarDragOffset = 0,
|
||||||
|
-- Frame tracking
|
||||||
lastHoverFrame = 0,
|
lastHoverFrame = 0,
|
||||||
lastPressedFrame = 0,
|
lastPressedFrame = 0,
|
||||||
lastFocusFrame = 0,
|
lastFocusFrame = 0,
|
||||||
@@ -244,6 +251,11 @@ function StateManager.getActiveState(id)
|
|||||||
focused = state.focused,
|
focused = state.focused,
|
||||||
disabled = state.disabled,
|
disabled = state.disabled,
|
||||||
active = state.active,
|
active = state.active,
|
||||||
|
scrollbarHoveredVertical = state.scrollbarHoveredVertical,
|
||||||
|
scrollbarHoveredHorizontal = state.scrollbarHoveredHorizontal,
|
||||||
|
scrollbarDragging = state.scrollbarDragging,
|
||||||
|
hoveredScrollbar = state.hoveredScrollbar,
|
||||||
|
scrollbarDragOffset = state.scrollbarDragOffset,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
332
testing/__tests__/32_state_manager_tests.lua
Normal file
332
testing/__tests__/32_state_manager_tests.lua
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
-- ====================
|
||||||
|
-- StateManager Module Tests
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
local luaunit = require("testing.luaunit")
|
||||||
|
require("testing.loveStub") -- Required to mock LOVE functions
|
||||||
|
local StateManager = require("modules.StateManager")
|
||||||
|
|
||||||
|
TestStateManager = {}
|
||||||
|
|
||||||
|
function TestStateManager:setUp()
|
||||||
|
-- Reset StateManager before each test
|
||||||
|
StateManager.clearAllStates()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:tearDown()
|
||||||
|
-- Clean up after each test
|
||||||
|
StateManager.clearAllStates()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Basic State Operations
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
function TestStateManager:test_getState_createsNewState()
|
||||||
|
local state = StateManager.getState("test-element")
|
||||||
|
|
||||||
|
luaunit.assertNotNil(state)
|
||||||
|
luaunit.assertEquals(state.hover, false)
|
||||||
|
luaunit.assertEquals(state.pressed, false)
|
||||||
|
luaunit.assertEquals(state.focused, false)
|
||||||
|
luaunit.assertEquals(state.disabled, false)
|
||||||
|
luaunit.assertEquals(state.active, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_getState_returnsExistingState()
|
||||||
|
local state1 = StateManager.getState("test-element")
|
||||||
|
state1.hover = true
|
||||||
|
|
||||||
|
local state2 = StateManager.getState("test-element")
|
||||||
|
|
||||||
|
luaunit.assertEquals(state2.hover, true)
|
||||||
|
luaunit.assertTrue(state1 == state2) -- Same reference
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_updateState_modifiesState()
|
||||||
|
StateManager.updateState("test-element", {
|
||||||
|
hover = true,
|
||||||
|
pressed = false,
|
||||||
|
})
|
||||||
|
|
||||||
|
local state = StateManager.getState("test-element")
|
||||||
|
luaunit.assertEquals(state.hover, true)
|
||||||
|
luaunit.assertEquals(state.pressed, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_updateState_mergesPartialState()
|
||||||
|
StateManager.updateState("test-element", { hover = true })
|
||||||
|
StateManager.updateState("test-element", { pressed = true })
|
||||||
|
|
||||||
|
local state = StateManager.getState("test-element")
|
||||||
|
luaunit.assertEquals(state.hover, true)
|
||||||
|
luaunit.assertEquals(state.pressed, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_clearState_removesState()
|
||||||
|
StateManager.updateState("test-element", { hover = true })
|
||||||
|
StateManager.clearState("test-element")
|
||||||
|
|
||||||
|
local state = StateManager.getState("test-element")
|
||||||
|
luaunit.assertEquals(state.hover, false) -- New state created with defaults
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Scrollbar State Tests
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
function TestStateManager:test_scrollbarStates_initialization()
|
||||||
|
local state = StateManager.getState("test-element")
|
||||||
|
|
||||||
|
luaunit.assertEquals(state.scrollbarHoveredVertical, false)
|
||||||
|
luaunit.assertEquals(state.scrollbarHoveredHorizontal, false)
|
||||||
|
luaunit.assertEquals(state.scrollbarDragging, false)
|
||||||
|
luaunit.assertNil(state.hoveredScrollbar)
|
||||||
|
luaunit.assertEquals(state.scrollbarDragOffset, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_scrollbarStates_updates()
|
||||||
|
StateManager.updateState("test-element", {
|
||||||
|
scrollbarHoveredVertical = true,
|
||||||
|
scrollbarDragging = true,
|
||||||
|
hoveredScrollbar = "vertical",
|
||||||
|
scrollbarDragOffset = 25,
|
||||||
|
})
|
||||||
|
|
||||||
|
local state = StateManager.getState("test-element")
|
||||||
|
luaunit.assertEquals(state.scrollbarHoveredVertical, true)
|
||||||
|
luaunit.assertEquals(state.scrollbarDragging, true)
|
||||||
|
luaunit.assertEquals(state.hoveredScrollbar, "vertical")
|
||||||
|
luaunit.assertEquals(state.scrollbarDragOffset, 25)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Frame Management Tests
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
function TestStateManager:test_frameNumber_increments()
|
||||||
|
local frame1 = StateManager.getFrameNumber()
|
||||||
|
StateManager.incrementFrame()
|
||||||
|
local frame2 = StateManager.getFrameNumber()
|
||||||
|
|
||||||
|
luaunit.assertEquals(frame2, frame1 + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_updateState_updatesFrameNumber()
|
||||||
|
StateManager.incrementFrame()
|
||||||
|
StateManager.incrementFrame()
|
||||||
|
local currentFrame = StateManager.getFrameNumber()
|
||||||
|
|
||||||
|
StateManager.updateState("test-element", { hover = true })
|
||||||
|
|
||||||
|
local state = StateManager.getState("test-element")
|
||||||
|
luaunit.assertEquals(state.lastUpdateFrame, currentFrame)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Cleanup Tests
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
function TestStateManager:test_cleanup_removesStaleStates()
|
||||||
|
-- Configure short retention
|
||||||
|
StateManager.configure({ stateRetentionFrames = 5 })
|
||||||
|
|
||||||
|
-- Create state
|
||||||
|
StateManager.updateState("test-element", { hover = true })
|
||||||
|
|
||||||
|
-- Advance frames beyond retention
|
||||||
|
for i = 1, 10 do
|
||||||
|
StateManager.incrementFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cleanup should remove the state
|
||||||
|
local cleanedCount = StateManager.cleanup()
|
||||||
|
luaunit.assertEquals(cleanedCount, 1)
|
||||||
|
|
||||||
|
-- Reset config
|
||||||
|
StateManager.configure({ stateRetentionFrames = 60 })
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_cleanup_keepsActiveStates()
|
||||||
|
StateManager.configure({ stateRetentionFrames = 5 })
|
||||||
|
|
||||||
|
StateManager.updateState("test-element", { hover = true })
|
||||||
|
|
||||||
|
-- Update state within retention period
|
||||||
|
for i = 1, 3 do
|
||||||
|
StateManager.incrementFrame()
|
||||||
|
StateManager.updateState("test-element", { hover = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
local cleanedCount = StateManager.cleanup()
|
||||||
|
luaunit.assertEquals(cleanedCount, 0) -- Should not clean active state
|
||||||
|
|
||||||
|
StateManager.configure({ stateRetentionFrames = 60 })
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_forceCleanupIfNeeded_activatesWhenOverLimit()
|
||||||
|
StateManager.configure({ maxStateEntries = 5 })
|
||||||
|
|
||||||
|
-- Create more states than limit
|
||||||
|
for i = 1, 10 do
|
||||||
|
StateManager.updateState("element-" .. i, { hover = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Advance frames
|
||||||
|
for i = 1, 15 do
|
||||||
|
StateManager.incrementFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
local cleanedCount = StateManager.forceCleanupIfNeeded()
|
||||||
|
luaunit.assertTrue(cleanedCount > 0)
|
||||||
|
|
||||||
|
StateManager.configure({ maxStateEntries = 1000 })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- State Count Tests
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
function TestStateManager:test_getStateCount_returnsCorrectCount()
|
||||||
|
luaunit.assertEquals(StateManager.getStateCount(), 0)
|
||||||
|
|
||||||
|
StateManager.getState("element-1")
|
||||||
|
StateManager.getState("element-2")
|
||||||
|
StateManager.getState("element-3")
|
||||||
|
|
||||||
|
luaunit.assertEquals(StateManager.getStateCount(), 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Active State Tests
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
function TestStateManager:test_getActiveState_returnsOnlyActiveProperties()
|
||||||
|
StateManager.updateState("test-element", {
|
||||||
|
hover = true,
|
||||||
|
pressed = false,
|
||||||
|
focused = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
local activeState = StateManager.getActiveState("test-element")
|
||||||
|
|
||||||
|
luaunit.assertEquals(activeState.hover, true)
|
||||||
|
luaunit.assertEquals(activeState.pressed, false)
|
||||||
|
luaunit.assertEquals(activeState.focused, true)
|
||||||
|
luaunit.assertNil(activeState.lastUpdateFrame) -- Should not include frame tracking
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Helper Function Tests
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
function TestStateManager:test_isHovered_returnsTrueWhenHovered()
|
||||||
|
StateManager.updateState("test-element", { hover = true })
|
||||||
|
luaunit.assertTrue(StateManager.isHovered("test-element"))
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_isHovered_returnsFalseWhenNotHovered()
|
||||||
|
StateManager.updateState("test-element", { hover = false })
|
||||||
|
luaunit.assertFalse(StateManager.isHovered("test-element"))
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_isPressed_returnsTrueWhenPressed()
|
||||||
|
StateManager.updateState("test-element", { pressed = true })
|
||||||
|
luaunit.assertTrue(StateManager.isPressed("test-element"))
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_isFocused_returnsTrueWhenFocused()
|
||||||
|
StateManager.updateState("test-element", { focused = true })
|
||||||
|
luaunit.assertTrue(StateManager.isFocused("test-element"))
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_isDisabled_returnsTrueWhenDisabled()
|
||||||
|
StateManager.updateState("test-element", { disabled = true })
|
||||||
|
luaunit.assertTrue(StateManager.isDisabled("test-element"))
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_isActive_returnsTrueWhenActive()
|
||||||
|
StateManager.updateState("test-element", { active = true })
|
||||||
|
luaunit.assertTrue(StateManager.isActive("test-element"))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- State Change Events Tests
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
function TestStateManager:test_subscribe_receivesStateChangeEvents()
|
||||||
|
local callbackInvoked = false
|
||||||
|
local receivedId = nil
|
||||||
|
local receivedProperty = nil
|
||||||
|
local receivedOldValue = nil
|
||||||
|
local receivedNewValue = nil
|
||||||
|
|
||||||
|
local callback = function(id, property, oldValue, newValue)
|
||||||
|
callbackInvoked = true
|
||||||
|
receivedId = id
|
||||||
|
receivedProperty = property
|
||||||
|
receivedOldValue = oldValue
|
||||||
|
receivedNewValue = newValue
|
||||||
|
end
|
||||||
|
|
||||||
|
StateManager.subscribe("test-element", callback)
|
||||||
|
StateManager.updateState("test-element", { hover = true })
|
||||||
|
|
||||||
|
luaunit.assertTrue(callbackInvoked)
|
||||||
|
luaunit.assertEquals(receivedId, "test-element")
|
||||||
|
luaunit.assertEquals(receivedProperty, "hover")
|
||||||
|
luaunit.assertEquals(receivedOldValue, false)
|
||||||
|
luaunit.assertEquals(receivedNewValue, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_subscribe_multipleListeners()
|
||||||
|
local callback1Invoked = false
|
||||||
|
local callback2Invoked = false
|
||||||
|
|
||||||
|
StateManager.subscribe("test-element", function() callback1Invoked = true end)
|
||||||
|
StateManager.subscribe("test-element", function() callback2Invoked = true end)
|
||||||
|
|
||||||
|
StateManager.updateState("test-element", { hover = true })
|
||||||
|
|
||||||
|
luaunit.assertTrue(callback1Invoked)
|
||||||
|
luaunit.assertTrue(callback2Invoked)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStateManager:test_unsubscribe_removesListener()
|
||||||
|
local callbackInvoked = false
|
||||||
|
local callback = function() callbackInvoked = true end
|
||||||
|
|
||||||
|
StateManager.subscribe("test-element", callback)
|
||||||
|
StateManager.unsubscribe("test-element", callback)
|
||||||
|
|
||||||
|
StateManager.updateState("test-element", { hover = true })
|
||||||
|
|
||||||
|
luaunit.assertFalse(callbackInvoked)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Configuration Tests
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
function TestStateManager:test_configure_updatesSettings()
|
||||||
|
StateManager.configure({
|
||||||
|
stateRetentionFrames = 30,
|
||||||
|
maxStateEntries = 500,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Test that configuration was applied by creating many states
|
||||||
|
-- and checking cleanup behavior (indirect test)
|
||||||
|
for i = 1, 10 do
|
||||||
|
StateManager.updateState("element-" .. i, { hover = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertEquals(StateManager.getStateCount(), 10)
|
||||||
|
|
||||||
|
-- Reset to defaults
|
||||||
|
StateManager.configure({
|
||||||
|
stateRetentionFrames = 60,
|
||||||
|
maxStateEntries = 1000,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return TestStateManager
|
||||||
@@ -35,6 +35,7 @@ local testFiles = {
|
|||||||
"testing/__tests__/29_drag_event_tests.lua",
|
"testing/__tests__/29_drag_event_tests.lua",
|
||||||
"testing/__tests__/30_scrollbar_features_tests.lua",
|
"testing/__tests__/30_scrollbar_features_tests.lua",
|
||||||
"testing/__tests__/31_immediate_mode_basic_tests.lua",
|
"testing/__tests__/31_immediate_mode_basic_tests.lua",
|
||||||
|
"testing/__tests__/32_state_manager_tests.lua",
|
||||||
}
|
}
|
||||||
|
|
||||||
local success = true
|
local success = true
|
||||||
|
|||||||
Reference in New Issue
Block a user