cleaned up rendering mode swapping
This commit is contained in:
19
FlexLove.lua
19
FlexLove.lua
@@ -399,6 +399,7 @@ function flexlove.beginFrame()
|
||||
-- Cleanup elements from PREVIOUS frame (after they've been drawn)
|
||||
-- This breaks circular references and allows GC to collect memory
|
||||
-- Note: Cleanup is minimal to preserve functionality
|
||||
-- IMPORTANT: Only cleanup immediate-mode elements, preserve retained-mode elements
|
||||
if flexlove._currentFrameElements then
|
||||
local function cleanupChildren(elem)
|
||||
for _, child in ipairs(elem.children) do
|
||||
@@ -408,17 +409,31 @@ function flexlove.beginFrame()
|
||||
end
|
||||
|
||||
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||
if not element.parent then
|
||||
-- Only cleanup immediate-mode top-level elements
|
||||
-- Retained-mode elements persist across frames
|
||||
if not element.parent and element._elementMode == "immediate" then
|
||||
cleanupChildren(element)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Preserve top-level retained elements before resetting
|
||||
local retainedTopElements = {}
|
||||
if flexlove.topElements then
|
||||
for _, element in ipairs(flexlove.topElements) do
|
||||
if element._elementMode == "retained" then
|
||||
table.insert(retainedTopElements, element)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
flexlove._frameNumber = flexlove._frameNumber + 1
|
||||
StateManager.incrementFrame()
|
||||
flexlove._currentFrameElements = {}
|
||||
flexlove._frameStarted = true
|
||||
flexlove.topElements = {}
|
||||
|
||||
-- Restore retained top-level elements
|
||||
flexlove.topElements = retainedTopElements
|
||||
|
||||
Context.clearFrameElements()
|
||||
end
|
||||
|
||||
45
README.md
45
README.md
@@ -221,7 +221,7 @@ function love.draw()
|
||||
width = "30vw",
|
||||
height = "40vh",
|
||||
backgroundColor = Color.new(0.2, 0.2, 0.2, 1),
|
||||
-- No ID auto-generation since it's in retained mode
|
||||
-- ID is auto-generated if not provided (works in both modes)
|
||||
})
|
||||
|
||||
FlexLove.endFrame()
|
||||
@@ -229,15 +229,51 @@ end
|
||||
```
|
||||
|
||||
**Key behaviors:**
|
||||
- `mode = "immediate"` - Element uses immediate-mode lifecycle (recreated each frame, auto-generates ID, uses StateManager)
|
||||
- `mode = "retained"` - Element uses retained-mode lifecycle (persists across frames, no auto-ID, no StateManager)
|
||||
- `mode = "immediate"` - Element uses immediate-mode lifecycle (recreated each frame, uses StateManager)
|
||||
- `mode = "retained"` - Element uses retained-mode lifecycle (persists across frames, uses StateManager for mixed-mode children)
|
||||
- `mode = nil` - Element inherits from global mode setting (default behavior)
|
||||
- **ID auto-generation** - All elements receive an auto-generated ID if not explicitly provided, enabling state management features
|
||||
- Mode does NOT inherit from parent to child - each element independently controls its own lifecycle
|
||||
- Common use cases:
|
||||
- Performance-critical static UI in retained mode while using immediate mode globally
|
||||
- Reactive debug overlays in immediate mode within a retained-mode application
|
||||
- Mixed UI where some components are static (menus) and others are dynamic (HUD)
|
||||
|
||||
#### Automatic ID Generation
|
||||
|
||||
All elements automatically receive a unique ID if not explicitly provided. This enables powerful state management features:
|
||||
|
||||
```lua
|
||||
-- ID is automatically generated based on call site
|
||||
local button = FlexLove.new({
|
||||
text = "Click Me",
|
||||
-- id is auto-generated (e.g., "main_L42_child0_123456")
|
||||
})
|
||||
|
||||
-- Or provide a custom ID for explicit control
|
||||
local namedButton = FlexLove.new({
|
||||
id = "submit_button",
|
||||
text = "Submit",
|
||||
})
|
||||
```
|
||||
|
||||
**ID Generation Strategy:**
|
||||
- **Top-level elements**: ID based on source file and line number (e.g., `main_L42`)
|
||||
- **Child elements**: ID based on parent ID + sibling index (e.g., `parent_child0`, `parent_child1`)
|
||||
- **Multiple elements at same location**: Automatically numbered (e.g., `main_L42_1`, `main_L42_2`)
|
||||
- **Property differentiation**: Similar elements get unique hash suffixes based on their properties
|
||||
|
||||
**Benefits of Auto-Generated IDs:**
|
||||
- Enables state persistence in immediate mode (scroll position, input text, animations)
|
||||
- Allows retained children in immediate parents (mixed-mode trees)
|
||||
- Supports state management features without manual ID tracking
|
||||
- Stable across frames when element structure is consistent
|
||||
|
||||
**When to Use Custom IDs:**
|
||||
- When you need to reference elements programmatically
|
||||
- For debugging and logging (readable names)
|
||||
- When element creation order might change but identity should remain stable
|
||||
|
||||
### Element Properties
|
||||
|
||||
Common properties for all elements:
|
||||
@@ -303,6 +339,9 @@ Common properties for all elements:
|
||||
-- Rendering Mode
|
||||
mode = nil, -- "immediate", "retained", or nil (uses global setting)
|
||||
|
||||
-- Element Identity
|
||||
id = nil, -- Element ID (auto-generated if not provided)
|
||||
|
||||
-- Hierarchy
|
||||
parent = nil, -- Parent element
|
||||
}
|
||||
|
||||
219
examples/mixed_mode_demo.lua
Normal file
219
examples/mixed_mode_demo.lua
Normal file
@@ -0,0 +1,219 @@
|
||||
-- Demo: Retained children with immediate parents
|
||||
-- Shows how retained-mode children persist when immediate-mode parents recreate each frame
|
||||
|
||||
local FlexLove = require("FlexLove")
|
||||
|
||||
-- Track frame count for demo
|
||||
local frameCount = 0
|
||||
|
||||
-- Retained button state (will persist across frames)
|
||||
local buttonClicks = 0
|
||||
local topLevelButtonClicks = 0
|
||||
|
||||
function love.load()
|
||||
love.window.setTitle("Mixed-Mode Demo: Retained Children + Immediate Parents")
|
||||
love.window.setMode(800, 600)
|
||||
|
||||
FlexLove.init({
|
||||
immediateMode = true,
|
||||
performanceMonitoring = true,
|
||||
})
|
||||
end
|
||||
|
||||
function love.update(dt)
|
||||
FlexLove.update(dt)
|
||||
end
|
||||
|
||||
function love.draw()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
-- Frame counter (immediate mode - recreates each frame)
|
||||
local header = FlexLove.new({
|
||||
width = 800,
|
||||
height = 60,
|
||||
backgroundColor = { 0.1, 0.1, 0.15, 1 },
|
||||
padding = 20,
|
||||
flexDirection = "horizontal",
|
||||
justifyContent = "space-between",
|
||||
alignItems = "center",
|
||||
})
|
||||
|
||||
FlexLove.new({
|
||||
parent = header,
|
||||
text = "Frame: " .. frameCount,
|
||||
textColor = { 1, 1, 1, 1 },
|
||||
textSize = 20,
|
||||
})
|
||||
|
||||
FlexLove.new({
|
||||
parent = header,
|
||||
text = "Mixed-Mode Element Tree Demo",
|
||||
textColor = { 0.8, 0.9, 1, 1 },
|
||||
textSize = 24,
|
||||
})
|
||||
|
||||
-- Main content area (immediate parent)
|
||||
local container = FlexLove.new({
|
||||
id = "main_container",
|
||||
y = 60,
|
||||
width = 800,
|
||||
height = 540,
|
||||
padding = 30,
|
||||
gap = 20,
|
||||
backgroundColor = { 0.05, 0.05, 0.08, 1 },
|
||||
flexDirection = "vertical",
|
||||
})
|
||||
|
||||
-- Section 1: Retained button in immediate parent
|
||||
FlexLove.new({
|
||||
parent = container,
|
||||
text = "1. Retained Button (persists across frames)",
|
||||
textColor = { 0.9, 0.9, 0.9, 1 },
|
||||
textSize = 18,
|
||||
})
|
||||
|
||||
local retainedButton = FlexLove.new({
|
||||
id = "retained_button",
|
||||
mode = "retained", -- This button will persist!
|
||||
parent = container,
|
||||
width = 300,
|
||||
height = 50,
|
||||
backgroundColor = { 0.2, 0.6, 0.9, 1 },
|
||||
cornerRadius = 8,
|
||||
text = "Clicks: " .. buttonClicks,
|
||||
textColor = { 1, 1, 1, 1 },
|
||||
textSize = 16,
|
||||
textAlign = "center",
|
||||
onEvent = function(element, event)
|
||||
if event.type == "click" then
|
||||
buttonClicks = buttonClicks + 1
|
||||
-- Update button text
|
||||
element.text = "Clicks: " .. buttonClicks
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
FlexLove.new({
|
||||
parent = container,
|
||||
text = "Note: Button state persists even though parent recreates every frame",
|
||||
textColor = { 0.6, 0.6, 0.6, 1 },
|
||||
textSize = 12,
|
||||
})
|
||||
|
||||
-- Section 2: Top-level retained element
|
||||
FlexLove.new({
|
||||
parent = container,
|
||||
text = "2. Top-Level Retained Element (also persists)",
|
||||
textColor = { 0.9, 0.9, 0.9, 1 },
|
||||
textSize = 18,
|
||||
margin = { top = 20 },
|
||||
})
|
||||
|
||||
FlexLove.new({
|
||||
parent = container,
|
||||
text = "Look at the bottom-left corner for a persistent panel",
|
||||
textColor = { 0.6, 0.6, 0.6, 1 },
|
||||
textSize = 12,
|
||||
})
|
||||
|
||||
-- Section 3: Comparison with immediate button
|
||||
FlexLove.new({
|
||||
parent = container,
|
||||
text = "3. Immediate Button (recreates every frame)",
|
||||
textColor = { 0.9, 0.9, 0.9, 1 },
|
||||
textSize = 18,
|
||||
margin = { top = 20 },
|
||||
})
|
||||
|
||||
FlexLove.new({
|
||||
parent = container,
|
||||
width = 300,
|
||||
height = 50,
|
||||
backgroundColor = { 0.9, 0.3, 0.3, 1 },
|
||||
cornerRadius = 8,
|
||||
text = "Can't track clicks (recreated)",
|
||||
textColor = { 1, 1, 1, 1 },
|
||||
textSize = 16,
|
||||
textAlign = "center",
|
||||
onEvent = function(element, event)
|
||||
if event.type == "click" then
|
||||
print("Immediate button clicked (but counter can't persist)")
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
FlexLove.new({
|
||||
parent = container,
|
||||
text = "Note: This button is recreated every frame, so it can't maintain state",
|
||||
textColor = { 0.6, 0.6, 0.6, 1 },
|
||||
textSize = 12,
|
||||
})
|
||||
|
||||
-- Top-level retained element (persists in immediate mode!)
|
||||
local topLevelPanel = FlexLove.new({
|
||||
id = "top_level_panel",
|
||||
mode = "retained",
|
||||
x = 10,
|
||||
y = 500,
|
||||
width = 250,
|
||||
height = 90,
|
||||
backgroundColor = { 0.15, 0.5, 0.3, 1 },
|
||||
cornerRadius = 10,
|
||||
padding = 15,
|
||||
gap = 10,
|
||||
flexDirection = "vertical",
|
||||
})
|
||||
|
||||
FlexLove.new({
|
||||
id = "panel_title",
|
||||
mode = "retained",
|
||||
parent = topLevelPanel,
|
||||
text = "Persistent Panel",
|
||||
textColor = { 1, 1, 1, 1 },
|
||||
textSize = 16,
|
||||
})
|
||||
|
||||
FlexLove.new({
|
||||
id = "panel_button",
|
||||
mode = "retained",
|
||||
parent = topLevelPanel,
|
||||
width = 220,
|
||||
height = 35,
|
||||
backgroundColor = { 1, 1, 1, 0.9 },
|
||||
cornerRadius = 5,
|
||||
text = "Panel Clicks: " .. topLevelButtonClicks,
|
||||
textColor = { 0.15, 0.5, 0.3, 1 },
|
||||
textSize = 14,
|
||||
textAlign = "center",
|
||||
onEvent = function(element, event)
|
||||
if event.type == "click" then
|
||||
topLevelButtonClicks = topLevelButtonClicks + 1
|
||||
element.text = "Panel Clicks: " .. topLevelButtonClicks
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Increment frame counter AFTER drawing
|
||||
frameCount = frameCount + 1
|
||||
|
||||
-- Draw all UI elements
|
||||
FlexLove.draw()
|
||||
end
|
||||
|
||||
function love.mousepressed(x, y, button)
|
||||
FlexLove.mousepressed(x, y, button)
|
||||
end
|
||||
|
||||
function love.mousereleased(x, y, button)
|
||||
FlexLove.mousereleased(x, y, button)
|
||||
end
|
||||
|
||||
function love.mousemoved(x, y, dx, dy)
|
||||
FlexLove.mousemoved(x, y, dx, dy)
|
||||
end
|
||||
|
||||
function love.wheelmoved(x, y)
|
||||
FlexLove.wheelmoved(x, y)
|
||||
end
|
||||
@@ -38,8 +38,19 @@ function Context.registerElement(element)
|
||||
end
|
||||
|
||||
function Context.clearFrameElements()
|
||||
-- Preserve retained-mode elements
|
||||
if Context._immediateMode then
|
||||
local retainedElements = {}
|
||||
for _, element in ipairs(Context._zIndexOrderedElements) do
|
||||
if element._elementMode == "retained" then
|
||||
table.insert(retainedElements, element)
|
||||
end
|
||||
end
|
||||
Context._zIndexOrderedElements = retainedElements
|
||||
else
|
||||
Context._zIndexOrderedElements = {}
|
||||
end
|
||||
end
|
||||
|
||||
--- Sort elements by z-index (called after all elements are registered)
|
||||
function Context.sortElementsByZIndex()
|
||||
|
||||
@@ -188,6 +188,24 @@ end
|
||||
---@param props ElementProps
|
||||
---@return Element
|
||||
function Element.new(props)
|
||||
-- Early check: If this is a retained-mode element in an immediate-mode context,
|
||||
-- check if it already exists (was restored to parent) to prevent duplicates
|
||||
local elementMode = props.mode
|
||||
if elementMode == nil then
|
||||
elementMode = Element._Context._immediateMode and "immediate" or "retained"
|
||||
end
|
||||
|
||||
-- If retained mode and has an ID, check if element already exists in parent's children
|
||||
if elementMode == "retained" and props.id and props.id ~= "" and props.parent then
|
||||
-- Check if this element already exists in parent's restored children
|
||||
for _, child in ipairs(props.parent.children) do
|
||||
if child.id == props.id and child._elementMode == "retained" then
|
||||
-- Element already exists (was restored), return existing instance
|
||||
return child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local self = setmetatable({}, Element)
|
||||
|
||||
-- Create dependency subsets for sub-modules (defined once, used throughout)
|
||||
@@ -272,11 +290,56 @@ function Element.new(props)
|
||||
self.children = {}
|
||||
self.onEvent = props.onEvent
|
||||
|
||||
-- Auto-generate ID in immediate mode if not provided
|
||||
if self._elementMode == "immediate" and (not props.id or props.id == "") then
|
||||
-- Track whether ID was auto-generated (before ID assignment)
|
||||
local idWasAutoGenerated = not props.id or props.id == ""
|
||||
|
||||
-- Auto-generate ID if not provided (for all elements)
|
||||
if idWasAutoGenerated then
|
||||
self.id = Element._StateManager.generateID(props, props.parent)
|
||||
else
|
||||
self.id = props.id or ""
|
||||
self.id = props.id
|
||||
end
|
||||
|
||||
-- AFTER ID is determined, check for duplicate top-level OR child retained elements
|
||||
-- ONLY for auto-generated IDs (same call site recreating same element)
|
||||
-- If user provides explicit ID, they control uniqueness
|
||||
if self._elementMode == "retained" and idWasAutoGenerated and self.id and self.id ~= "" then
|
||||
if not props.parent then
|
||||
-- Top-level element: check in topElements
|
||||
for _, existingElement in ipairs(Element._Context.topElements) do
|
||||
if existingElement.id == self.id and existingElement._elementMode == "retained" then
|
||||
-- Element already exists (was preserved from previous frame), return existing instance
|
||||
-- CRITICAL: Clear children array to prevent accumulation
|
||||
-- Children will be re-declared this frame (retained children will be found via duplicate check)
|
||||
existingElement.children = {}
|
||||
return existingElement
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Child element: check in parent's children
|
||||
for _, existingChild in ipairs(props.parent.children) do
|
||||
if existingChild.id == self.id and existingChild._elementMode == "retained" then
|
||||
-- Element already exists (was restored to parent), return existing instance
|
||||
-- CRITICAL: Clear children array to prevent accumulation
|
||||
-- Children will be re-declared this frame (retained children will be found via duplicate check)
|
||||
existingChild.children = {}
|
||||
return existingChild
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- In immediate mode, restore retained children from StateManager
|
||||
-- This allows retained-mode children to persist when immediate-mode parents recreate
|
||||
if self._elementMode == "immediate" and self.id and self.id ~= "" then
|
||||
local retainedChildren = Element._StateManager.getRetainedChildren(self.id)
|
||||
if retainedChildren and #retainedChildren > 0 then
|
||||
-- Restore retained children and update their parent references
|
||||
for _, child in ipairs(retainedChildren) do
|
||||
child.parent = self
|
||||
table.insert(self.children, child)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.userdata = props.userdata
|
||||
@@ -2237,6 +2300,11 @@ function Element:destroy()
|
||||
-- Clear children table
|
||||
self.children = {}
|
||||
|
||||
-- Clear retained children from StateManager (if this is an immediate-mode element)
|
||||
if self._elementMode == "immediate" and self.id and self.id ~= "" then
|
||||
Element._StateManager.clearRetainedChildren(self.id)
|
||||
end
|
||||
|
||||
-- Clear parent reference
|
||||
if self.parent then
|
||||
self.parent = nil
|
||||
@@ -3503,6 +3571,12 @@ function Element:saveState()
|
||||
state._textDragOccurred = self._textDragOccurred
|
||||
end
|
||||
|
||||
-- Save retained children references (for mixed-mode trees)
|
||||
-- Only save if this is an immediate-mode element with retained children
|
||||
if self._elementMode == "immediate" and #self.children > 0 then
|
||||
Element._StateManager.saveRetainedChildren(self.id, self.children)
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
|
||||
@@ -64,6 +64,9 @@ local stateDefaults = {
|
||||
_cursorVisible = true,
|
||||
_cursorBlinkPaused = false,
|
||||
_cursorBlinkPauseTimer = 0,
|
||||
|
||||
-- Retained children references (for mixed-mode trees)
|
||||
retainedChildren = nil,
|
||||
}
|
||||
|
||||
--- Check if a value equals the default for a key
|
||||
@@ -208,16 +211,24 @@ function StateManager.generateID(props, parent)
|
||||
|
||||
-- If we have a parent, use tree-based ID generation for stability
|
||||
if parent and parent.id and parent.id ~= "" then
|
||||
-- Count how many children the parent currently has
|
||||
-- This gives us a stable sibling index
|
||||
local siblingIndex = #(parent.children or {})
|
||||
-- For child elements, use call-site (file + line) like top-level elements
|
||||
-- This ensures the same call site always generates the same ID, even when
|
||||
-- retained children persist in parent.children array
|
||||
local baseID = parent.id .. "_" .. locationKey
|
||||
|
||||
-- Generate ID based on parent ID + sibling position (NO line number for stability)
|
||||
-- This ensures the same position in the tree always gets the same ID
|
||||
local baseID = parent.id .. "_child" .. siblingIndex
|
||||
-- Count how many children have been created at THIS call site
|
||||
local callSiteKey = parent.id .. "_" .. locationKey
|
||||
callSiteCounters[callSiteKey] = (callSiteCounters[callSiteKey] or 0) + 1
|
||||
local instanceNum = callSiteCounters[callSiteKey]
|
||||
|
||||
-- Add property hash if provided (for additional differentiation at same position)
|
||||
if props then
|
||||
if instanceNum > 1 then
|
||||
baseID = baseID .. "_" .. instanceNum
|
||||
end
|
||||
|
||||
-- Add property hash if provided (for additional differentiation)
|
||||
-- IMPORTANT: Skip property hash for retained-mode elements to ensure ID stability
|
||||
-- Retained elements should persist across frames even if props change slightly
|
||||
if props and props.mode ~= "retained" then
|
||||
local propHash = hashProps(props)
|
||||
if propHash ~= "" then
|
||||
-- Use first 8 chars of a simple hash
|
||||
@@ -245,7 +256,9 @@ function StateManager.generateID(props, parent)
|
||||
end
|
||||
|
||||
-- Add property hash if provided (for additional differentiation)
|
||||
if props then
|
||||
-- IMPORTANT: Skip property hash for retained-mode elements to ensure ID stability
|
||||
-- Retained elements should persist across frames even if props change slightly
|
||||
if props and props.mode ~= "retained" then
|
||||
local propHash = hashProps(props)
|
||||
if propHash ~= "" then
|
||||
-- Use first 8 chars of a simple hash
|
||||
@@ -655,4 +668,72 @@ function StateManager.isActive(id)
|
||||
return state.active or false
|
||||
end
|
||||
|
||||
-- ====================
|
||||
-- Retained Children Management (for mixed-mode trees)
|
||||
-- ====================
|
||||
|
||||
--- Save retained children for an element
|
||||
--- Only stores children that are in retained mode
|
||||
---@param id string Parent element ID
|
||||
---@param children table Array of child elements
|
||||
function StateManager.saveRetainedChildren(id, children)
|
||||
if not id or not children then
|
||||
return
|
||||
end
|
||||
|
||||
-- Filter to only retained-mode children
|
||||
local retainedChildren = {}
|
||||
for _, child in ipairs(children) do
|
||||
if child._elementMode == "retained" then
|
||||
table.insert(retainedChildren, child)
|
||||
end
|
||||
end
|
||||
|
||||
-- Only save if we have retained children
|
||||
if #retainedChildren > 0 then
|
||||
local state = StateManager.getState(id)
|
||||
state.retainedChildren = retainedChildren
|
||||
end
|
||||
end
|
||||
|
||||
--- Get retained children for an element
|
||||
--- Returns an array of retained-mode child elements
|
||||
---@param id string Parent element ID
|
||||
---@return table children Array of retained child elements (empty if none)
|
||||
function StateManager.getRetainedChildren(id)
|
||||
if not id then
|
||||
return {}
|
||||
end
|
||||
|
||||
local state = StateManager.getCurrentState(id)
|
||||
if state.retainedChildren then
|
||||
-- Verify children still exist (weren't destroyed)
|
||||
local validChildren = {}
|
||||
for _, child in ipairs(state.retainedChildren) do
|
||||
-- Children are element objects, check if they're still valid
|
||||
-- A destroyed element would have nil references or be garbage collected
|
||||
if child and type(child) == "table" and child.id then
|
||||
table.insert(validChildren, child)
|
||||
end
|
||||
end
|
||||
return validChildren
|
||||
end
|
||||
|
||||
return {}
|
||||
end
|
||||
|
||||
--- Clear retained children for an element
|
||||
--- Used when parent is destroyed or children are manually removed
|
||||
---@param id string Parent element ID
|
||||
function StateManager.clearRetainedChildren(id)
|
||||
if not id then
|
||||
return
|
||||
end
|
||||
|
||||
local state = StateManager.getCurrentState(id)
|
||||
if state.retainedChildren then
|
||||
state.retainedChildren = nil
|
||||
end
|
||||
end
|
||||
|
||||
return StateManager
|
||||
|
||||
400
testing/__tests__/mixed_mode_children_test.lua
Normal file
400
testing/__tests__/mixed_mode_children_test.lua
Normal file
@@ -0,0 +1,400 @@
|
||||
-- Test retained children persisting when immediate parents recreate
|
||||
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||
|
||||
-- Load love stub before anything else
|
||||
require("testing.loveStub")
|
||||
|
||||
local luaunit = require("testing.luaunit")
|
||||
local FlexLove = require("FlexLove")
|
||||
|
||||
TestMixedModeChildren = {}
|
||||
|
||||
function TestMixedModeChildren:setUp()
|
||||
FlexLove.init({ immediateMode = true })
|
||||
FlexLove.setMode("immediate")
|
||||
end
|
||||
|
||||
function TestMixedModeChildren:tearDown()
|
||||
FlexLove._defaultDependencies.StateManager.reset()
|
||||
FlexLove.topElements = {}
|
||||
FlexLove._currentFrameElements = {}
|
||||
FlexLove._frameStarted = false
|
||||
end
|
||||
|
||||
-- Test 1: Retained child persists when immediate parent recreates
|
||||
function TestMixedModeChildren:testRetainedChildPersistsWithImmediateParent()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
-- Frame 1: Create immediate parent with retained child
|
||||
local parent1 = FlexLove.new({
|
||||
id = "parent",
|
||||
mode = "immediate",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
local retainedChild = FlexLove.new({
|
||||
id = "retained_child",
|
||||
mode = "retained",
|
||||
width = 50,
|
||||
height = 50,
|
||||
})
|
||||
|
||||
parent1:addChild(retainedChild)
|
||||
|
||||
luaunit.assertEquals(#parent1.children, 1, "Parent should have 1 child")
|
||||
luaunit.assertEquals(parent1.children[1], retainedChild, "Child should be the retained element")
|
||||
luaunit.assertEquals(retainedChild.parent, parent1, "Child's parent should be set")
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2: Recreate immediate parent, retained child should persist
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local parent2 = FlexLove.new({
|
||||
id = "parent",
|
||||
mode = "immediate",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
-- The retained child should be automatically restored
|
||||
luaunit.assertEquals(#parent2.children, 1, "Parent should still have 1 child after recreation")
|
||||
luaunit.assertEquals(parent2.children[1], retainedChild, "Child should be the same retained element")
|
||||
luaunit.assertEquals(retainedChild.parent, parent2, "Child's parent reference should be updated")
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test 2: Multiple retained children persist
|
||||
function TestMixedModeChildren:testMultipleRetainedChildrenPersist()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local parent1 = FlexLove.new({
|
||||
id = "parent",
|
||||
mode = "immediate",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
local child1 = FlexLove.new({
|
||||
id = "child1",
|
||||
mode = "retained",
|
||||
width = 50,
|
||||
height = 50,
|
||||
})
|
||||
|
||||
local child2 = FlexLove.new({
|
||||
id = "child2",
|
||||
mode = "retained",
|
||||
width = 50,
|
||||
height = 50,
|
||||
})
|
||||
|
||||
local child3 = FlexLove.new({
|
||||
id = "child3",
|
||||
mode = "retained",
|
||||
width = 50,
|
||||
height = 50,
|
||||
})
|
||||
|
||||
parent1:addChild(child1)
|
||||
parent1:addChild(child2)
|
||||
parent1:addChild(child3)
|
||||
|
||||
luaunit.assertEquals(#parent1.children, 3, "Parent should have 3 children")
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local parent2 = FlexLove.new({
|
||||
id = "parent",
|
||||
mode = "immediate",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
luaunit.assertEquals(#parent2.children, 3, "Parent should still have 3 children")
|
||||
luaunit.assertEquals(parent2.children[1], child1, "First child should persist")
|
||||
luaunit.assertEquals(parent2.children[2], child2, "Second child should persist")
|
||||
luaunit.assertEquals(parent2.children[3], child3, "Third child should persist")
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test 3: Immediate children do NOT persist (only retained children)
|
||||
function TestMixedModeChildren:testImmediateChildrenDoNotPersist()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local parent1 = FlexLove.new({
|
||||
id = "parent",
|
||||
mode = "immediate",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
local immediateChild = FlexLove.new({
|
||||
id = "immediate_child",
|
||||
mode = "immediate",
|
||||
width = 50,
|
||||
height = 50,
|
||||
})
|
||||
|
||||
local retainedChild = FlexLove.new({
|
||||
id = "retained_child",
|
||||
mode = "retained",
|
||||
width = 50,
|
||||
height = 50,
|
||||
})
|
||||
|
||||
parent1:addChild(immediateChild)
|
||||
parent1:addChild(retainedChild)
|
||||
|
||||
luaunit.assertEquals(#parent1.children, 2, "Parent should have 2 children")
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local parent2 = FlexLove.new({
|
||||
id = "parent",
|
||||
mode = "immediate",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
-- Only retained child should persist
|
||||
luaunit.assertEquals(#parent2.children, 1, "Parent should only have 1 child (retained)")
|
||||
luaunit.assertEquals(parent2.children[1], retainedChild, "Only retained child should persist")
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test 4: Top-level retained element persists in immediate mode
|
||||
function TestMixedModeChildren:testTopLevelRetainedElementPersists()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local retainedElement = FlexLove.new({
|
||||
id = "top_retained",
|
||||
mode = "retained",
|
||||
width = 100,
|
||||
height = 100,
|
||||
})
|
||||
|
||||
luaunit.assertEquals(#FlexLove.topElements, 1, "Should have 1 top-level element")
|
||||
luaunit.assertEquals(FlexLove.topElements[1], retainedElement, "Top element should be retained element")
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2
|
||||
FlexLove.beginFrame()
|
||||
|
||||
-- Retained element should still be in topElements
|
||||
luaunit.assertEquals(#FlexLove.topElements, 1, "Retained element should persist in topElements")
|
||||
luaunit.assertEquals(FlexLove.topElements[1], retainedElement, "Should be same retained element")
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test 5: Mixed top-level elements (immediate and retained)
|
||||
function TestMixedModeChildren:testMixedTopLevelElements()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local immediateElement1 = FlexLove.new({
|
||||
id = "immediate1",
|
||||
mode = "immediate",
|
||||
width = 100,
|
||||
height = 100,
|
||||
})
|
||||
|
||||
local retainedElement = FlexLove.new({
|
||||
id = "retained",
|
||||
mode = "retained",
|
||||
width = 100,
|
||||
height = 100,
|
||||
})
|
||||
|
||||
luaunit.assertEquals(#FlexLove.topElements, 2, "Should have 2 top-level elements")
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local immediateElement2 = FlexLove.new({
|
||||
id = "immediate2",
|
||||
mode = "immediate",
|
||||
width = 100,
|
||||
height = 100,
|
||||
})
|
||||
|
||||
-- Should have retained element + new immediate element
|
||||
luaunit.assertEquals(#FlexLove.topElements, 2, "Should have 2 top-level elements")
|
||||
|
||||
-- Find retained element in topElements
|
||||
local foundRetained = false
|
||||
for _, elem in ipairs(FlexLove.topElements) do
|
||||
if elem == retainedElement then
|
||||
foundRetained = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
luaunit.assertTrue(foundRetained, "Retained element should still be in topElements")
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test 6: Retained child cleanup on parent destroy
|
||||
function TestMixedModeChildren:testRetainedChildCleanupOnDestroy()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local parent = FlexLove.new({
|
||||
id = "parent",
|
||||
mode = "immediate",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
local retainedChild = FlexLove.new({
|
||||
id = "retained_child",
|
||||
mode = "retained",
|
||||
width = 50,
|
||||
height = 50,
|
||||
})
|
||||
|
||||
parent:addChild(retainedChild)
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Destroy parent (simulating explicit cleanup)
|
||||
parent:destroy()
|
||||
|
||||
-- Frame 2: Create new parent with same ID
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local parent2 = FlexLove.new({
|
||||
id = "parent",
|
||||
mode = "immediate",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
-- Since parent was destroyed, retained children should be cleared
|
||||
luaunit.assertEquals(#parent2.children, 0, "Parent should have no children after destroy")
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test 7: Nested mixed-mode tree (immediate -> retained -> immediate)
|
||||
function TestMixedModeChildren:testNestedMixedModeTree()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local immediateParent = FlexLove.new({
|
||||
id = "immediate_parent",
|
||||
mode = "immediate",
|
||||
width = 300,
|
||||
height = 300,
|
||||
})
|
||||
|
||||
local retainedMiddle = FlexLove.new({
|
||||
id = "retained_middle",
|
||||
mode = "retained",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
local immediateGrandchild = FlexLove.new({
|
||||
id = "immediate_grandchild",
|
||||
mode = "immediate",
|
||||
width = 100,
|
||||
height = 100,
|
||||
})
|
||||
|
||||
immediateParent:addChild(retainedMiddle)
|
||||
retainedMiddle:addChild(immediateGrandchild)
|
||||
|
||||
luaunit.assertEquals(#immediateParent.children, 1, "Immediate parent should have 1 child")
|
||||
luaunit.assertEquals(#retainedMiddle.children, 1, "Retained middle should have 1 child")
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2: Recreate immediate parent
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local immediateParent2 = FlexLove.new({
|
||||
id = "immediate_parent",
|
||||
mode = "immediate",
|
||||
width = 300,
|
||||
height = 300,
|
||||
})
|
||||
|
||||
-- Retained middle should persist
|
||||
luaunit.assertEquals(#immediateParent2.children, 1, "Immediate parent should still have retained child")
|
||||
luaunit.assertEquals(immediateParent2.children[1], retainedMiddle, "Retained middle should persist")
|
||||
|
||||
-- Immediate grandchild should also persist (as child of retained middle)
|
||||
luaunit.assertEquals(#retainedMiddle.children, 1, "Retained middle should still have its child")
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test 8: Prevent duplicate creation of retained children
|
||||
function TestMixedModeChildren:testPreventDuplicateRetainedChildren()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
-- Frame 1: Create immediate parent with retained child
|
||||
local parent1 = FlexLove.new({
|
||||
id = "parent",
|
||||
mode = "immediate",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
local retainedChild = FlexLove.new({
|
||||
id = "unique_child",
|
||||
mode = "retained",
|
||||
parent = parent1,
|
||||
width = 50,
|
||||
height = 50,
|
||||
})
|
||||
|
||||
luaunit.assertEquals(#parent1.children, 1, "Parent should have 1 child")
|
||||
local originalChild = parent1.children[1]
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2: Recreate parent and try to create child again
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local parent2 = FlexLove.new({
|
||||
id = "parent",
|
||||
mode = "immediate",
|
||||
width = 200,
|
||||
height = 200,
|
||||
})
|
||||
|
||||
-- Try to create the same retained child again
|
||||
local duplicateAttempt = FlexLove.new({
|
||||
id = "unique_child",
|
||||
mode = "retained",
|
||||
parent = parent2,
|
||||
width = 50,
|
||||
height = 50,
|
||||
})
|
||||
|
||||
-- Should return the existing child, not create a new one
|
||||
luaunit.assertEquals(duplicateAttempt, originalChild, "Should return existing child instead of creating duplicate")
|
||||
luaunit.assertEquals(duplicateAttempt, retainedChild, "Should be the same retained child instance")
|
||||
luaunit.assertEquals(#parent2.children, 1, "Parent should still have only 1 child")
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Run tests
|
||||
if not _G.RUNNING_ALL_TESTS then
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
end
|
||||
@@ -16,7 +16,9 @@ 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
|
||||
return function()
|
||||
return require("modules." .. moduleName)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -189,4 +191,7 @@ function TestMixedModeEvents:test_multipleImmediateChildrenHandleEventsIndepende
|
||||
luaunit.assertTrue(button2Clicked)
|
||||
end
|
||||
|
||||
-- Run tests
|
||||
if not _G.RUNNING_ALL_TESTS then
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
end
|
||||
|
||||
193
testing/__tests__/retained_in_immediate_test.lua
Normal file
193
testing/__tests__/retained_in_immediate_test.lua
Normal file
@@ -0,0 +1,193 @@
|
||||
--[[
|
||||
Test: Retained Elements in Immediate Mode (No Duplication)
|
||||
|
||||
This test verifies that retained-mode elements don't get recreated
|
||||
when the overall application is in immediate mode.
|
||||
]]
|
||||
|
||||
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||
|
||||
require("testing.loveStub")
|
||||
|
||||
local luaunit = require("testing.luaunit")
|
||||
local FlexLove = require("FlexLove")
|
||||
local Color = require("modules.Color")
|
||||
|
||||
TestRetainedInImmediateMode = {}
|
||||
|
||||
function TestRetainedInImmediateMode:setUp()
|
||||
-- Initialize in IMMEDIATE mode
|
||||
FlexLove.init({ immediateMode = true })
|
||||
end
|
||||
|
||||
function TestRetainedInImmediateMode:tearDown()
|
||||
if FlexLove.getMode() == "immediate" then
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
FlexLove.init({ immediateMode = false })
|
||||
end
|
||||
|
||||
-- Test that top-level retained elements persist across frames
|
||||
function TestRetainedInImmediateMode:test_topLevelRetainedElementPersists()
|
||||
-- Helper function that creates elements (simulates user code called each frame)
|
||||
local function createUI()
|
||||
local backdrop = FlexLove.new({
|
||||
mode = "retained",
|
||||
width = "100%",
|
||||
height = "100%",
|
||||
backgroundColor = Color.new(0.1, 0.2, 0.3, 0.5),
|
||||
})
|
||||
return backdrop
|
||||
end
|
||||
|
||||
FlexLove.beginFrame()
|
||||
|
||||
-- Frame 1: Create a retained element (no explicit ID)
|
||||
local backdrop = createUI()
|
||||
|
||||
local backdropId = backdrop.id
|
||||
luaunit.assertNotNil(backdropId, "Backdrop should have auto-generated ID")
|
||||
luaunit.assertEquals(backdrop._elementMode, "retained")
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2: Call createUI() again (same function, same line numbers)
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local backdrop2 = createUI()
|
||||
|
||||
-- Should return the SAME element, not create a new one
|
||||
luaunit.assertEquals(backdrop2.id, backdropId, "Should return existing element with same ID")
|
||||
luaunit.assertEquals(backdrop2, backdrop, "Should return exact same element instance")
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test that retained elements with explicit IDs can be recreated
|
||||
function TestRetainedInImmediateMode:test_explicitIdAllowsNewElements()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
-- Create element with explicit ID
|
||||
local element1 = FlexLove.new({
|
||||
id = "my_custom_id",
|
||||
mode = "retained",
|
||||
width = 100,
|
||||
height = 100,
|
||||
backgroundColor = Color.new(1, 0, 0, 1),
|
||||
})
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
FlexLove.beginFrame()
|
||||
|
||||
-- Create another element with SAME explicit ID but different properties
|
||||
-- This should create a NEW element (user controls uniqueness)
|
||||
local element2 = FlexLove.new({
|
||||
id = "my_custom_id",
|
||||
mode = "retained",
|
||||
width = 200, -- Different properties
|
||||
height = 200,
|
||||
backgroundColor = Color.new(0, 1, 0, 1),
|
||||
})
|
||||
|
||||
-- With explicit IDs, we allow duplicates (user responsibility)
|
||||
luaunit.assertEquals(element2.id, "my_custom_id")
|
||||
-- Properties should match NEW element, not old
|
||||
luaunit.assertEquals(element2.width, 200)
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test that multiple retained elements persist independently
|
||||
function TestRetainedInImmediateMode:test_multipleRetainedElementsPersist()
|
||||
local function createUI()
|
||||
local backdrop = FlexLove.new({
|
||||
mode = "retained",
|
||||
width = "100%",
|
||||
height = "100%",
|
||||
})
|
||||
|
||||
local window = FlexLove.new({
|
||||
mode = "retained",
|
||||
width = "90%",
|
||||
height = "90%",
|
||||
})
|
||||
|
||||
return backdrop, window
|
||||
end
|
||||
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local backdrop, window = createUI()
|
||||
|
||||
local backdropId = backdrop.id
|
||||
local windowId = window.id
|
||||
|
||||
luaunit.assertNotEquals(backdropId, windowId, "Different elements should have different IDs")
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local backdrop2, window2 = createUI()
|
||||
|
||||
-- Both should return existing elements
|
||||
luaunit.assertEquals(backdrop2.id, backdropId)
|
||||
luaunit.assertEquals(window2.id, windowId)
|
||||
luaunit.assertEquals(backdrop2, backdrop)
|
||||
luaunit.assertEquals(window2, window)
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test that retained children of retained parents persist
|
||||
function TestRetainedInImmediateMode:test_retainedChildOfRetainedParentPersists()
|
||||
local function createUI()
|
||||
local parent = FlexLove.new({
|
||||
mode = "retained",
|
||||
width = 400,
|
||||
height = 400,
|
||||
})
|
||||
|
||||
local child = FlexLove.new({
|
||||
mode = "retained",
|
||||
parent = parent,
|
||||
width = 100,
|
||||
height = 100,
|
||||
})
|
||||
|
||||
return parent, child
|
||||
end
|
||||
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local parent, child = createUI()
|
||||
|
||||
local parentId = parent.id
|
||||
local childId = child.id
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local parent2, child2 = createUI()
|
||||
|
||||
-- Parent should be the same
|
||||
luaunit.assertEquals(parent2.id, parentId)
|
||||
luaunit.assertEquals(parent2, parent)
|
||||
|
||||
-- Child should also be the same instance
|
||||
luaunit.assertEquals(child2.id, childId, "Child ID should match")
|
||||
luaunit.assertEquals(child2, child, "Child should be same instance")
|
||||
|
||||
-- Child should still exist in parent's children
|
||||
luaunit.assertEquals(#parent2.children, 1, "Parent should have exactly 1 child")
|
||||
luaunit.assertEquals(parent2.children[1].id, childId)
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Run tests
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
184
testing/__tests__/retained_prop_stability_test.lua
Normal file
184
testing/__tests__/retained_prop_stability_test.lua
Normal file
@@ -0,0 +1,184 @@
|
||||
--[[
|
||||
Test: Retained Elements with Varying Props (ID Stability)
|
||||
|
||||
This test verifies that retained-mode elements return the same instance
|
||||
across frames even when props vary slightly (e.g., different Color instances).
|
||||
]]
|
||||
|
||||
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||
|
||||
require("testing.loveStub")
|
||||
|
||||
local luaunit = require("testing.luaunit")
|
||||
local FlexLove = require("FlexLove")
|
||||
local Color = require("modules.Color")
|
||||
|
||||
TestRetainedPropStability = {}
|
||||
|
||||
function TestRetainedPropStability:setUp()
|
||||
-- Initialize in IMMEDIATE mode
|
||||
FlexLove.init({ immediateMode = true })
|
||||
end
|
||||
|
||||
function TestRetainedPropStability:tearDown()
|
||||
if FlexLove.getMode() == "immediate" then
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
FlexLove.init({ immediateMode = false })
|
||||
end
|
||||
|
||||
-- Test that retained elements persist despite creating new Color instances
|
||||
function TestRetainedPropStability:test_retainedElementIgnoresColorInstanceChanges()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
-- Frame 1: Create retained element with Color instance
|
||||
local backdrop1 = FlexLove.new({
|
||||
mode = "retained",
|
||||
width = "100%",
|
||||
height = "100%",
|
||||
backgroundColor = Color.new(1, 1, 1, 0.1), -- NEW Color instance
|
||||
})
|
||||
|
||||
local id1 = backdrop1.id
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2: Same props but NEW Color instance (common pattern in user code)
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local backdrop2 = FlexLove.new({
|
||||
mode = "retained",
|
||||
width = "100%",
|
||||
height = "100%",
|
||||
backgroundColor = Color.new(1, 1, 1, 0.1), -- NEW Color instance (different table)
|
||||
})
|
||||
|
||||
-- Should return SAME element despite different Color instance
|
||||
luaunit.assertEquals(backdrop2.id, id1, "ID should be stable across frames")
|
||||
luaunit.assertEquals(backdrop2, backdrop1, "Should return same element instance")
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test that retained elements with complex props persist
|
||||
function TestRetainedPropStability:test_retainedElementWithComplexProps()
|
||||
local function createWindow()
|
||||
return FlexLove.new({
|
||||
mode = "retained",
|
||||
z = 100,
|
||||
x = "5%",
|
||||
y = "5%",
|
||||
width = "90%",
|
||||
height = "90%",
|
||||
themeComponent = "framev3",
|
||||
positioning = "flex",
|
||||
flexDirection = "vertical",
|
||||
justifySelf = "center",
|
||||
justifyContent = "flex-start",
|
||||
alignItems = "center",
|
||||
scaleCorners = 3,
|
||||
padding = { horizontal = "5%", vertical = "3%" },
|
||||
gap = 10,
|
||||
})
|
||||
end
|
||||
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local window1 = createWindow()
|
||||
local id1 = window1.id
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2: Same function, same props
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local window2 = createWindow()
|
||||
|
||||
-- Should return same element
|
||||
luaunit.assertEquals(window2.id, id1)
|
||||
luaunit.assertEquals(window2, window1)
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test that retained elements with backdrop blur persist
|
||||
function TestRetainedPropStability:test_retainedElementWithBackdropBlur()
|
||||
local function createBackdrop()
|
||||
return FlexLove.new({
|
||||
mode = "retained",
|
||||
width = "100%",
|
||||
height = "100%",
|
||||
backdropBlur = { radius = 10 }, -- Table prop
|
||||
backgroundColor = Color.new(1, 1, 1, 0.1),
|
||||
})
|
||||
end
|
||||
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local backdrop1 = createBackdrop()
|
||||
local id1 = backdrop1.id
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local backdrop2 = createBackdrop()
|
||||
|
||||
-- Should return same element
|
||||
luaunit.assertEquals(backdrop2.id, id1)
|
||||
luaunit.assertEquals(backdrop2, backdrop1)
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Test that multiple retained elements persist independently
|
||||
function TestRetainedPropStability:test_multipleRetainedElementsWithVaryingProps()
|
||||
local function createUI()
|
||||
local backdrop = FlexLove.new({
|
||||
mode = "retained",
|
||||
z = 50,
|
||||
width = "100%",
|
||||
height = "100%",
|
||||
backdropBlur = { radius = 10 },
|
||||
backgroundColor = Color.new(1, 1, 1, 0.1),
|
||||
})
|
||||
|
||||
local window = FlexLove.new({
|
||||
mode = "retained",
|
||||
z = 100,
|
||||
x = "5%",
|
||||
y = "5%",
|
||||
width = "90%",
|
||||
height = "90%",
|
||||
themeComponent = "framev3",
|
||||
padding = { horizontal = "5%", vertical = "3%" },
|
||||
})
|
||||
|
||||
return backdrop, window
|
||||
end
|
||||
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local backdrop1, window1 = createUI()
|
||||
local backdropId = backdrop1.id
|
||||
local windowId = window1.id
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
-- Frame 2: New Color instances, new table instances for props
|
||||
FlexLove.beginFrame()
|
||||
|
||||
local backdrop2, window2 = createUI()
|
||||
|
||||
-- Both should return existing elements
|
||||
luaunit.assertEquals(backdrop2.id, backdropId)
|
||||
luaunit.assertEquals(window2.id, windowId)
|
||||
luaunit.assertEquals(backdrop2, backdrop1)
|
||||
luaunit.assertEquals(window2, window1)
|
||||
|
||||
FlexLove.endFrame()
|
||||
end
|
||||
|
||||
-- Run tests
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
@@ -46,6 +46,9 @@ local testFiles = {
|
||||
"testing/__tests__/image_scaler_test.lua",
|
||||
"testing/__tests__/input_event_test.lua",
|
||||
"testing/__tests__/layout_engine_test.lua",
|
||||
"testing/__tests__/mixed_mode_events_test.lua",
|
||||
"testing/__tests__/mixed_mode_children_test.lua",
|
||||
"testing/__tests__/retained_in_immediate_test.lua",
|
||||
"testing/__tests__/module_loader_test.lua",
|
||||
"testing/__tests__/ninepatch_test.lua",
|
||||
"testing/__tests__/performance_test.lua",
|
||||
@@ -58,7 +61,7 @@ local testFiles = {
|
||||
"testing/__tests__/utils_test.lua",
|
||||
"testing/__tests__/calc_test.lua",
|
||||
-- Feature/Integration tests
|
||||
--"testing/__tests__/critical_failures_test.lua",
|
||||
"testing/__tests__/critical_failures_test.lua",
|
||||
"testing/__tests__/flexlove_test.lua",
|
||||
"testing/__tests__/touch_events_test.lua",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user