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