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,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