feat: gesture/multi-touch progress

This commit is contained in:
2026-02-25 10:17:16 -05:00
parent 998469141a
commit 309ebde985
11 changed files with 2283 additions and 11 deletions

View File

@@ -0,0 +1,328 @@
package.path = package.path .. ";./?.lua;./modules/?.lua"
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function()
return require("modules." .. moduleName)
end
end
end)
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local FlexLove = require("FlexLove")
FlexLove.init()
TestElementTouch = {}
function TestElementTouch:setUp()
FlexLove.setMode("immediate")
love.window.setMode(800, 600)
end
function TestElementTouch:tearDown()
FlexLove.destroy()
end
-- ============================================
-- Touch Property Tests
-- ============================================
function TestElementTouch:test_touchEnabled_defaults_true()
FlexLove.beginFrame()
local element = FlexLove.new({ width = 100, height = 100 })
FlexLove.endFrame()
luaunit.assertTrue(element.touchEnabled)
end
function TestElementTouch:test_touchEnabled_can_be_set_false()
FlexLove.beginFrame()
local element = FlexLove.new({ width = 100, height = 100, touchEnabled = false })
FlexLove.endFrame()
luaunit.assertFalse(element.touchEnabled)
end
function TestElementTouch:test_multiTouchEnabled_defaults_false()
FlexLove.beginFrame()
local element = FlexLove.new({ width = 100, height = 100 })
FlexLove.endFrame()
luaunit.assertFalse(element.multiTouchEnabled)
end
function TestElementTouch:test_multiTouchEnabled_can_be_set_true()
FlexLove.beginFrame()
local element = FlexLove.new({ width = 100, height = 100, multiTouchEnabled = true })
FlexLove.endFrame()
luaunit.assertTrue(element.multiTouchEnabled)
end
-- ============================================
-- Touch Callback Tests
-- ============================================
function TestElementTouch:test_onTouchEvent_callback()
FlexLove.beginFrame()
local receivedEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function(el, event)
table.insert(receivedEvents, event)
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("t1", 100, 100, 0, 0, 1.0)
luaunit.assertTrue(#receivedEvents >= 1)
luaunit.assertEquals(receivedEvents[1].type, "touchpress")
end
function TestElementTouch:test_onGesture_callback()
FlexLove.beginFrame()
local receivedGestures = {}
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function() end,
onGesture = function(el, gesture)
table.insert(receivedGestures, gesture)
end,
})
FlexLove.endFrame()
-- Quick tap
FlexLove.touchpressed("t1", 100, 100, 0, 0, 1.0)
love.timer.step(0.05)
FlexLove.touchreleased("t1", 100, 100, 0, 0, 1.0)
local tapGestures = {}
for _, g in ipairs(receivedGestures) do
if g.type == "tap" then
table.insert(tapGestures, g)
end
end
luaunit.assertTrue(#tapGestures >= 1, "Should receive tap gesture callback")
end
function TestElementTouch:test_onEvent_also_receives_touch()
FlexLove.beginFrame()
local receivedEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
onEvent = function(el, event)
table.insert(receivedEvents, event)
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("t1", 100, 100, 0, 0, 1.0)
local touchEvents = {}
for _, e in ipairs(receivedEvents) do
if e.type == "touchpress" then
table.insert(touchEvents, e)
end
end
luaunit.assertTrue(#touchEvents >= 1, "onEvent should receive touch events")
end
-- ============================================
-- handleTouchEvent direct tests
-- ============================================
function TestElementTouch:test_handleTouchEvent_disabled_element()
FlexLove.beginFrame()
local receivedEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
disabled = true,
onTouchEvent = function(el, event)
table.insert(receivedEvents, event)
end,
})
FlexLove.endFrame()
local InputEvent = package.loaded["modules.InputEvent"]
local touchEvent = InputEvent.fromTouch("t1", 100, 100, "began", 1.0)
element:handleTouchEvent(touchEvent)
luaunit.assertEquals(#receivedEvents, 0, "Disabled element should not receive touch events")
end
function TestElementTouch:test_handleTouchEvent_touchEnabled_false()
FlexLove.beginFrame()
local receivedEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
touchEnabled = false,
onTouchEvent = function(el, event)
table.insert(receivedEvents, event)
end,
})
FlexLove.endFrame()
local InputEvent = package.loaded["modules.InputEvent"]
local touchEvent = InputEvent.fromTouch("t1", 100, 100, "began", 1.0)
element:handleTouchEvent(touchEvent)
luaunit.assertEquals(#receivedEvents, 0, "touchEnabled=false should prevent events")
end
-- ============================================
-- handleGesture direct tests
-- ============================================
function TestElementTouch:test_handleGesture_fires_callback()
FlexLove.beginFrame()
local receivedGestures = {}
local element = FlexLove.new({
width = 200,
height = 200,
onGesture = function(el, gesture)
table.insert(receivedGestures, gesture)
end,
})
FlexLove.endFrame()
element:handleGesture({ type = "tap", state = "ended", x = 100, y = 100 })
luaunit.assertEquals(#receivedGestures, 1)
luaunit.assertEquals(receivedGestures[1].type, "tap")
end
function TestElementTouch:test_handleGesture_disabled_element()
FlexLove.beginFrame()
local receivedGestures = {}
local element = FlexLove.new({
width = 200,
height = 200,
disabled = true,
onGesture = function(el, gesture)
table.insert(receivedGestures, gesture)
end,
})
FlexLove.endFrame()
element:handleGesture({ type = "tap", state = "ended", x = 100, y = 100 })
luaunit.assertEquals(#receivedGestures, 0, "Disabled element should not receive gestures")
end
function TestElementTouch:test_handleGesture_touchEnabled_false()
FlexLove.beginFrame()
local receivedGestures = {}
local element = FlexLove.new({
width = 200,
height = 200,
touchEnabled = false,
onGesture = function(el, gesture)
table.insert(receivedGestures, gesture)
end,
})
FlexLove.endFrame()
element:handleGesture({ type = "tap", state = "ended", x = 100, y = 100 })
luaunit.assertEquals(#receivedGestures, 0, "touchEnabled=false should prevent gestures")
end
-- ============================================
-- getTouches tests
-- ============================================
function TestElementTouch:test_getTouches_returns_table()
FlexLove.beginFrame()
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function() end,
})
FlexLove.endFrame()
local touches = element:getTouches()
luaunit.assertEquals(type(touches), "table")
end
-- ============================================
-- Touch + Gesture combined lifecycle
-- ============================================
function TestElementTouch:test_touch_pan_lifecycle()
FlexLove.beginFrame()
local touchEvents = {}
local gestureEvents = {}
local element = FlexLove.new({
width = 400,
height = 400,
onTouchEvent = function(el, event)
table.insert(touchEvents, event)
end,
onGesture = function(el, gesture)
table.insert(gestureEvents, gesture)
end,
})
FlexLove.endFrame()
-- Simulate a pan gesture: press, move significantly, release
FlexLove.touchpressed("t1", 100, 100, 0, 0, 1.0)
love.timer.step(0.05)
FlexLove.touchmoved("t1", 150, 150, 50, 50, 1.0)
love.timer.step(0.05)
FlexLove.touchmoved("t1", 200, 200, 50, 50, 1.0)
love.timer.step(0.05)
FlexLove.touchreleased("t1", 200, 200, 0, 0, 1.0)
-- Should have received touch events
luaunit.assertTrue(#touchEvents >= 3, "Should receive press + move + release touch events")
-- Should have received pan gestures from GestureRecognizer
local panGestures = {}
for _, g in ipairs(gestureEvents) do
if g.type == "pan" then
table.insert(panGestures, g)
end
end
luaunit.assertTrue(#panGestures >= 1, "Should receive pan gesture events")
end
-- ============================================
-- Deferred callbacks
-- ============================================
function TestElementTouch:test_onTouchEventDeferred_prop_accepted()
FlexLove.beginFrame()
-- Just test that the prop is accepted without error
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEventDeferred = function() end,
})
FlexLove.endFrame()
luaunit.assertNotNil(element, "Element with onTouchEventDeferred should be created")
end
function TestElementTouch:test_onGestureDeferred_prop_accepted()
FlexLove.beginFrame()
local element = FlexLove.new({
width = 200,
height = 200,
onGestureDeferred = function() end,
})
FlexLove.endFrame()
luaunit.assertNotNil(element, "Element with onGestureDeferred should be created")
end
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -0,0 +1,533 @@
package.path = package.path .. ";./?.lua;./modules/?.lua"
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function()
return require("modules." .. moduleName)
end
end
end)
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local FlexLove = require("FlexLove")
FlexLove.init()
local InputEvent = package.loaded["modules.InputEvent"]
local GestureRecognizer = package.loaded["modules.GestureRecognizer"]
TestGestureRecognizer = {}
function TestGestureRecognizer:setUp()
self.recognizer = GestureRecognizer.new({}, { InputEvent = InputEvent, utils = {} })
love.timer.setTime(0)
end
function TestGestureRecognizer:tearDown()
self.recognizer:reset()
end
-- Helper: create touch event
local function touchEvent(id, x, y, phase, time)
if time then love.timer.setTime(time) end
local event = InputEvent.fromTouch(id, x, y, phase, 1.0)
return event
end
-- ============================================
-- Tap Gesture Tests
-- ============================================
function TestGestureRecognizer:test_tap_detected()
local event1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(event1)
local event2 = touchEvent("t1", 100, 100, "ended", 0.1)
local gestures = self.recognizer:processTouchEvent(event2)
luaunit.assertNotNil(gestures, "Tap gesture should be detected")
luaunit.assertEquals(gestures[1].type, "tap")
luaunit.assertEquals(gestures[1].state, "ended")
luaunit.assertEquals(gestures[1].x, 100)
luaunit.assertEquals(gestures[1].y, 100)
end
function TestGestureRecognizer:test_tap_not_detected_when_too_slow()
local event1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(event1)
-- Release after tapMaxDuration (0.3s)
local event2 = touchEvent("t1", 100, 100, "ended", 0.5)
local gestures = self.recognizer:processTouchEvent(event2)
-- Should not be a tap (too slow)
if gestures then
for _, g in ipairs(gestures) do
luaunit.assertNotEquals(g.type, "tap", "Slow touch should not be tap")
end
end
end
function TestGestureRecognizer:test_tap_not_detected_when_moved_too_far()
local event1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(event1)
-- Move more than tapMaxMovement (10px)
local event2 = touchEvent("t1", 120, 120, "moved", 0.05)
self.recognizer:processTouchEvent(event2)
local event3 = touchEvent("t1", 120, 120, "ended", 0.1)
local gestures = self.recognizer:processTouchEvent(event3)
-- Should not be a tap (moved too far)
if gestures then
for _, g in ipairs(gestures) do
if g.type == "tap" then
-- Tap detection checks distance from START to END position
local dx = 120 - 100
local dy = 120 - 100
local dist = math.sqrt(dx*dx + dy*dy)
luaunit.assertTrue(dist >= 10, "Movement should exceed tap threshold")
end
end
end
end
function TestGestureRecognizer:test_double_tap_detected()
-- First tap
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t1", 100, 100, "ended", 0.05)
self.recognizer:processTouchEvent(e2)
-- Second tap quickly
local e3 = touchEvent("t2", 100, 100, "began", 0.15)
self.recognizer:processTouchEvent(e3)
local e4 = touchEvent("t2", 100, 100, "ended", 0.2)
local gestures = self.recognizer:processTouchEvent(e4)
luaunit.assertNotNil(gestures, "Should detect gesture on second tap")
-- Should have double_tap
local foundDoubleTap = false
for _, g in ipairs(gestures) do
if g.type == "double_tap" then
foundDoubleTap = true
end
end
luaunit.assertTrue(foundDoubleTap, "Should detect double-tap gesture")
end
function TestGestureRecognizer:test_double_tap_not_detected_when_too_slow()
-- First tap
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t1", 100, 100, "ended", 0.05)
self.recognizer:processTouchEvent(e2)
-- Second tap too late (>0.3s interval)
local e3 = touchEvent("t2", 100, 100, "began", 0.5)
self.recognizer:processTouchEvent(e3)
local e4 = touchEvent("t2", 100, 100, "ended", 0.55)
local gestures = self.recognizer:processTouchEvent(e4)
-- Should detect tap but NOT double_tap
if gestures then
for _, g in ipairs(gestures) do
luaunit.assertNotEquals(g.type, "double_tap", "Too-slow second tap should not be double-tap")
end
end
end
-- ============================================
-- Pan Gesture Tests
-- ============================================
function TestGestureRecognizer:test_pan_began()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
-- Move beyond panMinMovement (5px)
local e2 = touchEvent("t1", 110, 110, "moved", 0.05)
local gestures = self.recognizer:processTouchEvent(e2)
luaunit.assertNotNil(gestures, "Pan should be detected")
luaunit.assertEquals(gestures[1].type, "pan")
luaunit.assertEquals(gestures[1].state, "began")
end
function TestGestureRecognizer:test_pan_changed()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
-- First move to start pan
local e2 = touchEvent("t1", 110, 110, "moved", 0.05)
self.recognizer:processTouchEvent(e2)
-- Continue moving
local e3 = touchEvent("t1", 120, 120, "moved", 0.1)
local gestures = self.recognizer:processTouchEvent(e3)
luaunit.assertNotNil(gestures)
local panChanged = nil
for _, g in ipairs(gestures) do
if g.type == "pan" and g.state == "changed" then
panChanged = g
end
end
luaunit.assertNotNil(panChanged, "Should detect pan changed")
luaunit.assertEquals(panChanged.dx, 20) -- delta from startX=100 (lastX set to startX on began)
luaunit.assertEquals(panChanged.dy, 20)
end
function TestGestureRecognizer:test_pan_ended()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t1", 110, 110, "moved", 0.05)
self.recognizer:processTouchEvent(e2)
local e3 = touchEvent("t1", 120, 120, "ended", 0.1)
local gestures = self.recognizer:processTouchEvent(e3)
luaunit.assertNotNil(gestures)
local panEnded = nil
for _, g in ipairs(gestures) do
if g.type == "pan" and g.state == "ended" then
panEnded = g
end
end
luaunit.assertNotNil(panEnded, "Should detect pan ended")
end
function TestGestureRecognizer:test_pan_not_detected_with_small_movement()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
-- Move less than panMinMovement (5px)
local e2 = touchEvent("t1", 102, 102, "moved", 0.05)
local gestures = self.recognizer:processTouchEvent(e2)
luaunit.assertNil(gestures, "Small movement should not trigger pan")
end
-- ============================================
-- Swipe Gesture Tests
-- ============================================
function TestGestureRecognizer:test_swipe_right()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
-- Fast swipe right (>50px in <0.2s with >200px/s velocity)
local e2 = touchEvent("t1", 200, 100, "moved", 0.1)
self.recognizer:processTouchEvent(e2)
local e3 = touchEvent("t1", 200, 100, "ended", 0.1)
local gestures = self.recognizer:processTouchEvent(e3)
luaunit.assertNotNil(gestures)
local swipe = nil
for _, g in ipairs(gestures) do
if g.type == "swipe" then
swipe = g
end
end
luaunit.assertNotNil(swipe, "Should detect swipe")
luaunit.assertEquals(swipe.direction, "right")
end
function TestGestureRecognizer:test_swipe_left()
local e1 = touchEvent("t1", 200, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t1", 100, 100, "moved", 0.1)
self.recognizer:processTouchEvent(e2)
local e3 = touchEvent("t1", 100, 100, "ended", 0.1)
local gestures = self.recognizer:processTouchEvent(e3)
luaunit.assertNotNil(gestures)
local swipe = nil
for _, g in ipairs(gestures) do
if g.type == "swipe" then
swipe = g
end
end
luaunit.assertNotNil(swipe, "Should detect left swipe")
luaunit.assertEquals(swipe.direction, "left")
end
function TestGestureRecognizer:test_swipe_down()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t1", 100, 200, "moved", 0.1)
self.recognizer:processTouchEvent(e2)
local e3 = touchEvent("t1", 100, 200, "ended", 0.1)
local gestures = self.recognizer:processTouchEvent(e3)
luaunit.assertNotNil(gestures)
local swipe = nil
for _, g in ipairs(gestures) do
if g.type == "swipe" then
swipe = g
end
end
luaunit.assertNotNil(swipe, "Should detect down swipe")
luaunit.assertEquals(swipe.direction, "down")
end
function TestGestureRecognizer:test_swipe_up()
local e1 = touchEvent("t1", 100, 200, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t1", 100, 100, "moved", 0.1)
self.recognizer:processTouchEvent(e2)
local e3 = touchEvent("t1", 100, 100, "ended", 0.1)
local gestures = self.recognizer:processTouchEvent(e3)
luaunit.assertNotNil(gestures)
local swipe = nil
for _, g in ipairs(gestures) do
if g.type == "swipe" then
swipe = g
end
end
luaunit.assertNotNil(swipe, "Should detect up swipe")
luaunit.assertEquals(swipe.direction, "up")
end
function TestGestureRecognizer:test_swipe_not_detected_when_too_slow()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
-- Too slow (>0.2s)
local e2 = touchEvent("t1", 200, 100, "moved", 0.3)
self.recognizer:processTouchEvent(e2)
local e3 = touchEvent("t1", 200, 100, "ended", 0.3)
local gestures = self.recognizer:processTouchEvent(e3)
if gestures then
for _, g in ipairs(gestures) do
luaunit.assertNotEquals(g.type, "swipe", "Slow movement should not be a swipe")
end
end
end
function TestGestureRecognizer:test_swipe_not_detected_when_too_short()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
-- Too short distance (<50px)
local e2 = touchEvent("t1", 130, 100, "moved", 0.05)
self.recognizer:processTouchEvent(e2)
local e3 = touchEvent("t1", 130, 100, "ended", 0.05)
local gestures = self.recognizer:processTouchEvent(e3)
if gestures then
for _, g in ipairs(gestures) do
luaunit.assertNotEquals(g.type, "swipe", "Short movement should not be a swipe")
end
end
end
-- ============================================
-- Pinch Gesture Tests
-- ============================================
function TestGestureRecognizer:test_pinch_detected()
-- Two fingers start 100px apart
local e1 = touchEvent("t1", 100, 200, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t2", 200, 200, "began", 0)
self.recognizer:processTouchEvent(e2)
-- Move fingers apart to 200px (scale = 2.0)
local e3 = touchEvent("t1", 50, 200, "moved", 0.1)
local gestures = self.recognizer:processTouchEvent(e3)
local e4 = touchEvent("t2", 250, 200, "moved", 0.1)
gestures = self.recognizer:processTouchEvent(e4)
luaunit.assertNotNil(gestures, "Pinch should be detected")
local pinch = nil
for _, g in ipairs(gestures) do
if g.type == "pinch" then
pinch = g
end
end
luaunit.assertNotNil(pinch, "Should detect pinch gesture")
luaunit.assertTrue(pinch.scale > 1.0, "Scale should be greater than 1.0 for spread")
end
function TestGestureRecognizer:test_pinch_scale_decreases()
-- Two fingers start 200px apart
local e1 = touchEvent("t1", 50, 200, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t2", 250, 200, "began", 0)
self.recognizer:processTouchEvent(e2)
-- Move fingers closer to 100px (scale = 0.5)
local e3 = touchEvent("t1", 100, 200, "moved", 0.1)
self.recognizer:processTouchEvent(e3)
local e4 = touchEvent("t2", 200, 200, "moved", 0.1)
local gestures = self.recognizer:processTouchEvent(e4)
if gestures then
local pinch = nil
for _, g in ipairs(gestures) do
if g.type == "pinch" then
pinch = g
end
end
if pinch then
luaunit.assertTrue(pinch.scale < 1.0, "Scale should be less than 1.0 for pinch")
end
end
end
function TestGestureRecognizer:test_pinch_not_detected_with_one_touch()
local e1 = touchEvent("t1", 100, 200, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t1", 150, 200, "moved", 0.1)
local gestures = self.recognizer:processTouchEvent(e2)
if gestures then
for _, g in ipairs(gestures) do
luaunit.assertNotEquals(g.type, "pinch", "Single touch should not trigger pinch")
end
end
end
-- ============================================
-- Rotate Gesture Tests
-- ============================================
function TestGestureRecognizer:test_rotate_detected()
-- Two fingers horizontally
local e1 = touchEvent("t1", 100, 200, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t2", 200, 200, "began", 0)
self.recognizer:processTouchEvent(e2)
-- Rotate: move t2 above t1 (significant angle change > 5 degrees)
local e3 = touchEvent("t2", 200, 150, "moved", 0.1)
local gestures = self.recognizer:processTouchEvent(e3)
if gestures then
local rotate = nil
for _, g in ipairs(gestures) do
if g.type == "rotate" then
rotate = g
end
end
if rotate then
luaunit.assertNotNil(rotate.rotation, "Rotate gesture should have rotation angle")
end
end
end
-- ============================================
-- processTouchEvent return value tests
-- ============================================
function TestGestureRecognizer:test_processTouchEvent_returns_nil_for_no_gesture()
local e1 = touchEvent("t1", 100, 100, "began", 0)
local gestures = self.recognizer:processTouchEvent(e1)
-- Press alone should not produce gesture
luaunit.assertNil(gestures, "Press alone should not produce gesture")
end
function TestGestureRecognizer:test_processTouchEvent_returns_gesture_array()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t1", 100, 100, "ended", 0.1)
local gestures = self.recognizer:processTouchEvent(e2)
luaunit.assertNotNil(gestures)
luaunit.assertTrue(#gestures >= 1, "Should return array with at least 1 gesture")
luaunit.assertEquals(type(gestures[1]), "table")
luaunit.assertNotNil(gestures[1].type)
end
function TestGestureRecognizer:test_processTouchEvent_ignores_no_touchId()
local event = { type = "touchpress", x = 100, y = 100 } -- No touchId
local gestures = self.recognizer:processTouchEvent(event)
luaunit.assertNil(gestures)
end
function TestGestureRecognizer:test_touchcancel_cleans_up()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
local e2 = touchEvent("t1", 100, 100, "cancelled", 0.1)
local gestures = self.recognizer:processTouchEvent(e2)
-- After cancel, no touches should remain
luaunit.assertEquals(self.recognizer:_getTouchCount(), 0)
end
-- ============================================
-- Reset Tests
-- ============================================
function TestGestureRecognizer:test_reset_clears_state()
local e1 = touchEvent("t1", 100, 100, "began", 0)
self.recognizer:processTouchEvent(e1)
luaunit.assertTrue(self.recognizer:_getTouchCount() > 0)
self.recognizer:reset()
luaunit.assertEquals(self.recognizer:_getTouchCount(), 0)
end
-- ============================================
-- Custom Configuration Tests
-- ============================================
function TestGestureRecognizer:test_custom_config_overrides_defaults()
local custom = GestureRecognizer.new({
tapMaxDuration = 1.0,
panMinMovement = 20,
}, { InputEvent = InputEvent, utils = {} })
luaunit.assertEquals(custom._config.tapMaxDuration, 1.0)
luaunit.assertEquals(custom._config.panMinMovement, 20)
-- Defaults for non-overridden values
luaunit.assertEquals(custom._config.swipeMinDistance, 50)
end
-- ============================================
-- GestureType and GestureState exports
-- ============================================
function TestGestureRecognizer:test_gesture_types_exported()
luaunit.assertEquals(GestureRecognizer.GestureType.TAP, "tap")
luaunit.assertEquals(GestureRecognizer.GestureType.DOUBLE_TAP, "double_tap")
luaunit.assertEquals(GestureRecognizer.GestureType.LONG_PRESS, "long_press")
luaunit.assertEquals(GestureRecognizer.GestureType.SWIPE, "swipe")
luaunit.assertEquals(GestureRecognizer.GestureType.PAN, "pan")
luaunit.assertEquals(GestureRecognizer.GestureType.PINCH, "pinch")
luaunit.assertEquals(GestureRecognizer.GestureType.ROTATE, "rotate")
end
function TestGestureRecognizer:test_gesture_states_exported()
luaunit.assertEquals(GestureRecognizer.GestureState.POSSIBLE, "possible")
luaunit.assertEquals(GestureRecognizer.GestureState.BEGAN, "began")
luaunit.assertEquals(GestureRecognizer.GestureState.CHANGED, "changed")
luaunit.assertEquals(GestureRecognizer.GestureState.ENDED, "ended")
luaunit.assertEquals(GestureRecognizer.GestureState.CANCELLED, "cancelled")
luaunit.assertEquals(GestureRecognizer.GestureState.FAILED, "failed")
end
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -230,6 +230,7 @@ function TestTouchEvents:testEventHandler_MultiTouch()
local element = FlexLove.new({
width = 200,
height = 200,
multiTouchEnabled = true,
onEvent = function(el, event)
table.insert(touchEvents, event)
end,

View File

@@ -0,0 +1,528 @@
package.path = package.path .. ";./?.lua;./modules/?.lua"
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function()
return require("modules." .. moduleName)
end
end
end)
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local FlexLove = require("FlexLove")
FlexLove.init()
TestTouchRouting = {}
function TestTouchRouting:setUp()
FlexLove.setMode("immediate")
love.window.setMode(800, 600)
end
function TestTouchRouting:tearDown()
FlexLove.destroy()
love.touch.getTouches = function() return {} end
love.touch.getPosition = function() return 0, 0 end
end
-- Test: touchpressed routes to element at position
function TestTouchRouting:test_touchpressed_routes_to_element()
FlexLove.beginFrame()
local touchEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function(el, event)
table.insert(touchEvents, event)
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertTrue(#touchEvents >= 1, "Should receive touchpress event")
luaunit.assertEquals(touchEvents[1].type, "touchpress")
luaunit.assertEquals(touchEvents[1].touchId, "touch1")
luaunit.assertEquals(touchEvents[1].x, 100)
luaunit.assertEquals(touchEvents[1].y, 100)
end
-- Test: touchmoved routes to owning element
function TestTouchRouting:test_touchmoved_routes_to_owner()
FlexLove.beginFrame()
local touchEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function(el, event)
table.insert(touchEvents, event)
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
FlexLove.touchmoved("touch1", 150, 150, 50, 50, 1.0)
-- Filter for move events
local moveEvents = {}
for _, e in ipairs(touchEvents) do
if e.type == "touchmove" then
table.insert(moveEvents, e)
end
end
luaunit.assertTrue(#moveEvents >= 1, "Should receive touchmove event")
luaunit.assertEquals(moveEvents[1].x, 150)
luaunit.assertEquals(moveEvents[1].y, 150)
end
-- Test: touchreleased routes to owning element and cleans up
function TestTouchRouting:test_touchreleased_routes_and_cleans_up()
FlexLove.beginFrame()
local touchEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function(el, event)
table.insert(touchEvents, event)
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertNotNil(FlexLove.getTouchOwner("touch1"), "Touch should be owned")
FlexLove.touchreleased("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertNil(FlexLove.getTouchOwner("touch1"), "Touch ownership should be cleaned up")
-- Filter for release events
local releaseEvents = {}
for _, e in ipairs(touchEvents) do
if e.type == "touchrelease" then
table.insert(releaseEvents, e)
end
end
luaunit.assertTrue(#releaseEvents >= 1, "Should receive touchrelease event")
end
-- Test: Touch ownership persists — move events route even outside element bounds
function TestTouchRouting:test_touch_ownership_persists_outside_bounds()
FlexLove.beginFrame()
local touchEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function(el, event)
table.insert(touchEvents, event)
end,
})
FlexLove.endFrame()
-- Press inside element
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
-- Move far outside element bounds
FlexLove.touchmoved("touch1", 500, 500, 400, 400, 1.0)
-- Should still receive the move event due to ownership
local moveEvents = {}
for _, e in ipairs(touchEvents) do
if e.type == "touchmove" then
table.insert(moveEvents, e)
end
end
luaunit.assertTrue(#moveEvents >= 1, "Move event should route to owner even outside bounds")
luaunit.assertEquals(moveEvents[1].x, 500)
luaunit.assertEquals(moveEvents[1].y, 500)
end
-- Test: Touch outside all elements creates no ownership
function TestTouchRouting:test_touch_outside_elements_no_ownership()
FlexLove.beginFrame()
local touchEvents = {}
local element = FlexLove.new({
width = 100,
height = 100,
onTouchEvent = function(el, event)
table.insert(touchEvents, event)
end,
})
FlexLove.endFrame()
-- Press outside element bounds
FlexLove.touchpressed("touch1", 500, 500, 0, 0, 1.0)
luaunit.assertNil(FlexLove.getTouchOwner("touch1"), "No element should own touch outside bounds")
luaunit.assertEquals(#touchEvents, 0, "No events should fire for touch outside bounds")
end
-- Test: Multiple touches route to different elements
function TestTouchRouting:test_multi_touch_different_elements()
FlexLove.beginFrame()
local events1 = {}
local events2 = {}
-- Two elements side by side (default row layout)
local container = FlexLove.new({
width = 400,
height = 200,
})
local element1 = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function(el, event)
table.insert(events1, event)
end,
parent = container,
})
local element2 = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function(el, event)
table.insert(events2, event)
end,
parent = container,
})
FlexLove.endFrame()
-- Touch element1 (at x=0..200, y=0..200)
FlexLove.touchpressed("touch1", 50, 100, 0, 0, 1.0)
-- Touch element2 (at x=200..400, y=0..200)
FlexLove.touchpressed("touch2", 300, 100, 0, 0, 1.0)
luaunit.assertTrue(#events1 >= 1, "Element1 should receive touch event")
luaunit.assertTrue(#events2 >= 1, "Element2 should receive touch event")
luaunit.assertEquals(events1[1].touchId, "touch1")
luaunit.assertEquals(events2[1].touchId, "touch2")
end
-- Test: Z-index ordering — higher z element receives touch
function TestTouchRouting:test_z_index_ordering()
FlexLove.beginFrame()
local eventsLow = {}
local eventsHigh = {}
-- Lower z element
local low = FlexLove.new({
width = 200,
height = 200,
z = 1,
onTouchEvent = function(el, event)
table.insert(eventsLow, event)
end,
})
-- Higher z element overlapping
local high = FlexLove.new({
width = 200,
height = 200,
z = 10,
onTouchEvent = function(el, event)
table.insert(eventsHigh, event)
end,
})
FlexLove.endFrame()
-- Touch overlapping area
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertTrue(#eventsHigh >= 1, "Higher z element should receive touch")
luaunit.assertEquals(#eventsLow, 0, "Lower z element should NOT receive touch")
end
-- Test: Disabled element does not receive touch
function TestTouchRouting:test_disabled_element_no_touch()
FlexLove.beginFrame()
local touchEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
disabled = true,
onTouchEvent = function(el, event)
table.insert(touchEvents, event)
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertEquals(#touchEvents, 0, "Disabled element should not receive touch events")
luaunit.assertNil(FlexLove.getTouchOwner("touch1"))
end
-- Test: getActiveTouchCount tracks active touches
function TestTouchRouting:test_getActiveTouchCount()
FlexLove.beginFrame()
local element = FlexLove.new({
width = 800,
height = 600,
onTouchEvent = function() end,
})
FlexLove.endFrame()
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 0)
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 1)
FlexLove.touchpressed("touch2", 200, 200, 0, 0, 1.0)
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 2)
FlexLove.touchreleased("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 1)
FlexLove.touchreleased("touch2", 200, 200, 0, 0, 1.0)
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 0)
end
-- Test: getTouchOwner returns correct element
function TestTouchRouting:test_getTouchOwner()
FlexLove.beginFrame()
local element = FlexLove.new({
id = "owner-test",
width = 200,
height = 200,
onTouchEvent = function() end,
})
FlexLove.endFrame()
luaunit.assertNil(FlexLove.getTouchOwner("touch1"))
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
local owner = FlexLove.getTouchOwner("touch1")
luaunit.assertNotNil(owner)
luaunit.assertEquals(owner.id, "owner-test")
end
-- Test: destroy() cleans up touch state
function TestTouchRouting:test_destroy_cleans_touch_state()
FlexLove.beginFrame()
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function() end,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 1)
FlexLove.destroy()
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 0)
end
-- Test: Touch routing with onEvent (not just onTouchEvent)
function TestTouchRouting:test_onEvent_receives_touch_events()
FlexLove.beginFrame()
local allEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
onEvent = function(el, event)
table.insert(allEvents, event)
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
-- onEvent should receive touch events via handleTouchEvent -> _invokeCallback
local touchPressEvents = {}
for _, e in ipairs(allEvents) do
if e.type == "touchpress" then
table.insert(touchPressEvents, e)
end
end
luaunit.assertTrue(#touchPressEvents >= 1, "onEvent should receive touchpress events")
end
-- Test: Touch routing with onGesture callback
function TestTouchRouting:test_gesture_routing()
FlexLove.beginFrame()
local gestureEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function() end,
onGesture = function(el, gesture)
table.insert(gestureEvents, gesture)
end,
})
FlexLove.endFrame()
-- Simulate a quick tap (press and release at same position within threshold)
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
-- Small time step to avoid zero dt
love.timer.step(0.05)
FlexLove.touchreleased("touch1", 100, 100, 0, 0, 1.0)
-- GestureRecognizer should detect a tap gesture
local tapGestures = {}
for _, g in ipairs(gestureEvents) do
if g.type == "tap" then
table.insert(tapGestures, g)
end
end
luaunit.assertTrue(#tapGestures >= 1, "Should detect tap gesture from press+release")
end
-- Test: touchpressed with no onTouchEvent but onGesture — should still find element
function TestTouchRouting:test_element_with_only_onGesture()
FlexLove.beginFrame()
local gestureEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
onGesture = function(el, gesture)
table.insert(gestureEvents, gesture)
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertNotNil(FlexLove.getTouchOwner("touch1"), "Element with onGesture should be found")
end
-- Test: touchEnabled=false prevents touch routing
function TestTouchRouting:test_touchEnabled_false_prevents_routing()
FlexLove.beginFrame()
local touchEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
touchEnabled = false,
onTouchEvent = function(el, event)
table.insert(touchEvents, event)
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertNil(FlexLove.getTouchOwner("touch1"), "touchEnabled=false should prevent ownership")
luaunit.assertEquals(#touchEvents, 0, "touchEnabled=false should prevent events")
end
-- Test: Complete touch lifecycle (press, move, release)
function TestTouchRouting:test_full_lifecycle()
FlexLove.beginFrame()
local phases = {}
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function(el, event)
table.insert(phases, event.type)
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
FlexLove.touchmoved("touch1", 110, 110, 10, 10, 1.0)
FlexLove.touchmoved("touch1", 120, 120, 10, 10, 1.0)
FlexLove.touchreleased("touch1", 120, 120, 0, 0, 1.0)
luaunit.assertEquals(phases[1], "touchpress")
luaunit.assertEquals(phases[2], "touchmove")
luaunit.assertEquals(phases[3], "touchmove")
luaunit.assertEquals(phases[4], "touchrelease")
luaunit.assertEquals(#phases, 4)
end
-- Test: Orphaned move/release with no owner (no crash)
function TestTouchRouting:test_orphaned_move_release_no_crash()
-- Move and release events with no prior press should not crash
FlexLove.touchmoved("ghost_touch", 100, 100, 0, 0, 1.0)
FlexLove.touchreleased("ghost_touch", 100, 100, 0, 0, 1.0)
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 0)
end
-- Test: Pressure value is passed through
function TestTouchRouting:test_pressure_passthrough()
FlexLove.beginFrame()
local receivedPressure = nil
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function(el, event)
if event.type == "touchpress" then
receivedPressure = event.pressure
end
end,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 0.75)
luaunit.assertAlmostEquals(receivedPressure, 0.75, 0.01)
end
-- Test: Retained mode touch routing
function TestTouchRouting:test_retained_mode_routing()
FlexLove.setMode("retained")
local touchEvents = {}
local element = FlexLove.new({
width = 200,
height = 200,
onTouchEvent = function(el, event)
table.insert(touchEvents, event)
end,
})
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertTrue(#touchEvents >= 1, "Touch routing should work in retained mode")
luaunit.assertEquals(touchEvents[1].type, "touchpress")
end
-- Test: Child element receives touch over parent
function TestTouchRouting:test_child_receives_touch_over_parent()
FlexLove.beginFrame()
local parentEvents = {}
local childEvents = {}
local parent = FlexLove.new({
width = 400,
height = 400,
onTouchEvent = function(el, event)
table.insert(parentEvents, event)
end,
})
local child = FlexLove.new({
width = 200,
height = 200,
z = 1, -- Ensure child has higher z than parent
onTouchEvent = function(el, event)
table.insert(childEvents, event)
end,
parent = parent,
})
FlexLove.endFrame()
-- Touch within child area (which is also within parent)
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
-- Child has explicit higher z, should receive touch
luaunit.assertTrue(#childEvents >= 1,
string.format("Child should receive touch (child=%d, parent=%d, topElements=%d)",
#childEvents, #parentEvents, #FlexLove.topElements))
end
-- Test: Element with no callbacks not found by touch routing
function TestTouchRouting:test_non_interactive_element_ignored()
FlexLove.beginFrame()
-- Element with no onEvent, onTouchEvent, or onGesture
local element = FlexLove.new({
width = 200,
height = 200,
})
FlexLove.endFrame()
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
luaunit.assertNil(FlexLove.getTouchOwner("touch1"), "Non-interactive element should not capture touch")
end
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -0,0 +1,475 @@
package.path = package.path .. ";./?.lua;./modules/?.lua"
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local ErrorHandler = require("modules.ErrorHandler")
-- Initialize ErrorHandler
ErrorHandler.init({})
local ScrollManager = require("modules.ScrollManager")
local Color = require("modules.Color")
local utils = require("modules.utils")
-- Initialize ScrollManager with ErrorHandler
ScrollManager.init({ ErrorHandler = ErrorHandler })
-- Helper to create ScrollManager with touch config
local function createTouchScrollManager(config)
config = config or {}
config.overflow = config.overflow or "scroll"
return ScrollManager.new(config, {
Color = Color,
utils = utils,
})
end
-- Helper to create mock element with content taller than container
local function createMockElement(width, height, contentWidth, contentHeight)
local children = {}
-- Create a single child that represents all content
table.insert(children, {
x = 0,
y = 0,
width = contentWidth or 200,
height = contentHeight or 600,
margin = { top = 0, right = 0, bottom = 0, left = 0 },
_explicitlyAbsolute = false,
getBorderBoxWidth = function(self) return self.width end,
getBorderBoxHeight = function(self) return self.height end,
})
return {
x = 0,
y = 0,
width = width or 200,
height = height or 300,
padding = { top = 0, right = 0, bottom = 0, left = 0 },
children = children,
getBorderBoxWidth = function(self) return self.width end,
getBorderBoxHeight = function(self) return self.height end,
}
end
-- ============================================================================
-- Test Suite: Touch Press
-- ============================================================================
TestTouchScrollPress = {}
function TestTouchScrollPress:setUp()
love.timer.setTime(0)
end
function TestTouchScrollPress:test_handleTouchPress_starts_scrolling()
local sm = createTouchScrollManager()
local el = createMockElement()
sm:detectOverflow(el)
local started = sm:handleTouchPress(100, 150)
luaunit.assertTrue(started)
luaunit.assertTrue(sm:isTouchScrolling())
end
function TestTouchScrollPress:test_handleTouchPress_disabled_returns_false()
local sm = createTouchScrollManager({ touchScrollEnabled = false })
local el = createMockElement()
sm:detectOverflow(el)
local started = sm:handleTouchPress(100, 150)
luaunit.assertFalse(started)
luaunit.assertFalse(sm:isTouchScrolling())
end
function TestTouchScrollPress:test_handleTouchPress_no_overflow_returns_false()
local sm = createTouchScrollManager({ overflow = "hidden" })
local started = sm:handleTouchPress(100, 150)
luaunit.assertFalse(started)
end
function TestTouchScrollPress:test_handleTouchPress_stops_momentum_scrolling()
local sm = createTouchScrollManager()
local el = createMockElement()
sm:detectOverflow(el)
-- Simulate momentum by starting touch, moving fast, releasing
sm:handleTouchPress(100, 200)
-- Manually set momentum state
sm._momentumScrolling = true
sm._scrollVelocityY = 500
-- New press should stop momentum
sm:handleTouchPress(100, 200)
luaunit.assertFalse(sm:isMomentumScrolling())
luaunit.assertEquals(sm._scrollVelocityX, 0)
luaunit.assertEquals(sm._scrollVelocityY, 0)
end
-- ============================================================================
-- Test Suite: Touch Move
-- ============================================================================
TestTouchScrollMove = {}
function TestTouchScrollMove:setUp()
love.timer.setTime(0)
end
function TestTouchScrollMove:test_handleTouchMove_scrolls_content()
local sm = createTouchScrollManager({ bounceEnabled = false })
local el = createMockElement()
sm:detectOverflow(el)
sm:handleTouchPress(100, 200)
-- Advance time so dt > 0 in handleTouchMove
love.timer.step(1 / 60)
local handled = sm:handleTouchMove(100, 150)
luaunit.assertTrue(handled)
-- Touch moved UP by 50px, so scroll should increase (content moves down relative to finger)
luaunit.assertTrue(sm._scrollY > 0)
end
function TestTouchScrollMove:test_handleTouchMove_without_press_returns_false()
local sm = createTouchScrollManager()
local handled = sm:handleTouchMove(100, 150)
luaunit.assertFalse(handled)
end
function TestTouchScrollMove:test_handleTouchMove_calculates_velocity()
local sm = createTouchScrollManager({ bounceEnabled = false })
local el = createMockElement()
sm:detectOverflow(el)
sm:handleTouchPress(100, 200)
love.timer.step(1 / 60)
sm:handleTouchMove(100, 100) -- Move 100px up
-- Velocity should be set (non-zero since time elapsed)
-- Note: velocity direction is inverted (touch up = scroll down = positive velocity)
luaunit.assertTrue(sm._scrollVelocityY > 0)
end
function TestTouchScrollMove:test_handleTouchMove_horizontal()
local sm = createTouchScrollManager({
bounceEnabled = false,
overflowX = "scroll",
overflowY = "hidden",
})
local el = createMockElement(200, 300, 600, 300) -- Wide content
sm:detectOverflow(el)
sm:handleTouchPress(200, 150)
love.timer.step(1 / 60)
sm:handleTouchMove(100, 150) -- Move 100px left
luaunit.assertTrue(sm._scrollX > 0)
end
function TestTouchScrollMove:test_handleTouchMove_with_bounce_allows_overscroll()
local sm = createTouchScrollManager({ bounceEnabled = true, maxOverscroll = 100 })
local el = createMockElement()
sm:detectOverflow(el)
-- Scroll is at 0 (top), try to scroll further up (negative)
sm:handleTouchPress(100, 100)
love.timer.step(1 / 60)
sm:handleTouchMove(100, 200) -- Move down = scroll up (negative)
-- With bounce, overscroll should be allowed (scroll < 0)
luaunit.assertTrue(sm._scrollY < 0)
end
-- ============================================================================
-- Test Suite: Touch Release and Momentum
-- ============================================================================
TestTouchScrollRelease = {}
function TestTouchScrollRelease:setUp()
love.timer.setTime(0)
end
function TestTouchScrollRelease:test_handleTouchRelease_ends_touch_scrolling()
local sm = createTouchScrollManager()
local el = createMockElement()
sm:detectOverflow(el)
sm:handleTouchPress(100, 200)
luaunit.assertTrue(sm:isTouchScrolling())
sm:handleTouchRelease()
luaunit.assertFalse(sm:isTouchScrolling())
end
function TestTouchScrollRelease:test_handleTouchRelease_without_press_returns_false()
local sm = createTouchScrollManager()
local released = sm:handleTouchRelease()
luaunit.assertFalse(released)
end
function TestTouchScrollRelease:test_handleTouchRelease_starts_momentum_with_velocity()
local sm = createTouchScrollManager({ bounceEnabled = false })
local el = createMockElement()
sm:detectOverflow(el)
sm:handleTouchPress(100, 200)
love.timer.step(1 / 60)
sm:handleTouchMove(100, 50) -- Fast swipe up
sm:handleTouchRelease()
-- Should start momentum scrolling due to high velocity
luaunit.assertTrue(sm:isMomentumScrolling())
end
function TestTouchScrollRelease:test_handleTouchRelease_no_momentum_with_low_velocity()
local sm = createTouchScrollManager()
local el = createMockElement()
sm:detectOverflow(el)
sm:handleTouchPress(100, 200)
-- Simulate a very slow move by setting low velocity manually
sm._scrollVelocityX = 0
sm._scrollVelocityY = 10 -- Below threshold of 50
sm:handleTouchRelease()
luaunit.assertFalse(sm:isMomentumScrolling())
end
function TestTouchScrollRelease:test_handleTouchRelease_no_momentum_when_disabled()
local sm = createTouchScrollManager({ momentumScrollEnabled = false })
local el = createMockElement()
sm:detectOverflow(el)
sm:handleTouchPress(100, 200)
sm._scrollVelocityY = 500
sm:handleTouchRelease()
luaunit.assertFalse(sm:isMomentumScrolling())
luaunit.assertEquals(sm._scrollVelocityX, 0)
luaunit.assertEquals(sm._scrollVelocityY, 0)
end
-- ============================================================================
-- Test Suite: Momentum Scrolling
-- ============================================================================
TestMomentumScrolling = {}
function TestMomentumScrolling:setUp()
love.timer.setTime(0)
end
function TestMomentumScrolling:test_momentum_decelerates_over_time()
local sm = createTouchScrollManager({ bounceEnabled = false })
local el = createMockElement()
sm:detectOverflow(el)
-- Set up momentum manually
sm._momentumScrolling = true
sm._scrollVelocityY = 200
local initialVelocity = sm._scrollVelocityY
sm:update(1 / 60)
luaunit.assertTrue(sm._scrollVelocityY < initialVelocity)
luaunit.assertTrue(sm._scrollVelocityY > 0)
end
function TestMomentumScrolling:test_momentum_stops_at_low_velocity()
local sm = createTouchScrollManager({ bounceEnabled = false })
local el = createMockElement()
sm:detectOverflow(el)
sm._momentumScrolling = true
sm._scrollVelocityY = 200
-- Run many frames until momentum stops
for i = 1, 500 do
sm:update(1 / 60)
if not sm:isMomentumScrolling() then
break
end
end
luaunit.assertFalse(sm:isMomentumScrolling())
luaunit.assertEquals(sm._scrollVelocityY, 0)
end
function TestMomentumScrolling:test_momentum_moves_scroll_position()
local sm = createTouchScrollManager({ bounceEnabled = false })
local el = createMockElement()
sm:detectOverflow(el)
sm._momentumScrolling = true
sm._scrollVelocityY = 500
local initialScrollY = sm._scrollY
sm:update(1 / 60)
luaunit.assertTrue(sm._scrollY > initialScrollY)
end
function TestMomentumScrolling:test_friction_coefficient_affects_deceleration()
local smFast = createTouchScrollManager({ scrollFriction = 0.99, bounceEnabled = false })
local smSlow = createTouchScrollManager({ scrollFriction = 0.90, bounceEnabled = false })
local el = createMockElement()
smFast:detectOverflow(el)
smSlow:detectOverflow(el)
smFast._momentumScrolling = true
smFast._scrollVelocityY = 200
smSlow._momentumScrolling = true
smSlow._scrollVelocityY = 200
smFast:update(1 / 60)
smSlow:update(1 / 60)
-- Higher friction (0.99) preserves more velocity than lower friction (0.90)
luaunit.assertTrue(smFast._scrollVelocityY > smSlow._scrollVelocityY)
end
-- ============================================================================
-- Test Suite: Bounce Effects
-- ============================================================================
TestTouchScrollBounce = {}
function TestTouchScrollBounce:setUp()
love.timer.setTime(0)
end
function TestTouchScrollBounce:test_bounce_returns_to_boundary()
local sm = createTouchScrollManager({ bounceEnabled = true })
local el = createMockElement()
sm:detectOverflow(el)
-- Force overscroll position
sm._scrollY = -50
-- Run bounce updates
for i = 1, 100 do
sm:update(1 / 60)
end
-- Should have bounced back to 0
luaunit.assertAlmostEquals(sm._scrollY, 0, 1)
end
function TestTouchScrollBounce:test_bounce_at_bottom_boundary()
local sm = createTouchScrollManager({ bounceEnabled = true })
local el = createMockElement()
sm:detectOverflow(el)
-- Force overscroll past max
sm._scrollY = sm._maxScrollY + 50
for i = 1, 100 do
sm:update(1 / 60)
end
luaunit.assertAlmostEquals(sm._scrollY, sm._maxScrollY, 1)
end
function TestTouchScrollBounce:test_no_bounce_when_disabled()
local sm = createTouchScrollManager({ bounceEnabled = false })
local el = createMockElement()
sm:detectOverflow(el)
sm._scrollY = -50
sm:update(1 / 60)
-- Without bounce, scroll should stay where it is (clamped by scrollBy)
-- But here we set it directly, so it stays
luaunit.assertEquals(sm._scrollY, -50)
end
-- ============================================================================
-- Test Suite: State Query Methods
-- ============================================================================
TestTouchScrollState = {}
function TestTouchScrollState:setUp()
love.timer.setTime(0)
end
function TestTouchScrollState:test_isTouchScrolling_initially_false()
local sm = createTouchScrollManager()
luaunit.assertFalse(sm:isTouchScrolling())
end
function TestTouchScrollState:test_isMomentumScrolling_initially_false()
local sm = createTouchScrollManager()
luaunit.assertFalse(sm:isMomentumScrolling())
end
function TestTouchScrollState:test_isTouchScrolling_true_during_touch()
local sm = createTouchScrollManager()
local el = createMockElement()
sm:detectOverflow(el)
sm:handleTouchPress(100, 200)
luaunit.assertTrue(sm:isTouchScrolling())
sm:handleTouchRelease()
luaunit.assertFalse(sm:isTouchScrolling())
end
-- ============================================================================
-- Test Suite: Configuration
-- ============================================================================
TestTouchScrollConfig = {}
function TestTouchScrollConfig:setUp()
love.timer.setTime(0)
end
function TestTouchScrollConfig:test_default_config_values()
local sm = createTouchScrollManager()
luaunit.assertTrue(sm.touchScrollEnabled)
luaunit.assertTrue(sm.momentumScrollEnabled)
luaunit.assertTrue(sm.bounceEnabled)
luaunit.assertEquals(sm.scrollFriction, 0.95)
luaunit.assertEquals(sm.bounceStiffness, 0.2)
luaunit.assertEquals(sm.maxOverscroll, 100)
end
function TestTouchScrollConfig:test_custom_config_values()
local sm = createTouchScrollManager({
touchScrollEnabled = false,
momentumScrollEnabled = false,
bounceEnabled = false,
scrollFriction = 0.98,
bounceStiffness = 0.1,
maxOverscroll = 50,
})
luaunit.assertFalse(sm.touchScrollEnabled)
luaunit.assertFalse(sm.momentumScrollEnabled)
luaunit.assertFalse(sm.bounceEnabled)
luaunit.assertEquals(sm.scrollFriction, 0.98)
luaunit.assertEquals(sm.bounceStiffness, 0.1)
luaunit.assertEquals(sm.maxOverscroll, 50)
end
-- Run all tests
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end