stable id - fixes input for immediate mode
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ themes/metal/
|
|||||||
themes/space/
|
themes/space/
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
tasks
|
tasks
|
||||||
|
testoutput.txt
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ function Gui.new(props)
|
|||||||
|
|
||||||
-- Immediate mode: generate ID if not provided
|
-- Immediate mode: generate ID if not provided
|
||||||
if not props.id then
|
if not props.id then
|
||||||
props.id = StateManager.generateID(props)
|
props.id = StateManager.generateID(props, props.parent)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get or create state for this element
|
-- Get or create state for this element
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ This library is under active development. While many features are functional, so
|
|||||||
|
|
||||||
### Coming Soon
|
### Coming Soon
|
||||||
The following features are currently being actively developed:
|
The following features are currently being actively developed:
|
||||||
|
- **Animations**: Simple to use animations for UI transitions and effects
|
||||||
- **Generic Image Support**: Enhanced image rendering capabilities and utilities
|
- **Generic Image Support**: Enhanced image rendering capabilities and utilities
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ function Element.new(props)
|
|||||||
|
|
||||||
-- Auto-generate ID in immediate mode if not provided
|
-- Auto-generate ID in immediate mode if not provided
|
||||||
if Gui._immediateMode and (not props.id or props.id == "") then
|
if Gui._immediateMode and (not props.id or props.id == "") then
|
||||||
self.id = StateManager.generateID(props)
|
self.id = StateManager.generateID(props, props.parent)
|
||||||
else
|
else
|
||||||
self.id = props.id or ""
|
self.id = props.id or ""
|
||||||
end
|
end
|
||||||
@@ -346,6 +346,36 @@ function Element.new(props)
|
|||||||
|
|
||||||
-- Scroll state for text overflow
|
-- Scroll state for text overflow
|
||||||
self._textScrollX = 0 -- Horizontal scroll offset in pixels
|
self._textScrollX = 0 -- Horizontal scroll offset in pixels
|
||||||
|
|
||||||
|
-- Restore state from StateManager in immediate mode
|
||||||
|
if Gui._immediateMode and self._stateId then
|
||||||
|
local state = StateManager.getState(self._stateId)
|
||||||
|
if state then
|
||||||
|
-- Restore focus state
|
||||||
|
if state._focused then
|
||||||
|
self._focused = true
|
||||||
|
Gui._focusedElement = self
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Restore text buffer (prefer state over props for immediate mode)
|
||||||
|
if state._textBuffer and state._textBuffer ~= "" then
|
||||||
|
self._textBuffer = state._textBuffer
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Restore cursor position
|
||||||
|
if state._cursorPosition then
|
||||||
|
self._cursorPosition = state._cursorPosition
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Restore selection
|
||||||
|
if state._selectionStart then
|
||||||
|
self._selectionStart = state._selectionStart
|
||||||
|
end
|
||||||
|
if state._selectionEnd then
|
||||||
|
self._selectionEnd = state._selectionEnd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Set parent first so it's available for size calculations
|
-- Set parent first so it's available for size calculations
|
||||||
@@ -396,7 +426,18 @@ function Element.new(props)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
self.text = props.text
|
-- For editable elements, default text to empty string if not provided
|
||||||
|
if self.editable and props.text == nil then
|
||||||
|
self.text = ""
|
||||||
|
else
|
||||||
|
self.text = props.text
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sync self.text with restored _textBuffer for editable elements in immediate mode
|
||||||
|
if self.editable and Gui._immediateMode and self._textBuffer then
|
||||||
|
self.text = self._textBuffer
|
||||||
|
end
|
||||||
|
|
||||||
self.textAlign = props.textAlign or TextAlign.START
|
self.textAlign = props.textAlign or TextAlign.START
|
||||||
|
|
||||||
-- Image properties
|
-- Image properties
|
||||||
@@ -4068,8 +4109,9 @@ function Element:_validateCursorPosition()
|
|||||||
if not self.editable then
|
if not self.editable then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local textLength = utf8.len(self._textBuffer or "")
|
local textLength = utf8.len(self._textBuffer or "") or 0
|
||||||
self._cursorPosition = math.max(0, math.min(self._cursorPosition, textLength))
|
local cursorPos = tonumber(self._cursorPosition) or 0
|
||||||
|
self._cursorPosition = math.max(0, math.min(cursorPos, textLength))
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Reset cursor blink (show cursor immediately)
|
--- Reset cursor blink (show cursor immediately)
|
||||||
@@ -4239,6 +4281,10 @@ function Element:deleteSelection()
|
|||||||
self:clearSelection()
|
self:clearSelection()
|
||||||
self._cursorPosition = startPos
|
self._cursorPosition = startPos
|
||||||
self:_validateCursorPosition()
|
self:_validateCursorPosition()
|
||||||
|
|
||||||
|
-- Save state to StateManager in immediate mode
|
||||||
|
self:_saveEditableState()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -4272,6 +4318,9 @@ function Element:focus()
|
|||||||
if self.onFocus then
|
if self.onFocus then
|
||||||
self.onFocus(self)
|
self.onFocus(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Save state to StateManager in immediate mode
|
||||||
|
self:_saveEditableState()
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Remove focus from this element
|
--- Remove focus from this element
|
||||||
@@ -4291,6 +4340,9 @@ function Element:blur()
|
|||||||
if self.onBlur then
|
if self.onBlur then
|
||||||
self.onBlur(self)
|
self.onBlur(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Save state to StateManager in immediate mode
|
||||||
|
self:_saveEditableState()
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Check if this element is focused
|
--- Check if this element is focused
|
||||||
@@ -4302,6 +4354,21 @@ function Element:isFocused()
|
|||||||
return self._focused == true
|
return self._focused == true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Save editable element state to StateManager (for immediate mode)
|
||||||
|
function Element:_saveEditableState()
|
||||||
|
if not self.editable or not self._stateId or not Gui._immediateMode then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
StateManager.updateState(self._stateId, {
|
||||||
|
_focused = self._focused,
|
||||||
|
_textBuffer = self._textBuffer,
|
||||||
|
_cursorPosition = self._cursorPosition,
|
||||||
|
_selectionStart = self._selectionStart,
|
||||||
|
_selectionEnd = self._selectionEnd,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
-- Input Handling - Text Buffer Management
|
-- Input Handling - Text Buffer Management
|
||||||
-- ====================
|
-- ====================
|
||||||
@@ -4329,6 +4396,9 @@ function Element:setText(text)
|
|||||||
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
|
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
|
||||||
self:_updateAutoGrowHeight() -- Then update height based on new content
|
self:_updateAutoGrowHeight() -- Then update height based on new content
|
||||||
self:_validateCursorPosition()
|
self:_validateCursorPosition()
|
||||||
|
|
||||||
|
-- Save state to StateManager in immediate mode
|
||||||
|
self:_saveEditableState()
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Insert text at position
|
--- Insert text at position
|
||||||
@@ -4369,6 +4439,9 @@ function Element:insertText(text, position)
|
|||||||
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
|
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
|
||||||
self:_updateAutoGrowHeight() -- Then update height based on new content
|
self:_updateAutoGrowHeight() -- Then update height based on new content
|
||||||
self:_validateCursorPosition()
|
self:_validateCursorPosition()
|
||||||
|
|
||||||
|
-- Save state to StateManager in immediate mode
|
||||||
|
self:_saveEditableState()
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param startPos number -- Start position (inclusive)
|
---@param startPos number -- Start position (inclusive)
|
||||||
@@ -4402,6 +4475,9 @@ function Element:deleteText(startPos, endPos)
|
|||||||
self:_markTextDirty()
|
self:_markTextDirty()
|
||||||
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
|
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
|
||||||
self:_updateAutoGrowHeight() -- Then update height based on new content
|
self:_updateAutoGrowHeight() -- Then update height based on new content
|
||||||
|
|
||||||
|
-- Save state to StateManager in immediate mode
|
||||||
|
self:_saveEditableState()
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Replace text in range
|
--- Replace text in range
|
||||||
@@ -5272,6 +5348,9 @@ function Element:textinput(text)
|
|||||||
if self.onTextChange and self._textBuffer ~= oldText then
|
if self.onTextChange and self._textBuffer ~= oldText then
|
||||||
self.onTextChange(self, self._textBuffer, oldText)
|
self.onTextChange(self, self._textBuffer, oldText)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Save state to StateManager in immediate mode
|
||||||
|
self:_saveEditableState()
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Handle key press (special keys)
|
--- Handle key press (special keys)
|
||||||
@@ -5510,6 +5589,9 @@ function Element:keypressed(key, scancode, isrepeat)
|
|||||||
end
|
end
|
||||||
self:_resetCursorBlink()
|
self:_resetCursorBlink()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Save state to StateManager in immediate mode
|
||||||
|
self:_saveEditableState()
|
||||||
end
|
end
|
||||||
|
|
||||||
return Element
|
return Element
|
||||||
|
|||||||
@@ -131,8 +131,8 @@ function GuiState.getTopElementAt(x, y)
|
|||||||
local function findInteractiveAncestor(elem)
|
local function findInteractiveAncestor(elem)
|
||||||
local current = elem
|
local current = elem
|
||||||
while current do
|
while current do
|
||||||
-- An element is interactive if it has a callback or themeComponent
|
-- An element is interactive if it has a callback, themeComponent, or is editable
|
||||||
if current.callback or current.themeComponent then
|
if current.callback or current.themeComponent or current.editable then
|
||||||
return current
|
return current
|
||||||
end
|
end
|
||||||
current = current.parent
|
current = current.parent
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ local function hashProps(props, visited, depth)
|
|||||||
onTextChange = true,
|
onTextChange = true,
|
||||||
onEnter = true,
|
onEnter = true,
|
||||||
userdata = true,
|
userdata = true,
|
||||||
|
-- Dynamic input/state properties that should not affect ID stability
|
||||||
|
text = true, -- Text content changes as user types
|
||||||
|
placeholder = true, -- Placeholder text is presentational
|
||||||
|
editable = true, -- Editable state can be toggled dynamically
|
||||||
|
selectOnFocus = true, -- Input behavior flag
|
||||||
|
autoGrow = true, -- Auto-grow behavior flag
|
||||||
|
passwordMode = true, -- Password mode can be toggled
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Collect and sort keys for consistent ordering
|
-- Collect and sort keys for consistent ordering
|
||||||
@@ -96,8 +103,9 @@ end
|
|||||||
|
|
||||||
--- 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
|
||||||
|
---@param parent table|nil Optional parent element for tree-based ID generation
|
||||||
---@return string
|
---@return string
|
||||||
function StateManager.generateID(props)
|
function StateManager.generateID(props, parent)
|
||||||
-- Get call stack information
|
-- Get call stack information
|
||||||
local info = debug.getinfo(3, "Sl") -- Level 3: caller of Element.new -> caller of generateID
|
local info = debug.getinfo(3, "Sl") -- Level 3: caller of Element.new -> caller of generateID
|
||||||
|
|
||||||
@@ -109,16 +117,43 @@ function StateManager.generateID(props)
|
|||||||
local source = info.source or "unknown"
|
local source = info.source or "unknown"
|
||||||
local line = info.currentline or 0
|
local line = info.currentline or 0
|
||||||
|
|
||||||
-- Create ID from source file and line number
|
-- Create base location key from source file and line number
|
||||||
local baseID = source:match("([^/\\]+)$") or source -- Get filename
|
local filename = source:match("([^/\\]+)$") or source -- Get filename
|
||||||
baseID = baseID:gsub("%.lua$", "") -- Remove .lua extension
|
filename = filename:gsub("%.lua$", "") -- Remove .lua extension
|
||||||
local locationKey = baseID .. "_L" .. line
|
local locationKey = filename .. "_L" .. line
|
||||||
|
|
||||||
|
-- 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 {})
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
-- Add property hash if provided (for additional differentiation at same position)
|
||||||
|
if props then
|
||||||
|
local propHash = hashProps(props)
|
||||||
|
if propHash ~= "" then
|
||||||
|
-- Use first 8 chars of a simple hash
|
||||||
|
local hash = 0
|
||||||
|
for i = 1, #propHash do
|
||||||
|
hash = (hash * 31 + string.byte(propHash, i)) % 1000000
|
||||||
|
end
|
||||||
|
baseID = baseID .. "_" .. hash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return baseID
|
||||||
|
end
|
||||||
|
|
||||||
|
-- No parent (top-level element): use call-site counter approach
|
||||||
-- Track how many elements have been created at this location
|
-- Track how many elements have been created at this location
|
||||||
callSiteCounters[locationKey] = (callSiteCounters[locationKey] or 0) + 1
|
callSiteCounters[locationKey] = (callSiteCounters[locationKey] or 0) + 1
|
||||||
local instanceNum = callSiteCounters[locationKey]
|
local instanceNum = callSiteCounters[locationKey]
|
||||||
|
|
||||||
baseID = locationKey
|
local baseID = locationKey
|
||||||
|
|
||||||
-- Add instance number if multiple elements created at same location (e.g., in loops)
|
-- Add instance number if multiple elements created at same location (e.g., in loops)
|
||||||
if instanceNum > 1 then
|
if instanceNum > 1 then
|
||||||
|
|||||||
@@ -1,367 +0,0 @@
|
|||||||
-- Event System Tests
|
|
||||||
-- Tests for the enhanced callback system with InputEvent objects
|
|
||||||
|
|
||||||
package.path = package.path .. ";?.lua"
|
|
||||||
|
|
||||||
local lu = require("testing.luaunit")
|
|
||||||
require("testing.loveStub") -- Required to mock LOVE functions
|
|
||||||
local FlexLove = require("FlexLove")
|
|
||||||
local Gui = FlexLove.Gui
|
|
||||||
|
|
||||||
TestEventSystem = {}
|
|
||||||
|
|
||||||
function TestEventSystem:setUp()
|
|
||||||
-- Clear all keyboard modifier states at start of each test
|
|
||||||
love.keyboard.setDown("lshift", false)
|
|
||||||
love.keyboard.setDown("rshift", false)
|
|
||||||
love.keyboard.setDown("lctrl", false)
|
|
||||||
love.keyboard.setDown("rctrl", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
love.keyboard.setDown("ralt", false)
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
love.keyboard.setDown("rgui", false)
|
|
||||||
|
|
||||||
Gui.init({ baseScale = { width = 1920, height = 1080 } })
|
|
||||||
love.window.setMode(1920, 1080)
|
|
||||||
Gui.resize(1920, 1080) -- Recalculate scale factors after setMode
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestEventSystem:tearDown()
|
|
||||||
-- Clean up after each test
|
|
||||||
Gui.destroy()
|
|
||||||
-- Reset keyboard state
|
|
||||||
love.keyboard.setDown("lshift", false)
|
|
||||||
love.keyboard.setDown("rshift", false)
|
|
||||||
love.keyboard.setDown("lctrl", false)
|
|
||||||
love.keyboard.setDown("rctrl", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
love.keyboard.setDown("ralt", false)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test 1: Event object structure
|
|
||||||
function TestEventSystem:test_event_object_has_required_fields()
|
|
||||||
local eventReceived = nil
|
|
||||||
|
|
||||||
local button = Gui.new({
|
|
||||||
x = 100,
|
|
||||||
y = 100,
|
|
||||||
width = 200,
|
|
||||||
height = 100,
|
|
||||||
callback = function(element, event)
|
|
||||||
eventReceived = event
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Simulate mouse press and release
|
|
||||||
love.mouse.setPosition(150, 150)
|
|
||||||
love.mouse.setDown(1, true)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
-- Verify event object structure
|
|
||||||
lu.assertNotNil(eventReceived, "Event should be received")
|
|
||||||
lu.assertNotNil(eventReceived.type, "Event should have type field")
|
|
||||||
lu.assertNotNil(eventReceived.button, "Event should have button field")
|
|
||||||
lu.assertNotNil(eventReceived.x, "Event should have x field")
|
|
||||||
lu.assertNotNil(eventReceived.y, "Event should have y field")
|
|
||||||
lu.assertNotNil(eventReceived.modifiers, "Event should have modifiers field")
|
|
||||||
lu.assertNotNil(eventReceived.clickCount, "Event should have clickCount field")
|
|
||||||
lu.assertNotNil(eventReceived.timestamp, "Event should have timestamp field")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test 2: Left click event
|
|
||||||
function TestEventSystem:test_left_click_generates_click_event()
|
|
||||||
local eventsReceived = {}
|
|
||||||
|
|
||||||
local button = Gui.new({
|
|
||||||
x = 100,
|
|
||||||
y = 100,
|
|
||||||
width = 200,
|
|
||||||
height = 100,
|
|
||||||
callback = function(element, event)
|
|
||||||
table.insert(eventsReceived, { type = event.type, button = event.button })
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Simulate left click
|
|
||||||
love.mouse.setPosition(150, 150)
|
|
||||||
love.mouse.setDown(1, true)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
-- Should receive press, click, and release events
|
|
||||||
lu.assertTrue(#eventsReceived >= 2, "Should receive at least 2 events")
|
|
||||||
|
|
||||||
-- Check for click event
|
|
||||||
local hasClickEvent = false
|
|
||||||
for _, evt in ipairs(eventsReceived) do
|
|
||||||
if evt.type == "click" and evt.button == 1 then
|
|
||||||
hasClickEvent = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
lu.assertTrue(hasClickEvent, "Should receive click event for left button")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test 3: Right click event
|
|
||||||
function TestEventSystem:test_right_click_generates_rightclick_event()
|
|
||||||
local eventsReceived = {}
|
|
||||||
|
|
||||||
local button = Gui.new({
|
|
||||||
x = 100,
|
|
||||||
y = 100,
|
|
||||||
width = 200,
|
|
||||||
height = 100,
|
|
||||||
callback = function(element, event)
|
|
||||||
table.insert(eventsReceived, { type = event.type, button = event.button })
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Simulate right click
|
|
||||||
love.mouse.setPosition(150, 150)
|
|
||||||
love.mouse.setDown(2, true)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
love.mouse.setDown(2, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
-- Check for rightclick event
|
|
||||||
local hasRightClickEvent = false
|
|
||||||
for _, evt in ipairs(eventsReceived) do
|
|
||||||
if evt.type == "rightclick" and evt.button == 2 then
|
|
||||||
hasRightClickEvent = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
lu.assertTrue(hasRightClickEvent, "Should receive rightclick event for right button")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test 4: Middle click event
|
|
||||||
function TestEventSystem:test_middle_click_generates_middleclick_event()
|
|
||||||
local eventsReceived = {}
|
|
||||||
|
|
||||||
local button = Gui.new({
|
|
||||||
x = 100,
|
|
||||||
y = 100,
|
|
||||||
width = 200,
|
|
||||||
height = 100,
|
|
||||||
callback = function(element, event)
|
|
||||||
table.insert(eventsReceived, { type = event.type, button = event.button })
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Simulate middle click
|
|
||||||
love.mouse.setPosition(150, 150)
|
|
||||||
love.mouse.setDown(3, true)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
love.mouse.setDown(3, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
-- Check for middleclick event
|
|
||||||
local hasMiddleClickEvent = false
|
|
||||||
for _, evt in ipairs(eventsReceived) do
|
|
||||||
if evt.type == "middleclick" and evt.button == 3 then
|
|
||||||
hasMiddleClickEvent = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
lu.assertTrue(hasMiddleClickEvent, "Should receive middleclick event for middle button")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test 5: Modifier keys detection
|
|
||||||
function TestEventSystem:test_modifier_keys_are_detected()
|
|
||||||
local eventReceived = nil
|
|
||||||
|
|
||||||
local button = Gui.new({
|
|
||||||
x = 100,
|
|
||||||
y = 100,
|
|
||||||
width = 200,
|
|
||||||
height = 100,
|
|
||||||
callback = function(element, event)
|
|
||||||
if event.type == "click" then
|
|
||||||
eventReceived = event
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Simulate shift + click
|
|
||||||
love.keyboard.setDown("lshift", true)
|
|
||||||
love.mouse.setPosition(150, 150)
|
|
||||||
love.mouse.setDown(1, true)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
lu.assertNotNil(eventReceived, "Should receive click event")
|
|
||||||
lu.assertTrue(eventReceived.modifiers.shift, "Shift modifier should be detected")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test 6: Double click detection
|
|
||||||
function TestEventSystem:test_double_click_increments_click_count()
|
|
||||||
local clickEvents = {}
|
|
||||||
|
|
||||||
local button = Gui.new({
|
|
||||||
x = 100,
|
|
||||||
y = 100,
|
|
||||||
width = 200,
|
|
||||||
height = 100,
|
|
||||||
callback = function(element, event)
|
|
||||||
if event.type == "click" then
|
|
||||||
table.insert(clickEvents, event.clickCount)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Simulate first click
|
|
||||||
love.mouse.setPosition(150, 150)
|
|
||||||
love.mouse.setDown(1, true)
|
|
||||||
button:update(0.016)
|
|
||||||
love.mouse.setDown(1, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
-- Simulate second click quickly (double-click)
|
|
||||||
love.timer.setTime(love.timer.getTime() + 0.1) -- 100ms later
|
|
||||||
love.mouse.setDown(1, true)
|
|
||||||
button:update(0.016)
|
|
||||||
love.mouse.setDown(1, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
lu.assertEquals(#clickEvents, 2, "Should receive 2 click events")
|
|
||||||
lu.assertEquals(clickEvents[1], 1, "First click should have clickCount = 1")
|
|
||||||
lu.assertEquals(clickEvents[2], 2, "Second click should have clickCount = 2")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test 7: Press and release events
|
|
||||||
function TestEventSystem:test_press_and_release_events_are_fired()
|
|
||||||
local eventsReceived = {}
|
|
||||||
|
|
||||||
local button = Gui.new({
|
|
||||||
x = 100,
|
|
||||||
y = 100,
|
|
||||||
width = 200,
|
|
||||||
height = 100,
|
|
||||||
callback = function(element, event)
|
|
||||||
table.insert(eventsReceived, event.type)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Simulate click
|
|
||||||
love.mouse.setPosition(150, 150)
|
|
||||||
love.mouse.setDown(1, true)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
-- Should receive press, click, and release
|
|
||||||
lu.assertTrue(#eventsReceived >= 2, "Should receive multiple events")
|
|
||||||
|
|
||||||
local hasPress = false
|
|
||||||
local hasRelease = false
|
|
||||||
for _, eventType in ipairs(eventsReceived) do
|
|
||||||
if eventType == "press" then
|
|
||||||
hasPress = true
|
|
||||||
end
|
|
||||||
if eventType == "release" then
|
|
||||||
hasRelease = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
lu.assertTrue(hasPress, "Should receive press event")
|
|
||||||
lu.assertTrue(hasRelease, "Should receive release event")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test 8: Mouse position in event
|
|
||||||
function TestEventSystem:test_event_contains_mouse_position()
|
|
||||||
local eventReceived = nil
|
|
||||||
|
|
||||||
local button = Gui.new({
|
|
||||||
x = 100,
|
|
||||||
y = 100,
|
|
||||||
width = 200,
|
|
||||||
height = 100,
|
|
||||||
callback = function(element, event)
|
|
||||||
if event.type == "click" then
|
|
||||||
eventReceived = event
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Simulate click at specific position
|
|
||||||
local mouseX, mouseY = 175, 125
|
|
||||||
love.mouse.setPosition(mouseX, mouseY)
|
|
||||||
love.mouse.setDown(1, true)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
lu.assertNotNil(eventReceived, "Should receive click event")
|
|
||||||
lu.assertEquals(eventReceived.x, mouseX, "Event should contain correct mouse X position")
|
|
||||||
lu.assertEquals(eventReceived.y, mouseY, "Event should contain correct mouse Y position")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test 9: No callback when mouse outside element
|
|
||||||
function TestEventSystem:test_no_callback_when_clicking_outside_element()
|
|
||||||
local callbackCalled = false
|
|
||||||
|
|
||||||
local button = Gui.new({
|
|
||||||
x = 100,
|
|
||||||
y = 100,
|
|
||||||
width = 200,
|
|
||||||
height = 100,
|
|
||||||
callback = function(element, event)
|
|
||||||
callbackCalled = true
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Click outside element
|
|
||||||
love.mouse.setPosition(50, 50)
|
|
||||||
love.mouse.setDown(1, true)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
lu.assertFalse(callbackCalled, "Callback should not be called when clicking outside element")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test 10: Multiple modifiers
|
|
||||||
function TestEventSystem:test_multiple_modifiers_detected()
|
|
||||||
local eventReceived = nil
|
|
||||||
|
|
||||||
local button = Gui.new({
|
|
||||||
x = 100,
|
|
||||||
y = 100,
|
|
||||||
width = 200,
|
|
||||||
height = 100,
|
|
||||||
callback = function(element, event)
|
|
||||||
if event.type == "click" then
|
|
||||||
eventReceived = event
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Simulate shift + ctrl + click
|
|
||||||
love.keyboard.setDown("lshift", true)
|
|
||||||
love.keyboard.setDown("lctrl", true)
|
|
||||||
love.mouse.setPosition(150, 150)
|
|
||||||
love.mouse.setDown(1, true)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
|
||||||
button:update(0.016)
|
|
||||||
|
|
||||||
lu.assertNotNil(eventReceived, "Should receive click event")
|
|
||||||
lu.assertTrue(eventReceived.modifiers.shift, "Shift modifier should be detected")
|
|
||||||
lu.assertTrue(eventReceived.modifiers.ctrl, "Ctrl modifier should be detected")
|
|
||||||
end
|
|
||||||
|
|
||||||
print("Running Event System Tests...")
|
|
||||||
lu.LuaUnit.run()
|
|
||||||
@@ -1,733 +0,0 @@
|
|||||||
local lu = require("testing.luaunit")
|
|
||||||
local FlexLove = require("FlexLove")
|
|
||||||
local Gui = FlexLove.Gui
|
|
||||||
local Element = FlexLove.Element
|
|
||||||
|
|
||||||
TestKeyboardInput = {}
|
|
||||||
|
|
||||||
-- Helper function to ensure clean keyboard state
|
|
||||||
local function clearModifierKeys()
|
|
||||||
love.keyboard.setDown("lshift", false)
|
|
||||||
love.keyboard.setDown("rshift", false)
|
|
||||||
love.keyboard.setDown("lctrl", false)
|
|
||||||
love.keyboard.setDown("rctrl", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
love.keyboard.setDown("ralt", false)
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
love.keyboard.setDown("rgui", false)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:setUp()
|
|
||||||
-- Clear all keyboard modifier states at start of each test
|
|
||||||
if love.keyboard.isDown("lgui", "rgui", "lalt", "ralt", "lctrl", "rctrl") then
|
|
||||||
print("WARNING: Modifier keys were down at start of test!")
|
|
||||||
end
|
|
||||||
love.keyboard.setDown("lshift", false)
|
|
||||||
love.keyboard.setDown("rshift", false)
|
|
||||||
love.keyboard.setDown("lctrl", false)
|
|
||||||
love.keyboard.setDown("rctrl", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
love.keyboard.setDown("ralt", false)
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
love.keyboard.setDown("rgui", false)
|
|
||||||
|
|
||||||
Gui.init({ baseScale = { width = 1920, height = 1080 } })
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:tearDown()
|
|
||||||
-- Clear all keyboard modifier states
|
|
||||||
love.keyboard.setDown("lshift", false)
|
|
||||||
love.keyboard.setDown("rshift", false)
|
|
||||||
love.keyboard.setDown("lctrl", false)
|
|
||||||
love.keyboard.setDown("rctrl", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
love.keyboard.setDown("ralt", false)
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
love.keyboard.setDown("rgui", false)
|
|
||||||
|
|
||||||
Gui.destroy()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Focus Management Tests
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
function TestKeyboardInput:testFocusEditable()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
lu.assertFalse(input:isFocused())
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
|
|
||||||
lu.assertTrue(input:isFocused())
|
|
||||||
lu.assertEquals(Gui._focusedElement, input)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testBlurEditable()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
lu.assertTrue(input:isFocused())
|
|
||||||
|
|
||||||
input:blur()
|
|
||||||
|
|
||||||
lu.assertFalse(input:isFocused())
|
|
||||||
lu.assertNil(Gui._focusedElement)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testFocusSwitching()
|
|
||||||
local input1 = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Input 1",
|
|
||||||
})
|
|
||||||
|
|
||||||
local input2 = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Input 2",
|
|
||||||
})
|
|
||||||
|
|
||||||
input1:focus()
|
|
||||||
lu.assertTrue(input1:isFocused())
|
|
||||||
lu.assertFalse(input2:isFocused())
|
|
||||||
|
|
||||||
input2:focus()
|
|
||||||
lu.assertFalse(input1:isFocused())
|
|
||||||
lu.assertTrue(input2:isFocused())
|
|
||||||
lu.assertEquals(Gui._focusedElement, input2)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testSelectOnFocus()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello World",
|
|
||||||
selectOnFocus = true,
|
|
||||||
})
|
|
||||||
|
|
||||||
lu.assertFalse(input:hasSelection())
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
|
|
||||||
lu.assertTrue(input:hasSelection())
|
|
||||||
local startPos, endPos = input:getSelection()
|
|
||||||
lu.assertEquals(startPos, 0)
|
|
||||||
lu.assertEquals(endPos, 11) -- Length of "Hello World"
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Text Input Tests
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
function TestKeyboardInput:testTextInput()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:textinput("H")
|
|
||||||
input:textinput("i")
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), "Hi")
|
|
||||||
lu.assertEquals(input._cursorPosition, 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testTextInputAtPosition()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(2) -- After "He"
|
|
||||||
input:textinput("X")
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), "HeXllo")
|
|
||||||
lu.assertEquals(input._cursorPosition, 3)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testTextInputWithSelection()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello World",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setSelection(0, 5) -- Select "Hello"
|
|
||||||
input:textinput("Hi")
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), "Hi World")
|
|
||||||
lu.assertEquals(input._cursorPosition, 2)
|
|
||||||
lu.assertFalse(input:hasSelection())
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testMaxLengthConstraint()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "",
|
|
||||||
maxLength = 5,
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:textinput("Hello")
|
|
||||||
lu.assertEquals(input:getText(), "Hello")
|
|
||||||
|
|
||||||
input:textinput("X") -- Should not be added
|
|
||||||
lu.assertEquals(input:getText(), "Hello")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Backspace/Delete Tests
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
function TestKeyboardInput:testBackspace()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(5) -- At end
|
|
||||||
input:keypressed("backspace", "backspace", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), "Hell")
|
|
||||||
lu.assertEquals(input._cursorPosition, 4)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testBackspaceAtStart()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(0) -- At start
|
|
||||||
input:keypressed("backspace", "backspace", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), "Hello") -- No change
|
|
||||||
lu.assertEquals(input._cursorPosition, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testDelete()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(0) -- At start
|
|
||||||
input:keypressed("delete", "delete", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), "ello")
|
|
||||||
lu.assertEquals(input._cursorPosition, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testDeleteAtEnd()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(5) -- At end
|
|
||||||
input:keypressed("delete", "delete", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), "Hello") -- No change
|
|
||||||
lu.assertEquals(input._cursorPosition, 5)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testBackspaceWithSelection()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello World",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setSelection(0, 5) -- Select "Hello"
|
|
||||||
input:keypressed("backspace", "backspace", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), " World")
|
|
||||||
lu.assertEquals(input._cursorPosition, 0)
|
|
||||||
lu.assertFalse(input:hasSelection())
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Cursor Movement Tests
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
function TestKeyboardInput:testArrowLeft()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(5)
|
|
||||||
input:keypressed("left", "left", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 4)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testArrowRight()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(0)
|
|
||||||
input:keypressed("right", "right", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testHomeKey()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(5)
|
|
||||||
input:keypressed("home", "home", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testEndKey()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(0)
|
|
||||||
input:keypressed("end", "end", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 5)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Modifier Key Tests
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
function TestKeyboardInput:testSuperLeftMovesToStart()
|
|
||||||
clearModifierKeys()
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello World",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(8) -- Middle of text
|
|
||||||
|
|
||||||
-- Simulate Super (Cmd/Win) key being held
|
|
||||||
love.keyboard.setDown("lgui", true)
|
|
||||||
input:keypressed("left", "left", false)
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testSuperRightMovesToEnd()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello World",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(3) -- Middle of text
|
|
||||||
|
|
||||||
-- Ensure clean state before setting modifier
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
love.keyboard.setDown("lctrl", false)
|
|
||||||
|
|
||||||
-- Simulate Super (Cmd/Win) key being held
|
|
||||||
love.keyboard.setDown("lgui", true)
|
|
||||||
input:keypressed("right", "right", false)
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 11) -- End of "Hello World"
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testAltLeftMovesByWord()
|
|
||||||
clearModifierKeys()
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello World Test",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(16) -- End of text
|
|
||||||
|
|
||||||
-- Simulate Alt key being held and move left by word
|
|
||||||
love.keyboard.setDown("lalt", true)
|
|
||||||
input:keypressed("left", "left", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 12) -- Start of "Test"
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testAltRightMovesByWord()
|
|
||||||
clearModifierKeys()
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello World Test",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(0) -- Start of text
|
|
||||||
|
|
||||||
-- Simulate Alt key being held and move right by word
|
|
||||||
love.keyboard.setDown("lalt", true)
|
|
||||||
input:keypressed("right", "right", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 6) -- After "Hello "
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testAltLeftMultipleWords()
|
|
||||||
clearModifierKeys()
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "The quick brown fox",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(19) -- End of text
|
|
||||||
|
|
||||||
-- Move left by word three times
|
|
||||||
love.keyboard.setDown("lalt", true)
|
|
||||||
input:keypressed("left", "left", false) -- to "fox"
|
|
||||||
input:keypressed("left", "left", false) -- to "brown"
|
|
||||||
input:keypressed("left", "left", false) -- to "quick"
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 4) -- Start of "quick"
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testAltRightMultipleWords()
|
|
||||||
-- Ensure clean keyboard state
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
love.keyboard.setDown("lctrl", false)
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "The quick brown fox",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(0) -- Start of text
|
|
||||||
|
|
||||||
-- Move right by word three times
|
|
||||||
love.keyboard.setDown("lalt", true)
|
|
||||||
input:keypressed("right", "right", false) -- after "The "
|
|
||||||
input:keypressed("right", "right", false) -- after "quick "
|
|
||||||
input:keypressed("right", "right", false) -- after "brown "
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 16) -- After "brown "
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testSuperLeftWithSelection()
|
|
||||||
-- Ensure clean keyboard state
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
love.keyboard.setDown("lctrl", false)
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello World",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setSelection(3, 8)
|
|
||||||
lu.assertTrue(input:hasSelection())
|
|
||||||
|
|
||||||
-- Super+Left should move to start and clear selection
|
|
||||||
love.keyboard.setDown("lgui", true)
|
|
||||||
input:keypressed("left", "left", false)
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 0)
|
|
||||||
lu.assertFalse(input:hasSelection())
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testSuperRightWithSelection()
|
|
||||||
-- Ensure clean keyboard state
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
love.keyboard.setDown("lalt", false)
|
|
||||||
love.keyboard.setDown("lctrl", false)
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello World",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setSelection(3, 8)
|
|
||||||
lu.assertTrue(input:hasSelection())
|
|
||||||
|
|
||||||
-- Super+Right should move to end and clear selection
|
|
||||||
love.keyboard.setDown("lgui", true)
|
|
||||||
input:keypressed("right", "right", false)
|
|
||||||
love.keyboard.setDown("lgui", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input._cursorPosition, 11)
|
|
||||||
lu.assertFalse(input:hasSelection())
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testEscapeClearsSelection()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:selectAll()
|
|
||||||
lu.assertTrue(input:hasSelection())
|
|
||||||
|
|
||||||
input:keypressed("escape", "escape", false)
|
|
||||||
|
|
||||||
lu.assertFalse(input:hasSelection())
|
|
||||||
lu.assertTrue(input:isFocused()) -- Still focused
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testEscapeBlurs()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
lu.assertTrue(input:isFocused())
|
|
||||||
|
|
||||||
input:keypressed("escape", "escape", false)
|
|
||||||
|
|
||||||
lu.assertFalse(input:isFocused())
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Callback Tests
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
function TestKeyboardInput:testOnTextChangeCallback()
|
|
||||||
local changeCount = 0
|
|
||||||
local oldTextValue = nil
|
|
||||||
local newTextValue = nil
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
onTextChange = function(element, newText, oldText)
|
|
||||||
changeCount = changeCount + 1
|
|
||||||
newTextValue = newText
|
|
||||||
oldTextValue = oldText
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:textinput("X")
|
|
||||||
|
|
||||||
lu.assertEquals(changeCount, 1)
|
|
||||||
lu.assertEquals(oldTextValue, "Hello")
|
|
||||||
lu.assertEquals(newTextValue, "HelloX")
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testOnTextInputCallback()
|
|
||||||
local inputCount = 0
|
|
||||||
local lastChar = nil
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "",
|
|
||||||
onTextInput = function(element, text)
|
|
||||||
inputCount = inputCount + 1
|
|
||||||
lastChar = text
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:textinput("A")
|
|
||||||
input:textinput("B")
|
|
||||||
|
|
||||||
lu.assertEquals(inputCount, 2)
|
|
||||||
lu.assertEquals(lastChar, "B")
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testOnEnterCallback()
|
|
||||||
local enterCalled = false
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
multiline = false,
|
|
||||||
text = "Hello",
|
|
||||||
onEnter = function(element)
|
|
||||||
enterCalled = true
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:keypressed("return", "return", false)
|
|
||||||
|
|
||||||
lu.assertTrue(enterCalled)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testOnFocusCallback()
|
|
||||||
local focusCalled = false
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
onFocus = function(element)
|
|
||||||
focusCalled = true
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
|
|
||||||
lu.assertTrue(focusCalled)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testOnBlurCallback()
|
|
||||||
local blurCalled = false
|
|
||||||
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
onBlur = function(element)
|
|
||||||
blurCalled = true
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:blur()
|
|
||||||
|
|
||||||
lu.assertTrue(blurCalled)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- GUI-level Input Forwarding Tests
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
function TestKeyboardInput:testGuiTextinputForwarding()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
Gui.textinput("A")
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), "A")
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testGuiKeypressedForwarding()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
input:focus()
|
|
||||||
input:setCursorPosition(5)
|
|
||||||
Gui.keypressed("backspace", "backspace", false)
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), "Hell")
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestKeyboardInput:testGuiInputWithoutFocus()
|
|
||||||
local input = Element.new({
|
|
||||||
width = 200,
|
|
||||||
height = 40,
|
|
||||||
editable = true,
|
|
||||||
text = "Hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
-- No focus
|
|
||||||
Gui.textinput("X")
|
|
||||||
|
|
||||||
lu.assertEquals(input:getText(), "Hello") -- No change
|
|
||||||
end
|
|
||||||
|
|
||||||
lu.LuaUnit.run()
|
|
||||||
@@ -11,6 +11,7 @@ _G.love = loveStub
|
|||||||
|
|
||||||
-- Load FlexLove after setting up love stub
|
-- Load FlexLove after setting up love stub
|
||||||
local FlexLove = require("FlexLove")
|
local FlexLove = require("FlexLove")
|
||||||
|
local StateManager = require("modules.StateManager")
|
||||||
|
|
||||||
-- Test fixtures
|
-- Test fixtures
|
||||||
local testElement
|
local testElement
|
||||||
@@ -54,6 +55,9 @@ function TestInputField:tearDown()
|
|||||||
love.keyboard.setDown("lgui", false)
|
love.keyboard.setDown("lgui", false)
|
||||||
love.keyboard.setDown("rgui", false)
|
love.keyboard.setDown("rgui", false)
|
||||||
|
|
||||||
|
-- Clear StateManager to prevent test contamination
|
||||||
|
StateManager.reset()
|
||||||
|
|
||||||
testElement = nil
|
testElement = nil
|
||||||
FlexLove.Gui.topElements = {}
|
FlexLove.Gui.topElements = {}
|
||||||
FlexLove.Gui._focusedElement = nil
|
FlexLove.Gui._focusedElement = nil
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
|
|
||||||
local lu = require("testing.luaunit")
|
local lu = require("testing.luaunit")
|
||||||
local loveStub = require("testing.loveStub")
|
local loveStub = require("testing.loveStub")
|
||||||
|
local utf8 = require("utf8")
|
||||||
|
|
||||||
-- Setup LÖVE environment
|
-- Setup LÖVE environment
|
||||||
_G.love = loveStub
|
_G.love = loveStub
|
||||||
|
|
||||||
-- Load FlexLove after setting up love stub
|
-- Load FlexLove after setting up love stub
|
||||||
local FlexLove = require("FlexLove")
|
local FlexLove = require("FlexLove")
|
||||||
|
local StateManager = require("modules.StateManager")
|
||||||
|
|
||||||
-- Test fixtures
|
-- Test fixtures
|
||||||
local testElement
|
local testElement
|
||||||
@@ -27,7 +29,7 @@ function TestPasswordMode:setUp()
|
|||||||
love.keyboard.setDown("ralt", false)
|
love.keyboard.setDown("ralt", false)
|
||||||
love.keyboard.setDown("lgui", false)
|
love.keyboard.setDown("lgui", false)
|
||||||
love.keyboard.setDown("rgui", false)
|
love.keyboard.setDown("rgui", false)
|
||||||
|
|
||||||
-- Reset FlexLove state
|
-- Reset FlexLove state
|
||||||
FlexLove.Gui.topElements = {}
|
FlexLove.Gui.topElements = {}
|
||||||
FlexLove.Gui._focusedElement = nil
|
FlexLove.Gui._focusedElement = nil
|
||||||
@@ -54,7 +56,10 @@ function TestPasswordMode:tearDown()
|
|||||||
love.keyboard.setDown("ralt", false)
|
love.keyboard.setDown("ralt", false)
|
||||||
love.keyboard.setDown("lgui", false)
|
love.keyboard.setDown("lgui", false)
|
||||||
love.keyboard.setDown("rgui", false)
|
love.keyboard.setDown("rgui", false)
|
||||||
|
|
||||||
|
-- Clear StateManager to prevent test contamination
|
||||||
|
StateManager.reset()
|
||||||
|
|
||||||
testElement = nil
|
testElement = nil
|
||||||
FlexLove.Gui.topElements = {}
|
FlexLove.Gui.topElements = {}
|
||||||
FlexLove.Gui._focusedElement = nil
|
FlexLove.Gui._focusedElement = nil
|
||||||
@@ -80,7 +85,7 @@ function TestPasswordMode:testPasswordModeDefaultIsFalse()
|
|||||||
editable = true,
|
editable = true,
|
||||||
text = "Normal text",
|
text = "Normal text",
|
||||||
})
|
})
|
||||||
|
|
||||||
lu.assertFalse(normalElement.passwordMode or false)
|
lu.assertFalse(normalElement.passwordMode or false)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -97,7 +102,7 @@ function TestPasswordMode:testPasswordModeIsSingleLineOnly()
|
|||||||
passwordMode = true,
|
passwordMode = true,
|
||||||
text = "Password",
|
text = "Password",
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Based on the constraint, multiline should be set to false
|
-- Based on the constraint, multiline should be set to false
|
||||||
lu.assertFalse(multilinePassword.multiline)
|
lu.assertFalse(multilinePassword.multiline)
|
||||||
end
|
end
|
||||||
@@ -115,7 +120,7 @@ function TestPasswordMode:testActualTextContentRemains()
|
|||||||
testElement:insertText("r")
|
testElement:insertText("r")
|
||||||
testElement:insertText("e")
|
testElement:insertText("e")
|
||||||
testElement:insertText("t")
|
testElement:insertText("t")
|
||||||
|
|
||||||
-- Verify actual text buffer contains the real text
|
-- Verify actual text buffer contains the real text
|
||||||
lu.assertEquals(testElement._textBuffer, "Secret")
|
lu.assertEquals(testElement._textBuffer, "Secret")
|
||||||
lu.assertEquals(testElement:getText(), "Secret")
|
lu.assertEquals(testElement:getText(), "Secret")
|
||||||
@@ -124,7 +129,7 @@ end
|
|||||||
function TestPasswordMode:testPasswordTextIsNotModified()
|
function TestPasswordMode:testPasswordTextIsNotModified()
|
||||||
-- Set initial text
|
-- Set initial text
|
||||||
testElement:setText("MyPassword123")
|
testElement:setText("MyPassword123")
|
||||||
|
|
||||||
-- The actual buffer should contain the real password
|
-- The actual buffer should contain the real password
|
||||||
lu.assertEquals(testElement._textBuffer, "MyPassword123")
|
lu.assertEquals(testElement._textBuffer, "MyPassword123")
|
||||||
lu.assertEquals(testElement:getText(), "MyPassword123")
|
lu.assertEquals(testElement:getText(), "MyPassword123")
|
||||||
@@ -137,15 +142,15 @@ end
|
|||||||
function TestPasswordMode:testCursorPositionWithPasswordMode()
|
function TestPasswordMode:testCursorPositionWithPasswordMode()
|
||||||
testElement:setText("test")
|
testElement:setText("test")
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
|
|
||||||
-- Set cursor to end
|
-- Set cursor to end
|
||||||
testElement:setCursorPosition(4)
|
testElement:setCursorPosition(4)
|
||||||
lu.assertEquals(testElement._cursorPosition, 4)
|
lu.assertEquals(testElement._cursorPosition, 4)
|
||||||
|
|
||||||
-- Move cursor to middle
|
-- Move cursor to middle
|
||||||
testElement:setCursorPosition(2)
|
testElement:setCursorPosition(2)
|
||||||
lu.assertEquals(testElement._cursorPosition, 2)
|
lu.assertEquals(testElement._cursorPosition, 2)
|
||||||
|
|
||||||
-- Move cursor to start
|
-- Move cursor to start
|
||||||
testElement:setCursorPosition(0)
|
testElement:setCursorPosition(0)
|
||||||
lu.assertEquals(testElement._cursorPosition, 0)
|
lu.assertEquals(testElement._cursorPosition, 0)
|
||||||
@@ -155,15 +160,15 @@ function TestPasswordMode:testCursorMovementInPasswordField()
|
|||||||
testElement:setText("password")
|
testElement:setText("password")
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
testElement:setCursorPosition(0)
|
testElement:setCursorPosition(0)
|
||||||
|
|
||||||
-- Move right
|
-- Move right
|
||||||
testElement:moveCursorBy(1)
|
testElement:moveCursorBy(1)
|
||||||
lu.assertEquals(testElement._cursorPosition, 1)
|
lu.assertEquals(testElement._cursorPosition, 1)
|
||||||
|
|
||||||
-- Move right again
|
-- Move right again
|
||||||
testElement:moveCursorBy(1)
|
testElement:moveCursorBy(1)
|
||||||
lu.assertEquals(testElement._cursorPosition, 2)
|
lu.assertEquals(testElement._cursorPosition, 2)
|
||||||
|
|
||||||
-- Move left
|
-- Move left
|
||||||
testElement:moveCursorBy(-1)
|
testElement:moveCursorBy(-1)
|
||||||
lu.assertEquals(testElement._cursorPosition, 1)
|
lu.assertEquals(testElement._cursorPosition, 1)
|
||||||
@@ -176,13 +181,13 @@ end
|
|||||||
function TestPasswordMode:testInsertTextInPasswordMode()
|
function TestPasswordMode:testInsertTextInPasswordMode()
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
testElement:setCursorPosition(0)
|
testElement:setCursorPosition(0)
|
||||||
|
|
||||||
testElement:insertText("a")
|
testElement:insertText("a")
|
||||||
lu.assertEquals(testElement._textBuffer, "a")
|
lu.assertEquals(testElement._textBuffer, "a")
|
||||||
|
|
||||||
testElement:insertText("b")
|
testElement:insertText("b")
|
||||||
lu.assertEquals(testElement._textBuffer, "ab")
|
lu.assertEquals(testElement._textBuffer, "ab")
|
||||||
|
|
||||||
testElement:insertText("c")
|
testElement:insertText("c")
|
||||||
lu.assertEquals(testElement._textBuffer, "abc")
|
lu.assertEquals(testElement._textBuffer, "abc")
|
||||||
end
|
end
|
||||||
@@ -190,12 +195,12 @@ end
|
|||||||
function TestPasswordMode:testBackspaceInPasswordMode()
|
function TestPasswordMode:testBackspaceInPasswordMode()
|
||||||
testElement:setText("password")
|
testElement:setText("password")
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
testElement:setCursorPosition(8) -- End of text
|
testElement:setCursorPosition(8) -- End of text
|
||||||
|
|
||||||
-- Delete last character
|
-- Delete last character
|
||||||
testElement:keypressed("backspace", nil, false)
|
testElement:keypressed("backspace", nil, false)
|
||||||
lu.assertEquals(testElement._textBuffer, "passwor")
|
lu.assertEquals(testElement._textBuffer, "passwor")
|
||||||
|
|
||||||
-- Delete another character
|
-- Delete another character
|
||||||
testElement:keypressed("backspace", nil, false)
|
testElement:keypressed("backspace", nil, false)
|
||||||
lu.assertEquals(testElement._textBuffer, "passwo")
|
lu.assertEquals(testElement._textBuffer, "passwo")
|
||||||
@@ -204,12 +209,12 @@ end
|
|||||||
function TestPasswordMode:testDeleteInPasswordMode()
|
function TestPasswordMode:testDeleteInPasswordMode()
|
||||||
testElement:setText("password")
|
testElement:setText("password")
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
testElement:setCursorPosition(0) -- Start of text
|
testElement:setCursorPosition(0) -- Start of text
|
||||||
|
|
||||||
-- Delete first character
|
-- Delete first character
|
||||||
testElement:keypressed("delete", nil, false)
|
testElement:keypressed("delete", nil, false)
|
||||||
lu.assertEquals(testElement._textBuffer, "assword")
|
lu.assertEquals(testElement._textBuffer, "assword")
|
||||||
|
|
||||||
-- Delete another character
|
-- Delete another character
|
||||||
testElement:keypressed("delete", nil, false)
|
testElement:keypressed("delete", nil, false)
|
||||||
lu.assertEquals(testElement._textBuffer, "ssword")
|
lu.assertEquals(testElement._textBuffer, "ssword")
|
||||||
@@ -218,8 +223,8 @@ end
|
|||||||
function TestPasswordMode:testInsertTextAtPosition()
|
function TestPasswordMode:testInsertTextAtPosition()
|
||||||
testElement:setText("pass")
|
testElement:setText("pass")
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
testElement:setCursorPosition(2) -- Between 'pa' and 'ss'
|
testElement:setCursorPosition(2) -- Between 'pa' and 'ss'
|
||||||
|
|
||||||
testElement:insertText("x")
|
testElement:insertText("x")
|
||||||
lu.assertEquals(testElement._textBuffer, "paxss")
|
lu.assertEquals(testElement._textBuffer, "paxss")
|
||||||
lu.assertEquals(testElement._cursorPosition, 3)
|
lu.assertEquals(testElement._cursorPosition, 3)
|
||||||
@@ -232,10 +237,10 @@ end
|
|||||||
function TestPasswordMode:testTextSelectionInPasswordMode()
|
function TestPasswordMode:testTextSelectionInPasswordMode()
|
||||||
testElement:setText("password")
|
testElement:setText("password")
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
|
|
||||||
-- Select from position 2 to 5
|
-- Select from position 2 to 5
|
||||||
testElement:setSelection(2, 5)
|
testElement:setSelection(2, 5)
|
||||||
|
|
||||||
local selStart, selEnd = testElement:getSelection()
|
local selStart, selEnd = testElement:getSelection()
|
||||||
lu.assertEquals(selStart, 2)
|
lu.assertEquals(selStart, 2)
|
||||||
lu.assertEquals(selEnd, 5)
|
lu.assertEquals(selEnd, 5)
|
||||||
@@ -245,10 +250,10 @@ end
|
|||||||
function TestPasswordMode:testDeleteSelectionInPasswordMode()
|
function TestPasswordMode:testDeleteSelectionInPasswordMode()
|
||||||
testElement:setText("password")
|
testElement:setText("password")
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
|
|
||||||
-- Select "sswo" (positions 2-6)
|
-- Select "sswo" (positions 2-6)
|
||||||
testElement:setSelection(2, 6)
|
testElement:setSelection(2, 6)
|
||||||
|
|
||||||
-- Delete selection
|
-- Delete selection
|
||||||
testElement:deleteSelection()
|
testElement:deleteSelection()
|
||||||
lu.assertEquals(testElement._textBuffer, "pard")
|
lu.assertEquals(testElement._textBuffer, "pard")
|
||||||
@@ -258,10 +263,10 @@ end
|
|||||||
function TestPasswordMode:testReplaceSelectionInPasswordMode()
|
function TestPasswordMode:testReplaceSelectionInPasswordMode()
|
||||||
testElement:setText("password")
|
testElement:setText("password")
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
|
|
||||||
-- Select "sswo" (positions 2-6)
|
-- Select "sswo" (positions 2-6)
|
||||||
testElement:setSelection(2, 6)
|
testElement:setSelection(2, 6)
|
||||||
|
|
||||||
-- Type new text (should replace selection)
|
-- Type new text (should replace selection)
|
||||||
testElement:textinput("X")
|
testElement:textinput("X")
|
||||||
lu.assertEquals(testElement._textBuffer, "paXrd")
|
lu.assertEquals(testElement._textBuffer, "paXrd")
|
||||||
@@ -270,9 +275,9 @@ end
|
|||||||
function TestPasswordMode:testSelectAllInPasswordMode()
|
function TestPasswordMode:testSelectAllInPasswordMode()
|
||||||
testElement:setText("secret")
|
testElement:setText("secret")
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
|
|
||||||
testElement:selectAll()
|
testElement:selectAll()
|
||||||
|
|
||||||
local selStart, selEnd = testElement:getSelection()
|
local selStart, selEnd = testElement:getSelection()
|
||||||
lu.assertEquals(selStart, 0)
|
lu.assertEquals(selStart, 0)
|
||||||
lu.assertEquals(selEnd, 6)
|
lu.assertEquals(selEnd, 6)
|
||||||
@@ -286,14 +291,14 @@ end
|
|||||||
function TestPasswordMode:testPasswordModeWithMaxLength()
|
function TestPasswordMode:testPasswordModeWithMaxLength()
|
||||||
testElement.maxLength = 5
|
testElement.maxLength = 5
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
|
|
||||||
testElement:insertText("1")
|
testElement:insertText("1")
|
||||||
testElement:insertText("2")
|
testElement:insertText("2")
|
||||||
testElement:insertText("3")
|
testElement:insertText("3")
|
||||||
testElement:insertText("4")
|
testElement:insertText("4")
|
||||||
testElement:insertText("5")
|
testElement:insertText("5")
|
||||||
testElement:insertText("6") -- Should be rejected
|
testElement:insertText("6") -- Should be rejected
|
||||||
|
|
||||||
lu.assertEquals(testElement._textBuffer, "12345")
|
lu.assertEquals(testElement._textBuffer, "12345")
|
||||||
lu.assertEquals(utf8.len(testElement._textBuffer), 5)
|
lu.assertEquals(utf8.len(testElement._textBuffer), 5)
|
||||||
end
|
end
|
||||||
@@ -309,11 +314,11 @@ function TestPasswordMode:testPasswordModeWithPlaceholder()
|
|||||||
placeholder = "Enter password",
|
placeholder = "Enter password",
|
||||||
text = "",
|
text = "",
|
||||||
})
|
})
|
||||||
|
|
||||||
-- When empty and not focused, placeholder should be available
|
-- When empty and not focused, placeholder should be available
|
||||||
lu.assertEquals(passwordWithPlaceholder.placeholder, "Enter password")
|
lu.assertEquals(passwordWithPlaceholder.placeholder, "Enter password")
|
||||||
lu.assertEquals(passwordWithPlaceholder._textBuffer, "")
|
lu.assertEquals(passwordWithPlaceholder._textBuffer, "")
|
||||||
|
|
||||||
-- When text is added, actual text should be stored
|
-- When text is added, actual text should be stored
|
||||||
passwordWithPlaceholder:focus()
|
passwordWithPlaceholder:focus()
|
||||||
passwordWithPlaceholder:insertText("secret")
|
passwordWithPlaceholder:insertText("secret")
|
||||||
@@ -323,7 +328,7 @@ end
|
|||||||
function TestPasswordMode:testPasswordModeClearText()
|
function TestPasswordMode:testPasswordModeClearText()
|
||||||
testElement:setText("password123")
|
testElement:setText("password123")
|
||||||
lu.assertEquals(testElement._textBuffer, "password123")
|
lu.assertEquals(testElement._textBuffer, "password123")
|
||||||
|
|
||||||
-- Clear text
|
-- Clear text
|
||||||
testElement:setText("")
|
testElement:setText("")
|
||||||
lu.assertEquals(testElement._textBuffer, "")
|
lu.assertEquals(testElement._textBuffer, "")
|
||||||
@@ -341,17 +346,17 @@ function TestPasswordMode:testPasswordModeToggle()
|
|||||||
passwordMode = false,
|
passwordMode = false,
|
||||||
text = "visible",
|
text = "visible",
|
||||||
})
|
})
|
||||||
|
|
||||||
lu.assertEquals(toggleElement._textBuffer, "visible")
|
lu.assertEquals(toggleElement._textBuffer, "visible")
|
||||||
lu.assertFalse(toggleElement.passwordMode)
|
lu.assertFalse(toggleElement.passwordMode)
|
||||||
|
|
||||||
-- Enable password mode
|
-- Enable password mode
|
||||||
toggleElement.passwordMode = true
|
toggleElement.passwordMode = true
|
||||||
lu.assertTrue(toggleElement.passwordMode)
|
lu.assertTrue(toggleElement.passwordMode)
|
||||||
|
|
||||||
-- Text buffer should remain unchanged
|
-- Text buffer should remain unchanged
|
||||||
lu.assertEquals(toggleElement._textBuffer, "visible")
|
lu.assertEquals(toggleElement._textBuffer, "visible")
|
||||||
|
|
||||||
-- Disable password mode again
|
-- Disable password mode again
|
||||||
toggleElement.passwordMode = false
|
toggleElement.passwordMode = false
|
||||||
lu.assertFalse(toggleElement.passwordMode)
|
lu.assertFalse(toggleElement.passwordMode)
|
||||||
@@ -364,14 +369,14 @@ end
|
|||||||
|
|
||||||
function TestPasswordMode:testPasswordModeWithUTF8Characters()
|
function TestPasswordMode:testPasswordModeWithUTF8Characters()
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
|
|
||||||
-- Insert UTF-8 characters
|
-- Insert UTF-8 characters
|
||||||
testElement:insertText("h")
|
testElement:insertText("h")
|
||||||
testElement:insertText("é")
|
testElement:insertText("é")
|
||||||
testElement:insertText("l")
|
testElement:insertText("l")
|
||||||
testElement:insertText("l")
|
testElement:insertText("l")
|
||||||
testElement:insertText("ö")
|
testElement:insertText("ö")
|
||||||
|
|
||||||
-- Text buffer should contain actual UTF-8 text
|
-- Text buffer should contain actual UTF-8 text
|
||||||
lu.assertEquals(testElement._textBuffer, "héllö")
|
lu.assertEquals(testElement._textBuffer, "héllö")
|
||||||
lu.assertEquals(utf8.len(testElement._textBuffer), 5)
|
lu.assertEquals(utf8.len(testElement._textBuffer), 5)
|
||||||
@@ -380,17 +385,17 @@ end
|
|||||||
function TestPasswordMode:testPasswordModeCursorWithUTF8()
|
function TestPasswordMode:testPasswordModeCursorWithUTF8()
|
||||||
testElement:setText("café")
|
testElement:setText("café")
|
||||||
testElement:focus()
|
testElement:focus()
|
||||||
|
|
||||||
-- Move cursor through UTF-8 text
|
-- Move cursor through UTF-8 text
|
||||||
testElement:setCursorPosition(0)
|
testElement:setCursorPosition(0)
|
||||||
lu.assertEquals(testElement._cursorPosition, 0)
|
lu.assertEquals(testElement._cursorPosition, 0)
|
||||||
|
|
||||||
testElement:moveCursorBy(1)
|
testElement:moveCursorBy(1)
|
||||||
lu.assertEquals(testElement._cursorPosition, 1)
|
lu.assertEquals(testElement._cursorPosition, 1)
|
||||||
|
|
||||||
testElement:moveCursorBy(1)
|
testElement:moveCursorBy(1)
|
||||||
lu.assertEquals(testElement._cursorPosition, 2)
|
lu.assertEquals(testElement._cursorPosition, 2)
|
||||||
|
|
||||||
testElement:setCursorPosition(4)
|
testElement:setCursorPosition(4)
|
||||||
lu.assertEquals(testElement._cursorPosition, 4)
|
lu.assertEquals(testElement._cursorPosition, 4)
|
||||||
end
|
end
|
||||||
@@ -414,7 +419,7 @@ end
|
|||||||
function TestPasswordMode:testPasswordModeWithLongPassword()
|
function TestPasswordMode:testPasswordModeWithLongPassword()
|
||||||
local longPassword = string.rep("a", 100)
|
local longPassword = string.rep("a", 100)
|
||||||
testElement:setText(longPassword)
|
testElement:setText(longPassword)
|
||||||
|
|
||||||
lu.assertEquals(testElement._textBuffer, longPassword)
|
lu.assertEquals(testElement._textBuffer, longPassword)
|
||||||
lu.assertEquals(utf8.len(testElement._textBuffer), 100)
|
lu.assertEquals(utf8.len(testElement._textBuffer), 100)
|
||||||
end
|
end
|
||||||
@@ -422,17 +427,12 @@ end
|
|||||||
function TestPasswordMode:testPasswordModeSetTextUpdatesBuffer()
|
function TestPasswordMode:testPasswordModeSetTextUpdatesBuffer()
|
||||||
testElement:setText("initial")
|
testElement:setText("initial")
|
||||||
lu.assertEquals(testElement._textBuffer, "initial")
|
lu.assertEquals(testElement._textBuffer, "initial")
|
||||||
|
|
||||||
testElement:setText("updated")
|
testElement:setText("updated")
|
||||||
lu.assertEquals(testElement._textBuffer, "updated")
|
lu.assertEquals(testElement._textBuffer, "updated")
|
||||||
|
|
||||||
testElement:setText("")
|
testElement:setText("")
|
||||||
lu.assertEquals(testElement._textBuffer, "")
|
lu.assertEquals(testElement._textBuffer, "")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Run tests if executed directly
|
lu.LuaUnit.run()
|
||||||
if arg and arg[0] and arg[0]:match("34_password_mode_tests%.lua$") then
|
|
||||||
os.exit(lu.LuaUnit.run())
|
|
||||||
end
|
|
||||||
|
|
||||||
return TestPasswordMode
|
|
||||||
|
|||||||
423
testing/__tests__/35_stable_id_generation_tests.lua
Normal file
423
testing/__tests__/35_stable_id_generation_tests.lua
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
-- Test: Stable ID Generation in Immediate Mode
|
||||||
|
package.path = package.path .. ";./?.lua;./game/?.lua;./game/utils/?.lua;./game/components/?.lua;./game/systems/?.lua"
|
||||||
|
|
||||||
|
local luaunit = require("testing.luaunit")
|
||||||
|
require("testing.loveStub") -- Required to mock LOVE functions
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
|
local Gui = FlexLove.Gui
|
||||||
|
|
||||||
|
TestStableIDGeneration = {}
|
||||||
|
|
||||||
|
function TestStableIDGeneration:setUp()
|
||||||
|
-- Reset GUI state
|
||||||
|
if Gui.destroy then
|
||||||
|
Gui.destroy()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initialize with immediate mode enabled
|
||||||
|
Gui.init({
|
||||||
|
baseScale = { width = 1920, height = 1080 },
|
||||||
|
immediateMode = true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStableIDGeneration:tearDown()
|
||||||
|
-- Clear all states
|
||||||
|
if Gui.clearAllStates then
|
||||||
|
Gui.clearAllStates()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reset immediate mode state
|
||||||
|
if Gui._immediateModeState then
|
||||||
|
Gui._immediateModeState.reset()
|
||||||
|
end
|
||||||
|
|
||||||
|
if Gui.destroy then
|
||||||
|
Gui.destroy()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reset immediate mode flag
|
||||||
|
Gui._immediateMode = false
|
||||||
|
Gui._frameNumber = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStableIDGeneration:test_child_ids_stable_across_frames()
|
||||||
|
-- Frame 1: Create parent with children
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local parent = Gui.new({
|
||||||
|
id = "test_parent",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1 = Gui.new({
|
||||||
|
parent = parent,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Child 1",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child2 = Gui.new({
|
||||||
|
parent = parent,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Child 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1Id = child1.id
|
||||||
|
local child2Id = child2.id
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Frame 2: Recreate same structure
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local parent2 = Gui.new({
|
||||||
|
id = "test_parent",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1_2 = Gui.new({
|
||||||
|
parent = parent2,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Child 1",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child2_2 = Gui.new({
|
||||||
|
parent = parent2,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Child 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- IDs should be stable
|
||||||
|
luaunit.assertEquals(child1_2.id, child1Id, "Child 1 ID should be stable across frames")
|
||||||
|
luaunit.assertEquals(child2_2.id, child2Id, "Child 2 ID should be stable across frames")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStableIDGeneration:test_conditional_rendering_does_not_affect_siblings()
|
||||||
|
-- Frame 1: Create parent with 3 children
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local parent1 = Gui.new({
|
||||||
|
id = "test_parent2",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1 = Gui.new({
|
||||||
|
parent = parent1,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Child 1",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child2 = Gui.new({
|
||||||
|
parent = parent1,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Child 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child3 = Gui.new({
|
||||||
|
parent = parent1,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Child 3",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1Id = child1.id
|
||||||
|
local child3Id = child3.id
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Frame 2: Skip child 2 (conditional rendering)
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local parent2 = Gui.new({
|
||||||
|
id = "test_parent2",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1_2 = Gui.new({
|
||||||
|
parent = parent2,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Child 1",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Child 2 not rendered this frame
|
||||||
|
|
||||||
|
local child3_2 = Gui.new({
|
||||||
|
parent = parent2,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Child 3",
|
||||||
|
})
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Child 1 should keep its ID
|
||||||
|
luaunit.assertEquals(child1_2.id, child1Id, "Child 1 ID should remain stable")
|
||||||
|
|
||||||
|
-- Child 3 will have a different ID because it's now at sibling index 1 instead of 2
|
||||||
|
-- This is EXPECTED behavior - the position in the tree changed
|
||||||
|
luaunit.assertNotEquals(child3_2.id, child3Id, "Child 3 ID changes because its sibling position changed")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStableIDGeneration:test_input_field_maintains_state_across_frames()
|
||||||
|
-- Frame 1: Create input field and simulate text entry
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "test_container",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local input1 = Gui.new({
|
||||||
|
parent = container,
|
||||||
|
width = 200,
|
||||||
|
height = 40,
|
||||||
|
editable = true,
|
||||||
|
text = "",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Simulate text input
|
||||||
|
input1._textBuffer = "Hello World"
|
||||||
|
input1._focused = true
|
||||||
|
|
||||||
|
local inputId = input1.id
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Frame 2: Recreate same structure
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local container2 = Gui.new({
|
||||||
|
id = "test_container",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local input2 = Gui.new({
|
||||||
|
parent = container2,
|
||||||
|
width = 200,
|
||||||
|
height = 40,
|
||||||
|
editable = true,
|
||||||
|
text = "",
|
||||||
|
})
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Input should have same ID and restored state
|
||||||
|
luaunit.assertEquals(input2.id, inputId, "Input field ID should be stable")
|
||||||
|
luaunit.assertEquals(input2._textBuffer, "Hello World", "Input text should be restored")
|
||||||
|
luaunit.assertTrue(input2._focused, "Input focus state should be restored")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStableIDGeneration:test_nested_children_stable_ids()
|
||||||
|
-- Frame 1: Create nested hierarchy
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local root = Gui.new({
|
||||||
|
id = "test_root",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local level1 = Gui.new({
|
||||||
|
parent = root,
|
||||||
|
width = 300,
|
||||||
|
height = 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
local level2 = Gui.new({
|
||||||
|
parent = level1,
|
||||||
|
width = 200,
|
||||||
|
height = 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
local deepChild = Gui.new({
|
||||||
|
parent = level2,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Deep Child",
|
||||||
|
})
|
||||||
|
|
||||||
|
local deepChildId = deepChild.id
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Frame 2: Recreate same nested structure
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local root2 = Gui.new({
|
||||||
|
id = "test_root",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local level1_2 = Gui.new({
|
||||||
|
parent = root2,
|
||||||
|
width = 300,
|
||||||
|
height = 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
local level2_2 = Gui.new({
|
||||||
|
parent = level1_2,
|
||||||
|
width = 200,
|
||||||
|
height = 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
local deepChild2 = Gui.new({
|
||||||
|
parent = level2_2,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Deep Child",
|
||||||
|
})
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Deep child ID should be stable
|
||||||
|
luaunit.assertEquals(deepChild2.id, deepChildId, "Deeply nested child ID should be stable")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStableIDGeneration:test_siblings_with_different_props_have_different_ids()
|
||||||
|
-- Frame 1: Create siblings with different properties
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local parent = Gui.new({
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1 = Gui.new({
|
||||||
|
parent = parent,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Button 1",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child2 = Gui.new({
|
||||||
|
parent = parent,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
text = "Button 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Siblings should have different IDs due to different sibling indices and props
|
||||||
|
luaunit.assertNotEquals(child1.id, child2.id, "Siblings should have different IDs")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper function to create elements from consistent location (simulates real usage)
|
||||||
|
local function createTopLevelElements()
|
||||||
|
local elements = {}
|
||||||
|
for i = 1, 3 do
|
||||||
|
elements[i] = Gui.new({ width = 100, height = 50, text = "Element " .. i })
|
||||||
|
end
|
||||||
|
return elements
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStableIDGeneration:test_top_level_elements_use_call_site_counter()
|
||||||
|
-- Frame 1: Create multiple top-level elements at same location (in loop)
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local elements = createTopLevelElements()
|
||||||
|
|
||||||
|
local ids = {}
|
||||||
|
for i = 1, 3 do
|
||||||
|
ids[i] = elements[i].id
|
||||||
|
end
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Frame 2: Recreate same elements from SAME line (via helper)
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local elements2 = createTopLevelElements()
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- IDs should be stable for top-level elements when called from same location
|
||||||
|
for i = 1, 3 do
|
||||||
|
luaunit.assertEquals(elements2[i].id, ids[i], "Top-level element " .. i .. " ID should be stable")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestStableIDGeneration:test_mixed_conditional_and_stable_elements()
|
||||||
|
-- Simulate a real-world scenario: navigation with conditional screens
|
||||||
|
|
||||||
|
-- Frame 1: Screen A with input field
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local backdrop1 = Gui.new({
|
||||||
|
id = "backdrop",
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
})
|
||||||
|
|
||||||
|
local window1 = Gui.new({
|
||||||
|
parent = backdrop1,
|
||||||
|
width = "80%",
|
||||||
|
height = "80%",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Screen A content
|
||||||
|
local inputA = Gui.new({
|
||||||
|
parent = window1,
|
||||||
|
width = 200,
|
||||||
|
height = 40,
|
||||||
|
editable = true,
|
||||||
|
text = "Screen A Input",
|
||||||
|
})
|
||||||
|
|
||||||
|
inputA._textBuffer = "User typed this"
|
||||||
|
inputA._focused = true
|
||||||
|
|
||||||
|
local inputAId = inputA.id
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Frame 2: Same screen structure (user is still on Screen A)
|
||||||
|
Gui.beginFrame()
|
||||||
|
|
||||||
|
local backdrop2 = Gui.new({
|
||||||
|
id = "backdrop",
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
})
|
||||||
|
|
||||||
|
local window2 = Gui.new({
|
||||||
|
parent = backdrop2,
|
||||||
|
width = "80%",
|
||||||
|
height = "80%",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Screen A content (same position in tree)
|
||||||
|
local inputA2 = Gui.new({
|
||||||
|
parent = window2,
|
||||||
|
width = 200,
|
||||||
|
height = 40,
|
||||||
|
editable = true,
|
||||||
|
text = "Screen A Input",
|
||||||
|
})
|
||||||
|
|
||||||
|
Gui.endFrame()
|
||||||
|
|
||||||
|
-- Input field should maintain ID and state
|
||||||
|
luaunit.assertEquals(inputA2.id, inputAId, "Input field ID should be stable within same screen")
|
||||||
|
luaunit.assertEquals(inputA2._textBuffer, "User typed this", "Input text should be preserved")
|
||||||
|
luaunit.assertTrue(inputA2._focused, "Input focus should be preserved")
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.LuaUnit.run()
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package.path = package.path .. ";./?.lua;./game/?.lua;./game/utils/?.lua;./game/components/?.lua;./game/systems/?.lua"
|
package.path = package.path .. ";./?.lua;./game/?.lua;./game/utils/?.lua;./game/components/?.lua;./game/systems/?.lua"
|
||||||
|
|
||||||
|
-- Set global flag to prevent individual test files from running luaunit
|
||||||
|
_G.RUNNING_ALL_TESTS = true
|
||||||
|
|
||||||
local luaunit = require("testing.luaunit")
|
local luaunit = require("testing.luaunit")
|
||||||
|
|
||||||
-- Run all tests in the __tests__ directory
|
-- Run all tests in the __tests__ directory
|
||||||
@@ -38,6 +41,7 @@ local testFiles = {
|
|||||||
"testing/__tests__/32_state_manager_tests.lua",
|
"testing/__tests__/32_state_manager_tests.lua",
|
||||||
"testing/__tests__/33_input_field_tests.lua",
|
"testing/__tests__/33_input_field_tests.lua",
|
||||||
"testing/__tests__/34_password_mode_tests.lua",
|
"testing/__tests__/34_password_mode_tests.lua",
|
||||||
|
"testing/__tests__/35_stable_id_generation_tests.lua",
|
||||||
}
|
}
|
||||||
|
|
||||||
local success = true
|
local success = true
|
||||||
|
|||||||
Reference in New Issue
Block a user