-- Edge case and unhappy path tests for ScrollManager module 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 }) TestScrollManagerEdgeCases = {} -- Helper to create ScrollManager with dependencies local function createScrollManager(config) config = config or {} return ScrollManager.new(config, { Color = Color, utils = utils, }) end -- Helper to create mock element with children local function createMockElement(width, height, children) children = children or {} return { x = 0, y = 0, width = width or 200, height = height or 300, padding = { top = 10, right = 10, bottom = 10, left = 10 }, children = children, getBorderBoxWidth = function(self) return self.width end, getBorderBoxHeight = function(self) return self.height end, } end -- Helper to create mock child element local function createMockChild(x, y, width, height) return { x = x or 0, y = y or 0, width = width or 50, height = height or 50, 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, } end function TestScrollManagerEdgeCases:setUp() -- Reset any state end function TestScrollManagerEdgeCases:tearDown() -- Clean up end -- ============================================================================ -- Constructor Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testConstructorWithNilConfig() local sm = createScrollManager(nil) luaunit.assertNotNil(sm) luaunit.assertEquals(sm.overflow, "hidden") -- Default value end function TestScrollManagerEdgeCases:testConstructorWithEmptyConfig() local sm = createScrollManager({}) luaunit.assertNotNil(sm) luaunit.assertEquals(sm.overflow, "hidden") luaunit.assertEquals(sm.scrollbarWidth, 12) end function TestScrollManagerEdgeCases:testConstructorWithInvalidOverflowValue() local sm = createScrollManager({ overflow = "invalid" }) luaunit.assertNotNil(sm) luaunit.assertEquals(sm.overflow, "invalid") -- No validation, stores as-is end function TestScrollManagerEdgeCases:testConstructorWithZeroScrollbarWidth() local sm = createScrollManager({ scrollbarWidth = 0 }) luaunit.assertEquals(sm.scrollbarWidth, 0) end function TestScrollManagerEdgeCases:testConstructorWithNegativeScrollbarWidth() local sm = createScrollManager({ scrollbarWidth = -10 }) luaunit.assertEquals(sm.scrollbarWidth, -10) -- No validation end function TestScrollManagerEdgeCases:testConstructorWithNegativeScrollSpeed() local sm = createScrollManager({ scrollSpeed = -50 }) luaunit.assertEquals(sm.scrollSpeed, -50) -- No validation end function TestScrollManagerEdgeCases:testConstructorWithZeroScrollSpeed() local sm = createScrollManager({ scrollSpeed = 0 }) luaunit.assertEquals(sm.scrollSpeed, 0) end function TestScrollManagerEdgeCases:testConstructorWithInvalidFriction() local sm = createScrollManager({ scrollFriction = 1.5 }) -- > 1 would increase velocity luaunit.assertEquals(sm.scrollFriction, 1.5) end function TestScrollManagerEdgeCases:testConstructorWithNegativeFriction() local sm = createScrollManager({ scrollFriction = -0.5 }) luaunit.assertEquals(sm.scrollFriction, -0.5) end function TestScrollManagerEdgeCases:testConstructorWithZeroBounceStiffness() local sm = createScrollManager({ bounceStiffness = 0 }) luaunit.assertEquals(sm.bounceStiffness, 0) end function TestScrollManagerEdgeCases:testConstructorWithNegativeBounceStiffness() local sm = createScrollManager({ bounceStiffness = -0.5 }) luaunit.assertEquals(sm.bounceStiffness, -0.5) end function TestScrollManagerEdgeCases:testConstructorWithNegativeMaxOverscroll() local sm = createScrollManager({ maxOverscroll = -100 }) luaunit.assertEquals(sm.maxOverscroll, -100) end function TestScrollManagerEdgeCases:testConstructorWithRestoredScrollState() local sm = createScrollManager({ _scrollX = 50, _scrollY = 100 }) luaunit.assertEquals(sm._scrollX, 50) luaunit.assertEquals(sm._scrollY, 100) end function TestScrollManagerEdgeCases:testConstructorWithHideScrollbarsBooleanTrue() local sm = createScrollManager({ hideScrollbars = true }) luaunit.assertTrue(sm.hideScrollbars.vertical) luaunit.assertTrue(sm.hideScrollbars.horizontal) end function TestScrollManagerEdgeCases:testConstructorWithHideScrollbarsBooleanFalse() local sm = createScrollManager({ hideScrollbars = false }) luaunit.assertFalse(sm.hideScrollbars.vertical) luaunit.assertFalse(sm.hideScrollbars.horizontal) end function TestScrollManagerEdgeCases:testConstructorWithHideScrollbarsTable() local sm = createScrollManager({ hideScrollbars = { vertical = true, horizontal = false } }) luaunit.assertTrue(sm.hideScrollbars.vertical) luaunit.assertFalse(sm.hideScrollbars.horizontal) end -- ============================================================================ -- Method Calls Before Initialization -- ============================================================================ function TestScrollManagerEdgeCases:testDetectOverflowWithoutElement() local sm = createScrollManager({}) -- Should crash when element is nil (no longer has error handling) local success = pcall(function() sm:detectOverflow(nil) end) luaunit.assertFalse(success) end function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithoutElement() local sm = createScrollManager({}) -- Should return empty result when element is nil (overflow defaults to "hidden") local dims = sm:calculateScrollbarDimensions(nil) luaunit.assertNotNil(dims) luaunit.assertFalse(dims.vertical.visible) luaunit.assertFalse(dims.horizontal.visible) end function TestScrollManagerEdgeCases:testGetScrollbarAtPositionWithoutElement() local sm = createScrollManager({}) local result = sm:getScrollbarAtPosition(nil, 50, 50) luaunit.assertNil(result) end function TestScrollManagerEdgeCases:testHandleMousePressWithoutElement() local sm = createScrollManager({}) local consumed = sm:handleMousePress(nil, 50, 50, 1) luaunit.assertFalse(consumed) end -- ============================================================================ -- detectOverflow Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testDetectOverflowWithNoChildren() local sm = createScrollManager({ overflow = "auto" }) local element = createMockElement(200, 300, {}) sm:detectOverflow(element) local hasOverflowX, hasOverflowY = sm:hasOverflow() luaunit.assertFalse(hasOverflowX) luaunit.assertFalse(hasOverflowY) end function TestScrollManagerEdgeCases:testDetectOverflowWithZeroDimensions() local sm = createScrollManager({ overflow = "auto" }) local element = createMockElement(0, 0, {}) sm:detectOverflow(element) local contentW, contentH = sm:getContentSize() luaunit.assertEquals(contentW, 0) luaunit.assertEquals(contentH, 0) end function TestScrollManagerEdgeCases:testDetectOverflowWithVisibleOverflow() local sm = createScrollManager({ overflow = "visible" }) local child = createMockChild(0, 0, 500, 500) local element = createMockElement(200, 300, { child }) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) -- Should skip detection for visible overflow local hasOverflowX, hasOverflowY = sm:hasOverflow() luaunit.assertFalse(hasOverflowX) luaunit.assertFalse(hasOverflowY) end function TestScrollManagerEdgeCases:testDetectOverflowWithAbsolutelyPositionedChildren() local sm = createScrollManager({ overflow = "auto" }) local child = createMockChild(0, 0, 500, 500) child._explicitlyAbsolute = true -- Should be ignored in overflow calc local element = createMockElement(200, 300, { child }) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local hasOverflowX, hasOverflowY = sm:hasOverflow() luaunit.assertFalse(hasOverflowX) -- Absolute children don't contribute luaunit.assertFalse(hasOverflowY) end function TestScrollManagerEdgeCases:testDetectOverflowWithNegativeChildMargins() local sm = createScrollManager({ overflow = "auto" }) local child = createMockChild(10, 10, 100, 100) child.margin = { top = -50, right = -50, bottom = -50, left = -50 } local element = createMockElement(200, 300, { child }) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) -- Negative margins shouldn't cause negative overflow detection local contentW, contentH = sm:getContentSize() luaunit.assertTrue(contentW >= 0) luaunit.assertTrue(contentH >= 0) end function TestScrollManagerEdgeCases:testDetectOverflowClampsExistingScroll() local sm = createScrollManager({ overflow = "auto", _scrollX = 1000, _scrollY = 1000 }) local child = createMockChild(10, 10, 100, 100) local element = createMockElement(200, 300, { child }) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) -- Scroll should be clamped to max bounds local scrollX, scrollY = sm:getScroll() local maxScrollX, maxScrollY = sm:getMaxScroll() luaunit.assertTrue(scrollX <= maxScrollX) luaunit.assertTrue(scrollY <= maxScrollY) end -- ============================================================================ -- Scroll Position Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testSetScrollNegativeValues() local sm = createScrollManager({ overflow = "auto" }) sm._maxScrollX = 100 sm._maxScrollY = 100 sm:setScroll(-50, -50) local scrollX, scrollY = sm:getScroll() luaunit.assertEquals(scrollX, 0) -- Should clamp to 0 luaunit.assertEquals(scrollY, 0) end function TestScrollManagerEdgeCases:testSetScrollBeyondMax() local sm = createScrollManager({ overflow = "auto" }) sm._maxScrollX = 100 sm._maxScrollY = 100 sm:setScroll(500, 500) local scrollX, scrollY = sm:getScroll() luaunit.assertEquals(scrollX, 100) -- Should clamp to max luaunit.assertEquals(scrollY, 100) end function TestScrollManagerEdgeCases:testSetScrollWithNilValues() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 50 sm._scrollY = 50 sm._maxScrollX = 100 sm._maxScrollY = 100 sm:setScroll(nil, nil) local scrollX, scrollY = sm:getScroll() luaunit.assertEquals(scrollX, 50) -- Should keep current luaunit.assertEquals(scrollY, 50) end function TestScrollManagerEdgeCases:testSetScrollPartialUpdate() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 50 sm._scrollY = 50 sm._maxScrollX = 100 sm._maxScrollY = 100 sm:setScroll(75, nil) local scrollX, scrollY = sm:getScroll() luaunit.assertEquals(scrollX, 75) luaunit.assertEquals(scrollY, 50) -- Unchanged end function TestScrollManagerEdgeCases:testScrollByNegativeValues() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 50 sm._scrollY = 50 sm._maxScrollX = 100 sm._maxScrollY = 100 sm:scrollBy(-100, -100) local scrollX, scrollY = sm:getScroll() luaunit.assertEquals(scrollX, 0) -- Should clamp to 0 luaunit.assertEquals(scrollY, 0) end function TestScrollManagerEdgeCases:testScrollByBeyondMax() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 50 sm._scrollY = 50 sm._maxScrollX = 100 sm._maxScrollY = 100 sm:scrollBy(100, 100) local scrollX, scrollY = sm:getScroll() luaunit.assertEquals(scrollX, 100) -- Should clamp to max luaunit.assertEquals(scrollY, 100) end function TestScrollManagerEdgeCases:testScrollByWithNilValues() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 50 sm._scrollY = 50 sm._maxScrollX = 100 sm._maxScrollY = 100 sm:scrollBy(nil, nil) local scrollX, scrollY = sm:getScroll() luaunit.assertEquals(scrollX, 50) -- Unchanged luaunit.assertEquals(scrollY, 50) end function TestScrollManagerEdgeCases:testGetScrollPercentageWithZeroMax() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 0 sm._scrollY = 0 sm._maxScrollX = 0 sm._maxScrollY = 0 local percentX, percentY = sm:getScrollPercentage() luaunit.assertEquals(percentX, 0) luaunit.assertEquals(percentY, 0) end function TestScrollManagerEdgeCases:testGetScrollPercentageAtMax() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 100 sm._scrollY = 100 sm._maxScrollX = 100 sm._maxScrollY = 100 local percentX, percentY = sm:getScrollPercentage() luaunit.assertEquals(percentX, 1) luaunit.assertEquals(percentY, 1) end -- ============================================================================ -- calculateScrollbarDimensions Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithZeroTrackSize() local sm = createScrollManager({ overflow = "scroll", scrollbarPadding = 150 }) -- Padding bigger than element local element = createMockElement(200, 300, {}) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local dims = sm:calculateScrollbarDimensions(element) -- Should handle zero or negative track sizes luaunit.assertNotNil(dims.vertical) luaunit.assertNotNil(dims.horizontal) end function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithScrollMode() local sm = createScrollManager({ overflow = "scroll" }) local element = createMockElement(200, 300, {}) -- No overflow -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local dims = sm:calculateScrollbarDimensions(element) -- Scrollbars should be visible in "scroll" mode even without overflow luaunit.assertTrue(dims.vertical.visible) luaunit.assertTrue(dims.horizontal.visible) end function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithAutoModeNoOverflow() local sm = createScrollManager({ overflow = "auto" }) local element = createMockElement(200, 300, {}) -- No overflow -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local dims = sm:calculateScrollbarDimensions(element) -- Scrollbars should NOT be visible in "auto" mode without overflow luaunit.assertFalse(dims.vertical.visible) luaunit.assertFalse(dims.horizontal.visible) end function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithAxisSpecificOverflow() local sm = createScrollManager({ overflowX = "scroll", overflowY = "hidden" }) local element = createMockElement(200, 300, {}) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local dims = sm:calculateScrollbarDimensions(element) luaunit.assertTrue(dims.horizontal.visible) -- X is scroll luaunit.assertFalse(dims.vertical.visible) -- Y is hidden end function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithMinThumbSize() local sm = createScrollManager({ overflow = "scroll" }) local child = createMockChild(10, 10, 100, 10000) -- Very tall child local element = createMockElement(200, 300, { child }) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local dims = sm:calculateScrollbarDimensions(element) -- Thumb should have minimum size of 20px luaunit.assertTrue(dims.vertical.thumbHeight >= 20) end -- ============================================================================ -- Mouse Interaction Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testGetScrollbarAtPositionOutsideBounds() local sm = createScrollManager({ overflow = "scroll" }) local element = createMockElement(200, 300, {}) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local result = sm:getScrollbarAtPosition(element, -100, -100) luaunit.assertNil(result) end function TestScrollManagerEdgeCases:testGetScrollbarAtPositionWithHiddenScrollbars() local sm = createScrollManager({ overflow = "scroll", hideScrollbars = true }) local element = createMockElement(200, 300, {}) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) -- Even though scrollbar exists, it's hidden so shouldn't be detected local dims = sm:calculateScrollbarDimensions(element) local result = sm:getScrollbarAtPosition(element, 190, 50) luaunit.assertNil(result) end function TestScrollManagerEdgeCases:testHandleMousePressWithRightButton() local sm = createScrollManager({ overflow = "scroll" }) local element = createMockElement(200, 300, {}) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local consumed = sm:handleMousePress(element, 50, 50, 2) -- Right button luaunit.assertFalse(consumed) end function TestScrollManagerEdgeCases:testHandleMousePressWithMiddleButton() local sm = createScrollManager({ overflow = "scroll" }) local element = createMockElement(200, 300, {}) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local consumed = sm:handleMousePress(element, 50, 50, 3) -- Middle button luaunit.assertFalse(consumed) end function TestScrollManagerEdgeCases:testHandleMouseMoveWithoutDragging() local sm = createScrollManager({ overflow = "scroll" }) local element = createMockElement(200, 300, {}) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local consumed = sm:handleMouseMove(element, 50, 50) luaunit.assertFalse(consumed) end function TestScrollManagerEdgeCases:testHandleMouseReleaseWithoutDragging() local sm = createScrollManager({ overflow = "scroll" }) local element = createMockElement(200, 300, {}) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) local consumed = sm:handleMouseRelease(1) luaunit.assertFalse(consumed) end function TestScrollManagerEdgeCases:testHandleMouseReleaseWithWrongButton() local sm = createScrollManager({ overflow = "scroll" }) local element = createMockElement(200, 300, {}) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) sm._scrollbarDragging = true -- Simulate dragging local consumed = sm:handleMouseRelease(2) -- Wrong button luaunit.assertFalse(consumed) luaunit.assertTrue(sm._scrollbarDragging) -- Should still be dragging end -- ============================================================================ -- Wheel Scrolling Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testHandleWheelWithNoOverflow() local sm = createScrollManager({ overflow = "auto" }) sm._overflowX = false sm._overflowY = false sm._maxScrollX = 0 sm._maxScrollY = 0 local scrolled = sm:handleWheel(0, 1) luaunit.assertFalse(scrolled) end function TestScrollManagerEdgeCases:testHandleWheelWithHiddenOverflow() local sm = createScrollManager({ overflow = "hidden" }) sm._overflowX = true sm._overflowY = true sm._maxScrollX = 100 sm._maxScrollY = 100 local scrolled = sm:handleWheel(0, 1) luaunit.assertFalse(scrolled) end function TestScrollManagerEdgeCases:testHandleWheelWithVisibleOverflow() local sm = createScrollManager({ overflow = "visible" }) sm._overflowX = true sm._overflowY = true sm._maxScrollX = 100 sm._maxScrollY = 100 local scrolled = sm:handleWheel(0, 1) luaunit.assertFalse(scrolled) end function TestScrollManagerEdgeCases:testHandleWheelWithZeroValues() local sm = createScrollManager({ overflow = "auto" }) sm._overflowY = true sm._maxScrollY = 100 local scrolled = sm:handleWheel(0, 0) luaunit.assertFalse(scrolled) end function TestScrollManagerEdgeCases:testHandleWheelWithExtremeValues() local sm = createScrollManager({ overflow = "auto", scrollSpeed = 20 }) sm._scrollY = 50 sm._overflowY = true sm._maxScrollY = 100 local scrolled = sm:handleWheel(0, 1000) -- Extreme value luaunit.assertTrue(scrolled) local scrollX, scrollY = sm:getScroll() luaunit.assertEquals(scrollY, 0) -- Should clamp to min (wheel up scrolls to top) end function TestScrollManagerEdgeCases:testHandleWheelWithNegativeScrollSpeed() local sm = createScrollManager({ overflow = "auto", scrollSpeed = -20 }) sm._scrollY = 50 sm._overflowY = true sm._maxScrollY = 100 local scrolled = sm:handleWheel(0, 1) luaunit.assertTrue(scrolled) -- Negative speed would invert scroll direction local scrollX, scrollY = sm:getScroll() -- Result depends on implementation end -- ============================================================================ -- Touch Scrolling Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testHandleTouchPressWithDisabled() local sm = createScrollManager({ overflow = "auto", touchScrollEnabled = false }) local started = sm:handleTouchPress(50, 50) luaunit.assertFalse(started) end function TestScrollManagerEdgeCases:testHandleTouchPressWithHiddenOverflow() local sm = createScrollManager({ overflow = "hidden", touchScrollEnabled = true }) local started = sm:handleTouchPress(50, 50) luaunit.assertFalse(started) end function TestScrollManagerEdgeCases:testHandleTouchPressStopsMomentum() local sm = createScrollManager({ overflow = "auto", touchScrollEnabled = true }) sm._momentumScrolling = true sm._scrollVelocityX = 500 sm._scrollVelocityY = 500 local started = sm:handleTouchPress(50, 50) luaunit.assertTrue(started) luaunit.assertFalse(sm._momentumScrolling) luaunit.assertEquals(sm._scrollVelocityX, 0) luaunit.assertEquals(sm._scrollVelocityY, 0) end function TestScrollManagerEdgeCases:testHandleTouchMoveWithoutPress() local sm = createScrollManager({ overflow = "auto", touchScrollEnabled = true }) sm._touchScrolling = false local handled = sm:handleTouchMove(50, 50) luaunit.assertFalse(handled) end function TestScrollManagerEdgeCases:testHandleTouchMoveWithZeroDeltaTime() local sm = createScrollManager({ overflow = "auto", touchScrollEnabled = true }) sm._touchScrolling = true sm._lastTouchTime = love.timer.getTime() -- Same time local handled = sm:handleTouchMove(50, 50) luaunit.assertFalse(handled) -- Should reject zero dt end function TestScrollManagerEdgeCases:testHandleTouchMoveWithBounceEnabled() local sm = createScrollManager({ overflow = "auto", touchScrollEnabled = true, bounceEnabled = true, maxOverscroll = 100, }) sm._touchScrolling = true sm._scrollX = 0 sm._scrollY = 0 sm._maxScrollX = 100 sm._maxScrollY = 100 sm._lastTouchX = 100 sm._lastTouchY = 100 sm._lastTouchTime = love.timer.getTime() - 0.1 -- Move backwards to cause overscroll local handled = sm:handleTouchMove(200, 200) luaunit.assertTrue(handled) -- Should allow negative scroll (overscroll) local scrollX, scrollY = sm:getScroll() luaunit.assertTrue(scrollX < 0) luaunit.assertTrue(scrollY < 0) end function TestScrollManagerEdgeCases:testHandleTouchMoveWithBounceDisabled() local sm = createScrollManager({ overflow = "auto", touchScrollEnabled = true, bounceEnabled = false, }) sm._touchScrolling = true sm._scrollX = 0 sm._scrollY = 0 sm._maxScrollX = 100 sm._maxScrollY = 100 sm._lastTouchX = 100 sm._lastTouchY = 100 sm._lastTouchTime = love.timer.getTime() - 0.1 -- Move backwards to try overscroll local handled = sm:handleTouchMove(200, 200) luaunit.assertTrue(handled) -- Should clamp to 0 local scrollX, scrollY = sm:getScroll() luaunit.assertEquals(scrollX, 0) luaunit.assertEquals(scrollY, 0) end function TestScrollManagerEdgeCases:testHandleTouchReleaseWithoutPress() local sm = createScrollManager({ overflow = "auto", touchScrollEnabled = true }) sm._touchScrolling = false local handled = sm:handleTouchRelease() luaunit.assertFalse(handled) end function TestScrollManagerEdgeCases:testHandleTouchReleaseWithMomentumDisabled() local sm = createScrollManager({ overflow = "auto", touchScrollEnabled = true, momentumScrollEnabled = false, }) sm._touchScrolling = true sm._scrollVelocityX = 500 sm._scrollVelocityY = 500 local handled = sm:handleTouchRelease() luaunit.assertTrue(handled) luaunit.assertFalse(sm._momentumScrolling) luaunit.assertEquals(sm._scrollVelocityX, 0) luaunit.assertEquals(sm._scrollVelocityY, 0) end function TestScrollManagerEdgeCases:testHandleTouchReleaseWithLowVelocity() local sm = createScrollManager({ overflow = "auto", touchScrollEnabled = true, momentumScrollEnabled = true, }) sm._touchScrolling = true sm._scrollVelocityX = 10 -- Below threshold sm._scrollVelocityY = 10 local handled = sm:handleTouchRelease() luaunit.assertTrue(handled) luaunit.assertFalse(sm._momentumScrolling) end function TestScrollManagerEdgeCases:testHandleTouchReleaseWithHighVelocity() local sm = createScrollManager({ overflow = "auto", touchScrollEnabled = true, momentumScrollEnabled = true, }) sm._touchScrolling = true sm._scrollVelocityX = 500 sm._scrollVelocityY = 500 local handled = sm:handleTouchRelease() luaunit.assertTrue(handled) luaunit.assertTrue(sm._momentumScrolling) end -- ============================================================================ -- Update and Momentum Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testUpdateWithoutMomentum() local sm = createScrollManager({ overflow = "auto" }) sm._momentumScrolling = false sm:update(0.016) -- Normal frame time -- Should not crash end function TestScrollManagerEdgeCases:testUpdateWithZeroDeltaTime() local sm = createScrollManager({ overflow = "auto" }) sm._momentumScrolling = true sm._scrollVelocityX = 100 sm._scrollVelocityY = 100 sm:update(0) -- Should not cause issues end function TestScrollManagerEdgeCases:testUpdateWithNegativeDeltaTime() local sm = createScrollManager({ overflow = "auto" }) sm._momentumScrolling = true sm._scrollVelocityX = 100 sm._scrollVelocityY = 100 sm:update(-0.016) -- Should handle gracefully (may cause backwards scroll) end function TestScrollManagerEdgeCases:testUpdateWithVeryLargeDeltaTime() local sm = createScrollManager({ overflow = "auto" }) sm._momentumScrolling = true sm._scrollX = 50 sm._scrollY = 50 sm._maxScrollX = 100 sm._maxScrollY = 100 sm._scrollVelocityX = 100 sm._scrollVelocityY = 100 sm:update(10) -- 10 seconds -- Should handle gracefully end function TestScrollManagerEdgeCases:testUpdateStopsMomentumWhenVelocityLow() local sm = createScrollManager({ overflow = "auto", scrollFriction = 0.1 }) -- Very low friction sm._momentumScrolling = true sm._scrollVelocityX = 0.5 -- Below threshold of 1 sm._scrollVelocityY = 0.5 sm:update(0.016) -- After friction, velocity should be below threshold and momentum stopped luaunit.assertFalse(sm._momentumScrolling) luaunit.assertEquals(sm._scrollVelocityX, 0) luaunit.assertEquals(sm._scrollVelocityY, 0) end function TestScrollManagerEdgeCases:testUpdateWithInvalidFriction() local sm = createScrollManager({ overflow = "auto", scrollFriction = 1.5 }) -- > 1 increases velocity sm._momentumScrolling = true sm._scrollVelocityX = 100 sm._scrollVelocityY = 100 local initialVX = sm._scrollVelocityX sm:update(0.016) -- Velocity should increase with friction > 1 luaunit.assertTrue(math.abs(sm._scrollVelocityX) > initialVX) end function TestScrollManagerEdgeCases:testUpdateBounceWithZeroBounceStiffness() local sm = createScrollManager({ overflow = "auto", bounceEnabled = true, bounceStiffness = 0, }) sm._scrollX = -50 -- Overscrolled sm._scrollY = -50 sm._maxScrollX = 100 sm._maxScrollY = 100 sm:update(0.016) -- With zero stiffness, no bounce force applied luaunit.assertEquals(sm._scrollX, -50) luaunit.assertEquals(sm._scrollY, -50) end function TestScrollManagerEdgeCases:testUpdateBounceWithNegativeStiffness() local sm = createScrollManager({ overflow = "auto", bounceEnabled = true, bounceStiffness = -0.2, -- Negative pushes away from bounds }) sm._scrollX = -50 -- Overscrolled sm._scrollY = -50 sm._maxScrollX = 100 sm._maxScrollY = 100 local initialX = sm._scrollX sm:update(0.016) -- Negative stiffness pushes further out luaunit.assertTrue(sm._scrollX < initialX) end function TestScrollManagerEdgeCases:testUpdateBounceSnapsToZero() local sm = createScrollManager({ overflow = "auto", bounceEnabled = true, bounceStiffness = 1.0, -- Very high stiffness }) sm._scrollX = -0.3 -- Small overscroll sm._scrollY = -0.3 sm._maxScrollX = 100 sm._maxScrollY = 100 sm:update(0.016) -- Should snap to 0 when close enough luaunit.assertEquals(sm._scrollX, 0) luaunit.assertEquals(sm._scrollY, 0) end function TestScrollManagerEdgeCases:testUpdateBounceSnapsToMax() local sm = createScrollManager({ overflow = "auto", bounceEnabled = true, bounceStiffness = 1.0, }) sm._scrollX = 100.3 -- Small overscroll beyond max sm._scrollY = 100.3 sm._maxScrollX = 100 sm._maxScrollY = 100 sm:update(0.016) -- Should snap to max when close enough luaunit.assertEquals(sm._scrollX, 100) luaunit.assertEquals(sm._scrollY, 100) end -- ============================================================================ -- State Persistence Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testGetState() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 50 sm._scrollY = 75 sm._scrollbarDragging = true sm._hoveredScrollbar = "vertical" sm._scrollbarDragOffset = 10 local state = sm:getState() luaunit.assertEquals(state._scrollX, 50) luaunit.assertEquals(state._scrollY, 75) luaunit.assertTrue(state._scrollbarDragging) luaunit.assertEquals(state._hoveredScrollbar, "vertical") luaunit.assertEquals(state._scrollbarDragOffset, 10) end function TestScrollManagerEdgeCases:testSetStateWithNil() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 50 sm._scrollY = 50 sm:setState(nil) -- Should not change anything luaunit.assertEquals(sm._scrollX, 50) luaunit.assertEquals(sm._scrollY, 50) end function TestScrollManagerEdgeCases:testSetStateWithEmptyTable() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 50 sm._scrollY = 50 sm:setState({}) -- Should not change anything luaunit.assertEquals(sm._scrollX, 50) luaunit.assertEquals(sm._scrollY, 50) end function TestScrollManagerEdgeCases:testSetStatePartial() local sm = createScrollManager({ overflow = "auto" }) sm._scrollX = 50 sm._scrollY = 50 sm._scrollbarDragging = false sm:setState({ scrollX = 100, scrollbarDragging = true }) luaunit.assertEquals(sm._scrollX, 100) luaunit.assertEquals(sm._scrollY, 50) -- Unchanged luaunit.assertTrue(sm._scrollbarDragging) end -- ============================================================================ -- Hover State Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testUpdateHoverStateOutsideScrollbar() local sm = createScrollManager({ overflow = "scroll" }) local element = createMockElement(200, 300, {}) -- sm:initialize(element) -- Removed: element now passed as parameter sm:detectOverflow(element) sm._scrollbarHoveredVertical = true sm._scrollbarHoveredHorizontal = true sm:updateHoverState(element, 0, 0) -- Far from scrollbar luaunit.assertFalse(sm._scrollbarHoveredVertical) luaunit.assertFalse(sm._scrollbarHoveredHorizontal) end -- ============================================================================ -- Flag Management Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testResetScrollbarPressFlag() local sm = createScrollManager({}) sm._scrollbarPressHandled = true sm:resetScrollbarPressFlag() luaunit.assertFalse(sm._scrollbarPressHandled) end function TestScrollManagerEdgeCases:testSetScrollbarPressHandled() local sm = createScrollManager({}) sm._scrollbarPressHandled = false sm:setScrollbarPressHandled() luaunit.assertTrue(sm._scrollbarPressHandled) end function TestScrollManagerEdgeCases:testWasScrollbarPressHandled() local sm = createScrollManager({}) sm._scrollbarPressHandled = true luaunit.assertTrue(sm:wasScrollbarPressHandled()) end -- ============================================================================ -- Query Methods Edge Cases -- ============================================================================ function TestScrollManagerEdgeCases:testIsTouchScrolling() local sm = createScrollManager({}) luaunit.assertFalse(sm:isTouchScrolling()) sm._touchScrolling = true luaunit.assertTrue(sm:isTouchScrolling()) end function TestScrollManagerEdgeCases:testIsMomentumScrolling() local sm = createScrollManager({}) luaunit.assertFalse(sm:isMomentumScrolling()) sm._momentumScrolling = true luaunit.assertTrue(sm:isMomentumScrolling()) end -- Test scrollbarKnobOffset configuration function TestScrollManagerEdgeCases:testScrollbarKnobOffsetNumber() local sm = createScrollManager({ scrollbarKnobOffset = 5 }) luaunit.assertNotNil(sm.scrollbarKnobOffset) luaunit.assertEquals(sm.scrollbarKnobOffset.x, 5) luaunit.assertEquals(sm.scrollbarKnobOffset.y, 5) luaunit.assertEquals(sm.scrollbarKnobOffset.horizontal, 5) luaunit.assertEquals(sm.scrollbarKnobOffset.vertical, 5) end function TestScrollManagerEdgeCases:testScrollbarKnobOffsetTableXY() local sm = createScrollManager({ scrollbarKnobOffset = { x = 10, y = 20 } }) luaunit.assertNotNil(sm.scrollbarKnobOffset) luaunit.assertEquals(sm.scrollbarKnobOffset.x, 10) luaunit.assertEquals(sm.scrollbarKnobOffset.y, 20) luaunit.assertEquals(sm.scrollbarKnobOffset.horizontal, 10) luaunit.assertEquals(sm.scrollbarKnobOffset.vertical, 20) end function TestScrollManagerEdgeCases:testScrollbarKnobOffsetTableHorizontalVertical() local sm = createScrollManager({ scrollbarKnobOffset = { horizontal = 15, vertical = 25 } }) luaunit.assertNotNil(sm.scrollbarKnobOffset) luaunit.assertEquals(sm.scrollbarKnobOffset.x, 15) luaunit.assertEquals(sm.scrollbarKnobOffset.y, 25) luaunit.assertEquals(sm.scrollbarKnobOffset.horizontal, 15) luaunit.assertEquals(sm.scrollbarKnobOffset.vertical, 25) end function TestScrollManagerEdgeCases:testScrollbarKnobOffsetDefault() local sm = createScrollManager({}) luaunit.assertNotNil(sm.scrollbarKnobOffset) luaunit.assertEquals(sm.scrollbarKnobOffset.x, 0) luaunit.assertEquals(sm.scrollbarKnobOffset.y, 0) luaunit.assertEquals(sm.scrollbarKnobOffset.horizontal, 0) luaunit.assertEquals(sm.scrollbarKnobOffset.vertical, 0) end function TestScrollManagerEdgeCases:testScrollbarKnobOffsetStatePersistence() local sm = createScrollManager({ scrollbarKnobOffset = { x = 5, y = 10 } }) local state = sm:getState() luaunit.assertNotNil(state.scrollbarKnobOffset) local sm2 = createScrollManager({}) sm2:setState(state) luaunit.assertEquals(sm2.scrollbarKnobOffset.x, 5) luaunit.assertEquals(sm2.scrollbarKnobOffset.y, 10) end if not _G.RUNNING_ALL_TESTS then os.exit(luaunit.LuaUnit.run()) end