better input field keyboard handling

This commit is contained in:
Michael Freno
2025-11-09 12:00:42 -05:00
parent 3690202f48
commit 694a2d0a2e
8 changed files with 1846 additions and 20 deletions

View File

@@ -11,7 +11,16 @@ local Gui = FlexLove.Gui
TestEventSystem = {}
function TestEventSystem:setUp()
-- Initialize GUI before each test
-- 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
@@ -27,6 +36,8 @@ function TestEventSystem:tearDown()
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
-- Test 1: Event object structure

View File

@@ -0,0 +1,367 @@
-- 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()

View File

@@ -5,11 +5,55 @@ 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", "lshift", "rshift") then
local mods = {}
if love.keyboard.isDown("lshift") then table.insert(mods, "lshift") end
if love.keyboard.isDown("rshift") then table.insert(mods, "rshift") end
if love.keyboard.isDown("lctrl") then table.insert(mods, "lctrl") end
if love.keyboard.isDown("rctrl") then table.insert(mods, "rctrl") end
if love.keyboard.isDown("lalt") then table.insert(mods, "lalt") end
if love.keyboard.isDown("ralt") then table.insert(mods, "ralt") end
if love.keyboard.isDown("lgui") then table.insert(mods, "lgui") end
if love.keyboard.isDown("rgui") then table.insert(mods, "rgui") end
print("WARNING: Modifiers down at setUp: " .. table.concat(mods, ", "))
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
@@ -313,6 +357,186 @@ function TestKeyboardInput:testEndKey()
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()
clearModifierKeys()
local input = Element.new({
width = 200,
height = 40,
editable = true,
text = "Hello World",
})
input:focus()
input:setCursorPosition(3) -- Middle of text
-- 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()
clearModifierKeys()
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()
clearModifierKeys()
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()
clearModifierKeys()
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,

View File

@@ -0,0 +1,733 @@
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()

View File

@@ -18,6 +18,16 @@ local testElement
TestInputField = {}
function TestInputField: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)
-- Reset FlexLove state
FlexLove.Gui.topElements = {}
FlexLove.Gui._focusedElement = nil
@@ -34,6 +44,16 @@ function TestInputField:setUp()
end
function TestInputField: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)
testElement = nil
FlexLove.Gui.topElements = {}
FlexLove.Gui._focusedElement = nil

View File

@@ -0,0 +1,438 @@
-- ====================
-- Password Mode Tests
-- ====================
-- Test suite for password mode functionality in FlexLove input fields
local lu = require("testing.luaunit")
local loveStub = require("testing.loveStub")
-- Setup LÖVE environment
_G.love = loveStub
-- Load FlexLove after setting up love stub
local FlexLove = require("FlexLove")
-- Test fixtures
local testElement
TestPasswordMode = {}
function TestPasswordMode: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)
-- Reset FlexLove state
FlexLove.Gui.topElements = {}
FlexLove.Gui._focusedElement = nil
-- Create a test password input element
testElement = FlexLove.Element.new({
x = 100,
y = 100,
width = 200,
height = 40,
editable = true,
passwordMode = true,
text = "",
})
end
function TestPasswordMode: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)
testElement = nil
FlexLove.Gui.topElements = {}
FlexLove.Gui._focusedElement = nil
end
-- ====================
-- Property Tests
-- ====================
function TestPasswordMode:testPasswordModePropertyExists()
-- Test that passwordMode property exists and can be set
lu.assertNotNil(testElement.passwordMode)
lu.assertTrue(testElement.passwordMode)
end
function TestPasswordMode:testPasswordModeDefaultIsFalse()
-- Test that passwordMode defaults to false
local normalElement = FlexLove.Element.new({
x = 100,
y = 100,
width = 200,
height = 40,
editable = true,
text = "Normal text",
})
lu.assertFalse(normalElement.passwordMode or false)
end
function TestPasswordMode:testPasswordModeIsSingleLineOnly()
-- Password mode should only work with single-line inputs
-- The constraint is enforced in Element.lua line 292-293
local multilinePassword = FlexLove.Element.new({
x = 100,
y = 100,
width = 200,
height = 100,
editable = true,
multiline = true,
passwordMode = true,
text = "Password",
})
-- Based on the constraint, multiline should be set to false
lu.assertFalse(multilinePassword.multiline)
end
-- ====================
-- Text Buffer Tests
-- ====================
function TestPasswordMode:testActualTextContentRemains()
-- Insert text into password field
testElement:focus()
testElement:insertText("S")
testElement:insertText("e")
testElement:insertText("c")
testElement:insertText("r")
testElement:insertText("e")
testElement:insertText("t")
-- Verify actual text buffer contains the real text
lu.assertEquals(testElement._textBuffer, "Secret")
lu.assertEquals(testElement:getText(), "Secret")
end
function TestPasswordMode:testPasswordTextIsNotModified()
-- Set initial text
testElement:setText("MyPassword123")
-- The actual buffer should contain the real password
lu.assertEquals(testElement._textBuffer, "MyPassword123")
lu.assertEquals(testElement:getText(), "MyPassword123")
end
-- ====================
-- Cursor Position Tests
-- ====================
function TestPasswordMode:testCursorPositionWithPasswordMode()
testElement:setText("test")
testElement:focus()
-- Set cursor to end
testElement:setCursorPosition(4)
lu.assertEquals(testElement._cursorPosition, 4)
-- Move cursor to middle
testElement:setCursorPosition(2)
lu.assertEquals(testElement._cursorPosition, 2)
-- Move cursor to start
testElement:setCursorPosition(0)
lu.assertEquals(testElement._cursorPosition, 0)
end
function TestPasswordMode:testCursorMovementInPasswordField()
testElement:setText("password")
testElement:focus()
testElement:setCursorPosition(0)
-- Move right
testElement:moveCursorBy(1)
lu.assertEquals(testElement._cursorPosition, 1)
-- Move right again
testElement:moveCursorBy(1)
lu.assertEquals(testElement._cursorPosition, 2)
-- Move left
testElement:moveCursorBy(-1)
lu.assertEquals(testElement._cursorPosition, 1)
end
-- ====================
-- Text Editing Tests
-- ====================
function TestPasswordMode:testInsertTextInPasswordMode()
testElement:focus()
testElement:setCursorPosition(0)
testElement:insertText("a")
lu.assertEquals(testElement._textBuffer, "a")
testElement:insertText("b")
lu.assertEquals(testElement._textBuffer, "ab")
testElement:insertText("c")
lu.assertEquals(testElement._textBuffer, "abc")
end
function TestPasswordMode:testBackspaceInPasswordMode()
testElement:setText("password")
testElement:focus()
testElement:setCursorPosition(8) -- End of text
-- Delete last character
testElement:keypressed("backspace", nil, false)
lu.assertEquals(testElement._textBuffer, "passwor")
-- Delete another character
testElement:keypressed("backspace", nil, false)
lu.assertEquals(testElement._textBuffer, "passwo")
end
function TestPasswordMode:testDeleteInPasswordMode()
testElement:setText("password")
testElement:focus()
testElement:setCursorPosition(0) -- Start of text
-- Delete first character
testElement:keypressed("delete", nil, false)
lu.assertEquals(testElement._textBuffer, "assword")
-- Delete another character
testElement:keypressed("delete", nil, false)
lu.assertEquals(testElement._textBuffer, "ssword")
end
function TestPasswordMode:testInsertTextAtPosition()
testElement:setText("pass")
testElement:focus()
testElement:setCursorPosition(2) -- Between 'pa' and 'ss'
testElement:insertText("x")
lu.assertEquals(testElement._textBuffer, "paxss")
lu.assertEquals(testElement._cursorPosition, 3)
end
-- ====================
-- Selection Tests
-- ====================
function TestPasswordMode:testTextSelectionInPasswordMode()
testElement:setText("password")
testElement:focus()
-- Select from position 2 to 5
testElement:setSelection(2, 5)
local selStart, selEnd = testElement:getSelection()
lu.assertEquals(selStart, 2)
lu.assertEquals(selEnd, 5)
lu.assertTrue(testElement:hasSelection())
end
function TestPasswordMode:testDeleteSelectionInPasswordMode()
testElement:setText("password")
testElement:focus()
-- Select "sswo" (positions 2-6)
testElement:setSelection(2, 6)
-- Delete selection
testElement:deleteSelection()
lu.assertEquals(testElement._textBuffer, "pard")
lu.assertFalse(testElement:hasSelection())
end
function TestPasswordMode:testReplaceSelectionInPasswordMode()
testElement:setText("password")
testElement:focus()
-- Select "sswo" (positions 2-6)
testElement:setSelection(2, 6)
-- Type new text (should replace selection)
testElement:textinput("X")
lu.assertEquals(testElement._textBuffer, "paXrd")
end
function TestPasswordMode:testSelectAllInPasswordMode()
testElement:setText("secret")
testElement:focus()
testElement:selectAll()
local selStart, selEnd = testElement:getSelection()
lu.assertEquals(selStart, 0)
lu.assertEquals(selEnd, 6)
lu.assertTrue(testElement:hasSelection())
end
-- ====================
-- Integration Tests
-- ====================
function TestPasswordMode:testPasswordModeWithMaxLength()
testElement.maxLength = 5
testElement:focus()
testElement:insertText("1")
testElement:insertText("2")
testElement:insertText("3")
testElement:insertText("4")
testElement:insertText("5")
testElement:insertText("6") -- Should be rejected
lu.assertEquals(testElement._textBuffer, "12345")
lu.assertEquals(utf8.len(testElement._textBuffer), 5)
end
function TestPasswordMode:testPasswordModeWithPlaceholder()
local passwordWithPlaceholder = FlexLove.Element.new({
x = 100,
y = 100,
width = 200,
height = 40,
editable = true,
passwordMode = true,
placeholder = "Enter password",
text = "",
})
-- When empty and not focused, placeholder should be available
lu.assertEquals(passwordWithPlaceholder.placeholder, "Enter password")
lu.assertEquals(passwordWithPlaceholder._textBuffer, "")
-- When text is added, actual text should be stored
passwordWithPlaceholder:focus()
passwordWithPlaceholder:insertText("secret")
lu.assertEquals(passwordWithPlaceholder._textBuffer, "secret")
end
function TestPasswordMode:testPasswordModeClearText()
testElement:setText("password123")
lu.assertEquals(testElement._textBuffer, "password123")
-- Clear text
testElement:setText("")
lu.assertEquals(testElement._textBuffer, "")
lu.assertEquals(testElement:getText(), "")
end
function TestPasswordMode:testPasswordModeToggle()
-- Start with password mode off
local toggleElement = FlexLove.Element.new({
x = 100,
y = 100,
width = 200,
height = 40,
editable = true,
passwordMode = false,
text = "visible",
})
lu.assertEquals(toggleElement._textBuffer, "visible")
lu.assertFalse(toggleElement.passwordMode)
-- Enable password mode
toggleElement.passwordMode = true
lu.assertTrue(toggleElement.passwordMode)
-- Text buffer should remain unchanged
lu.assertEquals(toggleElement._textBuffer, "visible")
-- Disable password mode again
toggleElement.passwordMode = false
lu.assertFalse(toggleElement.passwordMode)
lu.assertEquals(toggleElement._textBuffer, "visible")
end
-- ====================
-- UTF-8 Support Tests
-- ====================
function TestPasswordMode:testPasswordModeWithUTF8Characters()
testElement:focus()
-- Insert UTF-8 characters
testElement:insertText("h")
testElement:insertText("é")
testElement:insertText("l")
testElement:insertText("l")
testElement:insertText("ö")
-- Text buffer should contain actual UTF-8 text
lu.assertEquals(testElement._textBuffer, "héllö")
lu.assertEquals(utf8.len(testElement._textBuffer), 5)
end
function TestPasswordMode:testPasswordModeCursorWithUTF8()
testElement:setText("café")
testElement:focus()
-- Move cursor through UTF-8 text
testElement:setCursorPosition(0)
lu.assertEquals(testElement._cursorPosition, 0)
testElement:moveCursorBy(1)
lu.assertEquals(testElement._cursorPosition, 1)
testElement:moveCursorBy(1)
lu.assertEquals(testElement._cursorPosition, 2)
testElement:setCursorPosition(4)
lu.assertEquals(testElement._cursorPosition, 4)
end
-- ====================
-- Edge Cases
-- ====================
function TestPasswordMode:testPasswordModeWithEmptyString()
testElement:setText("")
lu.assertEquals(testElement._textBuffer, "")
lu.assertEquals(testElement:getText(), "")
end
function TestPasswordMode:testPasswordModeWithSingleCharacter()
testElement:setText("x")
lu.assertEquals(testElement._textBuffer, "x")
lu.assertEquals(utf8.len(testElement._textBuffer), 1)
end
function TestPasswordMode:testPasswordModeWithLongPassword()
local longPassword = string.rep("a", 100)
testElement:setText(longPassword)
lu.assertEquals(testElement._textBuffer, longPassword)
lu.assertEquals(utf8.len(testElement._textBuffer), 100)
end
function TestPasswordMode:testPasswordModeSetTextUpdatesBuffer()
testElement:setText("initial")
lu.assertEquals(testElement._textBuffer, "initial")
testElement:setText("updated")
lu.assertEquals(testElement._textBuffer, "updated")
testElement:setText("")
lu.assertEquals(testElement._textBuffer, "")
end
-- Run tests if executed directly
if arg and arg[0] and arg[0]:match("34_password_mode_tests%.lua$") then
os.exit(lu.LuaUnit.run())
end
return TestPasswordMode

View File

@@ -37,6 +37,7 @@ local testFiles = {
"testing/__tests__/31_immediate_mode_basic_tests.lua",
"testing/__tests__/32_state_manager_tests.lua",
"testing/__tests__/33_input_field_tests.lua",
"testing/__tests__/34_password_mode_tests.lua",
}
local success = true