diff --git a/modules/Element.lua b/modules/Element.lua index 670b07a..77d5458 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -2595,6 +2595,7 @@ function Element:draw(backdropCanvas) -- For editable elements, use _textBuffer; for non-editable, use text local displayText = self.editable and self._textBuffer or self.text local isPlaceholder = false + local isPasswordMasked = false -- Show placeholder if editable, empty, and not focused if self.editable and (not displayText or displayText == "") and self.placeholder and not self._focused then @@ -2602,6 +2603,13 @@ function Element:draw(backdropCanvas) isPlaceholder = true end + -- Apply password masking if enabled + if self.passwordMode and displayText and displayText ~= "" and not isPlaceholder then + local maskedText = string.rep("•", utf8.len(displayText)) + displayText = maskedText + isPasswordMasked = true + end + if displayText and displayText ~= "" then local textColor = isPlaceholder and Color.new(self.textColor.r * 0.5, self.textColor.g * 0.5, self.textColor.b * 0.5, self.textColor.a * 0.5) or self.textColor @@ -4661,13 +4669,19 @@ function Element:_getCursorScreenPosition() local text = self._textBuffer or "" local cursorPos = self._cursorPosition or 0 + -- Apply password masking for cursor position calculation + local textForMeasurement = text + if self.passwordMode and text ~= "" then + textForMeasurement = string.rep("•", utf8.len(text)) + end + -- For single-line text, calculate simple X position if not self.multiline then local cursorText = "" - if text ~= "" and cursorPos > 0 then - local byteOffset = utf8.offset(text, cursorPos + 1) + if textForMeasurement ~= "" and cursorPos > 0 then + local byteOffset = utf8.offset(textForMeasurement, cursorPos + 1) if byteOffset then - cursorText = text:sub(1, byteOffset - 1) + cursorText = textForMeasurement:sub(1, byteOffset - 1) end end return font:getWidth(cursorText), 0 @@ -4775,14 +4789,20 @@ function Element:_getSelectionRects(selStart, selEnd) local text = self._textBuffer or "" local rects = {} + -- Apply password masking for selection rectangle calculation + local textForMeasurement = text + if self.passwordMode and text ~= "" then + textForMeasurement = string.rep("•", utf8.len(text)) + end + -- For single-line text, calculate simple rectangle if not self.multiline then - local startByte = utf8.offset(text, selStart + 1) - local endByte = utf8.offset(text, selEnd + 1) + local startByte = utf8.offset(textForMeasurement, selStart + 1) + local endByte = utf8.offset(textForMeasurement, selEnd + 1) if startByte and endByte then - local beforeSelection = text:sub(1, startByte - 1) - local selectedText = text:sub(startByte, endByte - 1) + local beforeSelection = textForMeasurement:sub(1, startByte - 1) + local selectedText = textForMeasurement:sub(startByte, endByte - 1) local selX = font:getWidth(beforeSelection) local selWidth = font:getWidth(selectedText) local selY = 0 @@ -5278,32 +5298,44 @@ function Element:keypressed(key, scancode, isrepeat) -- Move cursor based on key if key == "left" then - if self:hasSelection() and not modifiers.shift then + if modifiers.super then + -- Cmd/Super+Left: Move to start + self:moveCursorToStart() + if not modifiers.shift then + self:clearSelection() + end + elseif modifiers.alt then + -- Alt+Left: Move to previous word + self:moveCursorToPreviousWord() + elseif self:hasSelection() and not modifiers.shift then -- Move to start of selection local startPos, _ = self:getSelection() self._cursorPosition = startPos self:clearSelection() - elseif ctrl then - -- Ctrl+Left: Move to previous word - self:moveCursorToPreviousWord() else self:moveCursorBy(-1) end elseif key == "right" then - if self:hasSelection() and not modifiers.shift then + if modifiers.super then + -- Cmd/Super+Right: Move to end + self:moveCursorToEnd() + if not modifiers.shift then + self:clearSelection() + end + elseif modifiers.alt then + -- Alt+Right: Move to next word + self:moveCursorToNextWord() + elseif self:hasSelection() and not modifiers.shift then -- Move to end of selection local _, endPos = self:getSelection() self._cursorPosition = endPos self:clearSelection() - elseif ctrl then - -- Ctrl+Right: Move to next word - self:moveCursorToNextWord() else self:moveCursorBy(1) end elseif key == "home" then - -- Move to line start (or document start for single-line) - if ctrl or not self.multiline then + -- Home: Move to start (or line start for multiline) + if not self.multiline then self:moveCursorToStart() else self:moveCursorToLineStart() @@ -5312,8 +5344,8 @@ function Element:keypressed(key, scancode, isrepeat) self:clearSelection() end elseif key == "end" then - -- Move to line end (or document end for single-line) - if ctrl or not self.multiline then + -- End: Move to end (or line end for multiline) + if not self.multiline then self:moveCursorToEnd() else self:moveCursorToLineEnd() diff --git a/testing/__tests__/16_event_system_tests.lua b/testing/__tests__/16_event_system_tests.lua index ec6b437..eae2c40 100644 --- a/testing/__tests__/16_event_system_tests.lua +++ b/testing/__tests__/16_event_system_tests.lua @@ -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 diff --git a/testing/__tests__/16_event_system_tests.lua.bak b/testing/__tests__/16_event_system_tests.lua.bak new file mode 100644 index 0000000..2246e5c --- /dev/null +++ b/testing/__tests__/16_event_system_tests.lua.bak @@ -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() diff --git a/testing/__tests__/24_keyboard_input_tests.lua b/testing/__tests__/24_keyboard_input_tests.lua index f072886..b22c855 100644 --- a/testing/__tests__/24_keyboard_input_tests.lua +++ b/testing/__tests__/24_keyboard_input_tests.lua @@ -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, diff --git a/testing/__tests__/24_keyboard_input_tests.lua.bak b/testing/__tests__/24_keyboard_input_tests.lua.bak new file mode 100644 index 0000000..99151ba --- /dev/null +++ b/testing/__tests__/24_keyboard_input_tests.lua.bak @@ -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() diff --git a/testing/__tests__/33_input_field_tests.lua b/testing/__tests__/33_input_field_tests.lua index f86d4d9..d2331b0 100644 --- a/testing/__tests__/33_input_field_tests.lua +++ b/testing/__tests__/33_input_field_tests.lua @@ -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 diff --git a/testing/__tests__/34_password_mode_tests.lua b/testing/__tests__/34_password_mode_tests.lua new file mode 100644 index 0000000..879632a --- /dev/null +++ b/testing/__tests__/34_password_mode_tests.lua @@ -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 diff --git a/testing/runAll.lua b/testing/runAll.lua index 8d0c816..834e85b 100644 --- a/testing/runAll.lua +++ b/testing/runAll.lua @@ -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