From 92068d531546caf2f3a06aa000db6f540e43d91d Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Thu, 20 Nov 2025 14:27:34 -0500 Subject: [PATCH] trying to get coverage analysis to reasonable time --- .luacov | 12 +- modules/Blur.lua | 7 +- modules/Element.lua | 29 + modules/ErrorHandler.lua | 2 +- modules/types.lua | 6 +- .../__profiles__/animation_stress_profile.lua | 20 +- .../__profiles__/layout_stress_profile.lua | 20 +- .../__profiles__/render_stress_profile.lua | 32 +- profiling/main.lua | 2 +- profiling/utils/PerformanceProfiler.lua | 79 ++ testing/__tests__/blur_test.lua | 355 ++++-- testing/__tests__/element_test.lua | 243 ++-- testing/__tests__/flexlove_test.lua | 23 - .../scroll_manager_edge_cases_test.lua | 1031 +++++++++++++++++ testing/__tests__/shorthand_syntax_test.lua | 730 ++++++++++++ .../__tests__/text_editor_edge_cases_test.lua | 610 ++++++++++ testing/runParallel.sh | 190 --- 17 files changed, 3011 insertions(+), 380 deletions(-) create mode 100644 testing/__tests__/scroll_manager_edge_cases_test.lua create mode 100644 testing/__tests__/shorthand_syntax_test.lua create mode 100644 testing/__tests__/text_editor_edge_cases_test.lua delete mode 100755 testing/runParallel.sh diff --git a/.luacov b/.luacov index 5b640c1..841f35f 100644 --- a/.luacov +++ b/.luacov @@ -13,6 +13,13 @@ return { "tasks", "themes", "luarocks", + "loveStub", -- Exclude LÖVE stub from coverage + }, + + -- Include patterns - focus coverage on core modules + include = { + "modules/", + "FlexLove%.lua", }, -- Run reporter by default @@ -21,6 +28,7 @@ return { -- Delete stats file after reporting deletestats = false, - -- Tick options - tick = true + -- Tick options - enable for better line-by-line tracking + -- Note: With cluacov this is faster than pure Lua luacov + tick = true, } diff --git a/modules/Blur.lua b/modules/Blur.lua index d430fbb..fd669f2 100644 --- a/modules/Blur.lua +++ b/modules/Blur.lua @@ -1,3 +1,6 @@ +-- Lua 5.2+ compatibility for unpack +local unpack = table.unpack or unpack + local Cache = { canvases = {}, quads = {}, @@ -368,7 +371,9 @@ end --- Initialize Blur module with dependencies ---@param deps table Dependencies: { ErrorHandler = ErrorHandler? } function Blur.init(deps) - Blur._ErrorHandler = deps.ErrorHandler + if type(deps) == "table" then + Blur._ErrorHandler = deps.ErrorHandler + end end Blur.Cache = Cache diff --git a/modules/Element.lua b/modules/Element.lua index e5023c6..82b570a 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -225,6 +225,35 @@ function Element.new(props) Color = Element._Color, } + -- Normalize flexDirection: convert "row"→"horizontal", "column"→"vertical" + if props.flexDirection == "row" then + props.flexDirection = "horizontal" + elseif props.flexDirection == "column" then + props.flexDirection = "vertical" + end + + -- Normalize padding: convert single value to table with all sides + if props.padding ~= nil and type(props.padding) ~= "table" then + local singleValue = props.padding + props.padding = { + top = singleValue, + right = singleValue, + bottom = singleValue, + left = singleValue, + } + end + + -- Normalize margin: convert single value to table with all sides + if props.margin ~= nil and type(props.margin) ~= "table" then + local singleValue = props.margin + props.margin = { + top = singleValue, + right = singleValue, + bottom = singleValue, + left = singleValue, + } + end + self.children = {} self.onEvent = props.onEvent diff --git a/modules/ErrorHandler.lua b/modules/ErrorHandler.lua index abeec71..f6b02dc 100644 --- a/modules/ErrorHandler.lua +++ b/modules/ErrorHandler.lua @@ -737,7 +737,7 @@ end ---@param suggestion string|nil Suggestion function ErrorHandler:_writeLog(level, levelNum, module, code, message, details, suggestion) -- Check if we should log this level - if levelNum > self.logLevel then + if not levelNum or not self.logLevel or levelNum > self.logLevel then return end diff --git a/modules/types.lua b/modules/types.lua index 235602d..fdfae2b 100644 --- a/modules/types.lua +++ b/modules/types.lua @@ -50,8 +50,8 @@ local AnimationProps = {} ---@field backgroundColor Color? -- Background color (default: transparent) ---@field cornerRadius number|{topLeft:number?, topRight:number?, bottomLeft:number?, bottomRight:number?}? -- Corner radius: number (all corners) or table for individual corners (default: 0) ---@field gap number|string? -- Space between children elements (default: 0) ----@field padding {top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal:number|string?, vertical:number|string?}? -- Padding around children (default: {top=0, right=0, bottom=0, left=0}) ----@field margin {top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal:number|string?, vertical:number|string?}? -- Margin around element (default: {top=0, right=0, bottom=0, left=0}) +---@field padding number|string|{top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal:number|string?, vertical:number|string?}? -- Padding around children: single value for all sides or table for individual sides (default: {top=0, right=0, bottom=0, left=0}) +---@field margin number|string|{top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal:number|string?, vertical:number|string?}? -- Margin around element: single value for all sides or table for individual sides (default: {top=0, right=0, bottom=0, left=0}) ---@field text string? -- Text content to display (default: nil) ---@field textAlign TextAlign? -- Alignment of the text content (default: START) ---@field textColor Color? -- Color of the text content (default: black or theme text color) @@ -61,7 +61,7 @@ local AnimationProps = {} ---@field fontFamily string? -- Font family name from theme or path to font file (default: theme default or system default, inherits from parent) ---@field autoScaleText boolean? -- Whether text should auto-scale with window size (default: true) ---@field positioning Positioning? -- Layout positioning mode: "absolute"|"relative"|"flex"|"grid" (default: RELATIVE) ----@field flexDirection FlexDirection? -- Direction of flex layout: "horizontal"|"vertical" (default: HORIZONTAL) +---@field flexDirection FlexDirection? -- Direction of flex layout: "horizontal"|"vertical"|"row"|"column" (row→horizontal, column→vertical, default: HORIZONTAL) ---@field justifyContent JustifyContent? -- Alignment of items along main axis (default: FLEX_START) ---@field alignItems AlignItems? -- Alignment of items along cross axis (default: STRETCH) ---@field alignContent AlignContent? -- Alignment of lines in multi-line flex containers (default: STRETCH) diff --git a/profiling/__profiles__/animation_stress_profile.lua b/profiling/__profiles__/animation_stress_profile.lua index f41a2c1..322071b 100644 --- a/profiling/__profiles__/animation_stress_profile.lua +++ b/profiling/__profiles__/animation_stress_profile.lua @@ -194,11 +194,29 @@ function profile.draw() love.graphics.print("Press - to remove 10 animated elements", 10, love.graphics.getHeight() - 45) end -function profile.keypressed(key) +function profile.keypressed(key, profiler) if key == "=" or key == "+" then + -- Create snapshot before changing animation count + if profiler then + local label = string.format("%d animations", profile.animationCount) + profiler:createSnapshot(label, { + animationCount = profile.animationCount, + activeAnimations = #profile.animations + }) + end + profile.animationCount = math.min(profile.maxAnimations, profile.animationCount + 10) profile.buildLayout() elseif key == "-" or key == "_" then + -- Create snapshot before changing animation count + if profiler then + local label = string.format("%d animations", profile.animationCount) + profiler:createSnapshot(label, { + animationCount = profile.animationCount, + activeAnimations = #profile.animations + }) + end + profile.animationCount = math.max(profile.minAnimations, profile.animationCount - 10) profile.buildLayout() end diff --git a/profiling/__profiles__/layout_stress_profile.lua b/profiling/__profiles__/layout_stress_profile.lua index dd0cdc4..c528294 100644 --- a/profiling/__profiles__/layout_stress_profile.lua +++ b/profiling/__profiles__/layout_stress_profile.lua @@ -127,11 +127,29 @@ function profile.draw() love.graphics.print("Press - to remove 50 elements", 10, love.graphics.getHeight() - 45) end -function profile.keypressed(key) +function profile.keypressed(key, profiler) if key == "=" or key == "+" then + -- Create snapshot before changing element count + if profiler then + local label = string.format("%d elements", profile.elementCount) + profiler:createSnapshot(label, { + elementCount = profile.elementCount, + nestingDepth = profile.nestingDepth + }) + end + profile.elementCount = math.min(profile.maxElements, profile.elementCount + 50) profile.buildLayout() elseif key == "-" or key == "_" then + -- Create snapshot before changing element count + if profiler then + local label = string.format("%d elements", profile.elementCount) + profiler:createSnapshot(label, { + elementCount = profile.elementCount, + nestingDepth = profile.nestingDepth + }) + end + profile.elementCount = math.max(10, profile.elementCount - 50) profile.buildLayout() end diff --git a/profiling/__profiles__/render_stress_profile.lua b/profiling/__profiles__/render_stress_profile.lua index 2ee4913..30a084e 100644 --- a/profiling/__profiles__/render_stress_profile.lua +++ b/profiling/__profiles__/render_stress_profile.lua @@ -152,11 +152,41 @@ function profile.draw() love.graphics.print("Press R/T/L to toggle features", 10, love.graphics.getHeight() - 50) end -function profile.keypressed(key) +function profile.keypressed(key, profiler) if key == "=" or key == "+" then + -- Create snapshot before changing element count + if profiler then + local label = string.format("%d elements (R:%s T:%s L:%s)", + profile.elementCount, + profile.showRounded and "on" or "off", + profile.showText and "on" or "off", + profile.showLayering and "on" or "off") + profiler:createSnapshot(label, { + elementCount = profile.elementCount, + showRounded = profile.showRounded, + showText = profile.showText, + showLayering = profile.showLayering + }) + end + profile.elementCount = math.min(profile.maxElements, profile.elementCount + 50) profile.buildLayout() elseif key == "-" or key == "_" then + -- Create snapshot before changing element count + if profiler then + local label = string.format("%d elements (R:%s T:%s L:%s)", + profile.elementCount, + profile.showRounded and "on" or "off", + profile.showText and "on" or "off", + profile.showLayering and "on" or "off") + profiler:createSnapshot(label, { + elementCount = profile.elementCount, + showRounded = profile.showRounded, + showText = profile.showText, + showLayering = profile.showLayering + }) + end + profile.elementCount = math.max(profile.minElements, profile.elementCount - 50) profile.buildLayout() elseif key == "r" then diff --git a/profiling/main.lua b/profiling/main.lua index a1fde53..cad3648 100644 --- a/profiling/main.lua +++ b/profiling/main.lua @@ -335,7 +335,7 @@ function love.keypressed(key) if state.currentProfile and type(state.currentProfile.keypressed) == "function" then pcall(function() - state.currentProfile.keypressed(key) + state.currentProfile.keypressed(key, state.profiler) end) end end diff --git a/profiling/utils/PerformanceProfiler.lua b/profiling/utils/PerformanceProfiler.lua index ed7e568..1e89ad8 100644 --- a/profiling/utils/PerformanceProfiler.lua +++ b/profiling/utils/PerformanceProfiler.lua @@ -9,6 +9,8 @@ ---@field _currentFrameStart number? ---@field _maxHistorySize number ---@field _lastGcCount number +---@field _snapshots table +---@field _currentSnapshot table? local PerformanceProfiler = {} PerformanceProfiler.__index = PerformanceProfiler @@ -29,6 +31,8 @@ function PerformanceProfiler.new(config) self._markers = {} self._currentFrameStart = nil self._lastGcCount = collectgarbage("count") + self._snapshots = {} + self._currentSnapshot = nil return self end @@ -379,6 +383,42 @@ function PerformanceProfiler:reset() self._markers = {} self._currentFrameStart = nil self._lastGcCount = collectgarbage("count") + -- Don't reset snapshots - they persist across resets +end + +--- Create a snapshot of current metrics with a label +---@param label string Label for this snapshot (e.g., "100 elements", "500 elements") +---@param metadata table? Additional metadata to store with snapshot +---@return nil +function PerformanceProfiler:createSnapshot(label, metadata) + local report = self:getReport() + + table.insert(self._snapshots, { + label = label, + timestamp = os.date("%Y-%m-%d %H:%M:%S"), + metadata = metadata or {}, + report = report, + }) + + -- Reset current metrics for next snapshot period + self._frameCount = 0 + self._startTime = love.timer.getTime() + self._frameTimes = {} + self._fpsHistory = {} + self._memoryHistory = {} + self._currentFrameStart = nil +end + +--- Get all snapshots +---@return table +function PerformanceProfiler:getSnapshots() + return self._snapshots +end + +--- Clear all snapshots +---@return nil +function PerformanceProfiler:clearSnapshots() + self._snapshots = {} end ---@return string @@ -571,6 +611,45 @@ function PerformanceProfiler:_saveWithIO(filepath, profileName) end end + -- Snapshots (if any) + if #self._snapshots > 0 then + table.insert(lines, "## Snapshots") + table.insert(lines, "") + table.insert(lines, "> Performance metrics captured at different configuration points") + table.insert(lines, "") + + for i, snapshot in ipairs(self._snapshots) do + table.insert(lines, string.format("### %d. %s", i, snapshot.label)) + table.insert(lines, "") + table.insert(lines, string.format("**Captured:** %s", snapshot.timestamp)) + + -- Show metadata if present + if next(snapshot.metadata) then + table.insert(lines, "") + table.insert(lines, "**Configuration:**") + for key, value in pairs(snapshot.metadata) do + table.insert(lines, string.format("- %s: `%s`", key, tostring(value))) + end + end + + table.insert(lines, "") + + local r = snapshot.report + + -- Compact FPS/Frame Time table + table.insert(lines, "| FPS | Frame Time (ms) | Memory (MB) | Frames |") + table.insert(lines, "|-----|-----------------|-------------|--------|") + table.insert(lines, string.format("| Avg: %.1f | Avg: %.2f | Avg: %.2f | %d |", + r.fps.average, r.frameTime.average, r.memory.average, r.frameCount)) + table.insert(lines, string.format("| 1%% Worst: **%.1f** | P99: %.2f | Peak: %.2f | Duration: %.1fs |", + r.fps.worst_1_percent, r.frameTime.p99, r.memory.peak, r.totalTime)) + table.insert(lines, "") + end + + table.insert(lines, "---") + table.insert(lines, "") + end + table.insert(lines, "---") table.insert(lines, "") diff --git a/testing/__tests__/blur_test.lua b/testing/__tests__/blur_test.lua index 444d199..6ce4f70 100644 --- a/testing/__tests__/blur_test.lua +++ b/testing/__tests__/blur_test.lua @@ -1,7 +1,14 @@ -local luaunit = require("testing.luaunit") +package.path = package.path .. ";./?.lua;./modules/?.lua" + require("testing.loveStub") +local luaunit = require("testing.luaunit") local Blur = require("modules.Blur") +local ErrorHandler = require("modules.ErrorHandler") + +-- Initialize ErrorHandler +ErrorHandler.init({}) +Blur.init({ ErrorHandler = ErrorHandler }) TestBlur = {} @@ -10,9 +17,11 @@ function TestBlur:setUp() Blur.clearCache() end --- Unhappy path tests for Blur.new({quality = ) +-- ============================================================================ +-- Constructor Tests: Blur.new() +-- ============================================================================ -function TestBlur:testNewWithNilQuality(}) +function TestBlur:testNewWithNilQuality() -- Should default to quality 5 local blur = Blur.new({quality = nil}) luaunit.assertNotNil(blur) @@ -62,19 +71,35 @@ function TestBlur:testNewEnsuresOddTaps() end end --- Unhappy path tests for Blur.applyToRegion() - -function TestBlur:testApplyToRegionWithNilBlurInstance() - local called = false - local drawFunc = function() - called = true - end - - luaunit.assertError(function() - Blur.applyToRegion(nil, 50, 0, 0, 100, 100, drawFunc) - end) +function TestBlur:testNewWithEmptyProps() + -- Should work with no props table + local blur = Blur.new() + luaunit.assertNotNil(blur) + luaunit.assertEquals(blur.quality, 5) end +function TestBlur:testNewWithNilProps() + -- Should work with explicit nil + local blur = Blur.new(nil) + luaunit.assertNotNil(blur) + luaunit.assertEquals(blur.quality, 5) +end + +function TestBlur:testNewCreatesUniqueShaders() + -- Each instance should have its own shader + local blur1 = Blur.new({quality = 5}) + local blur2 = Blur.new({quality = 5}) + + luaunit.assertNotNil(blur1.shader) + luaunit.assertNotNil(blur2.shader) + -- Shaders should be different objects even if same quality + luaunit.assertNotEquals(blur1.shader, blur2.shader) +end + +-- ============================================================================ +-- applyToRegion() Edge Cases +-- ============================================================================ + function TestBlur:testApplyToRegionWithZeroIntensity() local blur = Blur.new({quality = 5}) local called = false @@ -83,7 +108,7 @@ function TestBlur:testApplyToRegionWithZeroIntensity() end -- Should just call drawFunc and return early - Blur.applyToRegion(blur, 0, 0, 0, 100, 100, drawFunc) + blur:applyToRegion(0, 0, 0, 100, 100, drawFunc) luaunit.assertTrue(called) end @@ -95,7 +120,7 @@ function TestBlur:testApplyToRegionWithNegativeIntensity() end -- Should just call drawFunc and return early - Blur.applyToRegion(blur, -10, 0, 0, 100, 100, drawFunc) + blur:applyToRegion(-10, 0, 0, 100, 100, drawFunc) luaunit.assertTrue(called) end @@ -107,7 +132,7 @@ function TestBlur:testApplyToRegionWithZeroWidth() end -- Should just call drawFunc and return early - Blur.applyToRegion(blur, 50, 0, 0, 0, 100, drawFunc) + blur:applyToRegion(50, 0, 0, 0, 100, drawFunc) luaunit.assertTrue(called) end @@ -119,7 +144,7 @@ function TestBlur:testApplyToRegionWithZeroHeight() end -- Should just call drawFunc and return early - Blur.applyToRegion(blur, 50, 0, 0, 100, 0, drawFunc) + blur:applyToRegion(50, 0, 0, 100, 0, drawFunc) luaunit.assertTrue(called) end @@ -131,7 +156,7 @@ function TestBlur:testApplyToRegionWithNegativeWidth() end -- Should just call drawFunc and return early - Blur.applyToRegion(blur, 50, 0, 0, -100, 100, drawFunc) + blur:applyToRegion(50, 0, 0, -100, 100, drawFunc) luaunit.assertTrue(called) end @@ -143,54 +168,111 @@ function TestBlur:testApplyToRegionWithNegativeHeight() end -- Should just call drawFunc and return early - Blur.applyToRegion(blur, 50, 0, 0, 100, -100, drawFunc) + blur:applyToRegion(50, 0, 0, 100, -100, drawFunc) luaunit.assertTrue(called) end function TestBlur:testApplyToRegionWithIntensityOver100() local blur = Blur.new({quality = 5}) - - -- We can't fully test rendering without complete LÖVE graphics - -- But we can verify the blur instance was created - luaunit.assertNotNil(blur) - luaunit.assertTrue(true) -end - -function TestBlur:testApplyToRegionWithSmallDimensions() - local blur = Blur.new({quality = 5}) local called = false local drawFunc = function() called = true end - -- For small dimensions, we test that it doesn't error - -- We can't fully test the rendering without full LÖVE graphics - luaunit.assertNotNil(blur) - luaunit.assertTrue(true) + -- Should clamp intensity to 100 + blur:applyToRegion(150, 0, 0, 100, 100, drawFunc) + luaunit.assertTrue(called) +end + +function TestBlur:testApplyToRegionWithNonFunctionDrawFunc() + local blur = Blur.new({quality = 5}) + + -- Should not error but warn through ErrorHandler + blur:applyToRegion(50, 0, 0, 100, 100, "not a function") + luaunit.assertTrue(true) -- Should reach here without crash end function TestBlur:testApplyToRegionWithNilDrawFunc() local blur = Blur.new({quality = 5}) - luaunit.assertError(function() - Blur.applyToRegion(blur, 50, 0, 0, 100, 100, nil) - end) + -- Should not error but warn through ErrorHandler + blur:applyToRegion(50, 0, 0, 100, 100, nil) + luaunit.assertTrue(true) -- Should reach here without crash end --- Unhappy path tests for Blur.applyBackdrop() +function TestBlur:testApplyToRegionWithNegativeCoordinates() + local blur = Blur.new({quality = 5}) + local called = false + local drawFunc = function() + called = true + end -function TestBlur:testApplyBackdropWithNilBlurInstance() - local mockCanvas = { - getDimensions = function() - return 100, 100 - end, - } - - luaunit.assertError(function() - Blur.applyBackdrop(nil, 50, 0, 0, 100, 100, mockCanvas) - end) + -- Negative coordinates should work (off-screen rendering) + blur:applyToRegion(50, -100, -100, 100, 100, drawFunc) + luaunit.assertTrue(called) end +function TestBlur:testApplyToRegionWithVerySmallDimensions() + local blur = Blur.new({quality = 5}) + local called = false + local drawFunc = function() + called = true + end + + -- Very small dimensions (1x1) + blur:applyToRegion(50, 0, 0, 1, 1, drawFunc) + luaunit.assertTrue(called) +end + +function TestBlur:testApplyToRegionWithVeryLargeDimensions() + local blur = Blur.new({quality = 5}) + local called = false + local drawFunc = function() + called = true + end + + -- Very large dimensions (might stress cache) + blur:applyToRegion(50, 0, 0, 4096, 4096, drawFunc) + luaunit.assertTrue(called) +end + +function TestBlur:testApplyToRegionIntensityBoundaries() + local blur = Blur.new({quality = 5}) + local called = false + local drawFunc = function() + called = true + end + + -- Test boundary values that affect passes calculation + -- intensity 20 = 1 pass + blur:applyToRegion(20, 0, 0, 100, 100, drawFunc) + luaunit.assertTrue(called) + + called = false + -- intensity 40 = 2 passes + blur:applyToRegion(40, 0, 0, 100, 100, drawFunc) + luaunit.assertTrue(called) + + called = false + -- intensity 60 = 3 passes + blur:applyToRegion(60, 0, 0, 100, 100, drawFunc) + luaunit.assertTrue(called) + + called = false + -- intensity 80 = 4 passes + blur:applyToRegion(80, 0, 0, 100, 100, drawFunc) + luaunit.assertTrue(called) + + called = false + -- intensity 100 = 5 passes + blur:applyToRegion(100, 0, 0, 100, 100, drawFunc) + luaunit.assertTrue(called) +end + +-- ============================================================================ +-- applyBackdrop() Edge Cases +-- ============================================================================ + function TestBlur:testApplyBackdropWithZeroIntensity() local blur = Blur.new({quality = 5}) local mockCanvas = { @@ -200,7 +282,7 @@ function TestBlur:testApplyBackdropWithZeroIntensity() } -- Should return early without error - Blur.applyBackdrop(blur, 0, 0, 0, 100, 100, mockCanvas) + blur:applyBackdrop(0, 0, 0, 100, 100, mockCanvas) luaunit.assertTrue(true) end @@ -213,7 +295,7 @@ function TestBlur:testApplyBackdropWithNegativeIntensity() } -- Should return early without error - Blur.applyBackdrop(blur, -10, 0, 0, 100, 100, mockCanvas) + blur:applyBackdrop(-10, 0, 0, 100, 100, mockCanvas) luaunit.assertTrue(true) end @@ -226,7 +308,7 @@ function TestBlur:testApplyBackdropWithZeroWidth() } -- Should return early without error - Blur.applyBackdrop(blur, 50, 0, 0, 0, 100, mockCanvas) + blur:applyBackdrop(50, 0, 0, 0, 100, mockCanvas) luaunit.assertTrue(true) end @@ -239,16 +321,16 @@ function TestBlur:testApplyBackdropWithZeroHeight() } -- Should return early without error - Blur.applyBackdrop(blur, 50, 0, 0, 100, 0, mockCanvas) + blur:applyBackdrop(50, 0, 0, 100, 0, mockCanvas) luaunit.assertTrue(true) end function TestBlur:testApplyBackdropWithNilCanvas() local blur = Blur.new({quality = 5}) - luaunit.assertError(function() - Blur.applyBackdrop(blur, 50, 0, 0, 100, 100, nil) - end) + -- Should not error but warn through ErrorHandler + blur:applyBackdrop(50, 0, 0, 100, 100, nil) + luaunit.assertTrue(true) -- Should reach here without crash end function TestBlur:testApplyBackdropWithIntensityOver100() @@ -259,13 +341,22 @@ function TestBlur:testApplyBackdropWithIntensityOver100() end, } - -- We can't fully test rendering without complete LÖVE graphics - luaunit.assertNotNil(blur) - luaunit.assertNotNil(mockCanvas) + -- Should clamp intensity to 100 + blur:applyBackdrop(200, 0, 0, 100, 100, mockCanvas) luaunit.assertTrue(true) end -function TestBlur:testApplyBackdropWithSmallDimensions() +function TestBlur:testApplyBackdropWithInvalidCanvas() + local blur = Blur.new({quality = 5}) + local invalidCanvas = "not a canvas" + + -- Should error when trying to call getDimensions + luaunit.assertErrorMsgContains("attempt", function() + blur:applyBackdrop(50, 0, 0, 100, 100, invalidCanvas) + end) +end + +function TestBlur:testApplyBackdropRegionBeyondCanvas() local blur = Blur.new({quality = 5}) local mockCanvas = { getDimensions = function() @@ -273,12 +364,43 @@ function TestBlur:testApplyBackdropWithSmallDimensions() end, } - -- We can't fully test rendering without complete LÖVE graphics - luaunit.assertNotNil(blur) + -- Region starts beyond canvas bounds + blur:applyBackdrop(50, 150, 150, 100, 100, mockCanvas) luaunit.assertTrue(true) end --- Tests for Blur.clearCache() +function TestBlur:testApplyBackdropWithNegativeCoordinates() + local blur = Blur.new({quality = 5}) + local mockCanvas = { + getDimensions = function() + return 100, 100 + end, + } + + -- Negative coordinates (region partially off-screen) + blur:applyBackdrop(50, -50, -50, 100, 100, mockCanvas) + luaunit.assertTrue(true) +end + +-- ============================================================================ +-- Getter Methods +-- ============================================================================ + +function TestBlur:testGetQuality() + local blur = Blur.new({quality = 7}) + luaunit.assertEquals(blur:getQuality(), 7) +end + +function TestBlur:testGetTaps() + local blur = Blur.new({quality = 5}) + luaunit.assertIsNumber(blur:getTaps()) + luaunit.assertTrue(blur:getTaps() > 0) + luaunit.assertTrue(blur:getTaps() % 2 == 1) -- Must be odd +end + +-- ============================================================================ +-- Cache Tests +-- ============================================================================ function TestBlur:testClearCacheDoesNotError() -- Create some blur instances to populate cache @@ -297,22 +419,109 @@ function TestBlur:testClearCacheMultipleTimes() luaunit.assertTrue(true) end --- Edge case: intensity boundaries - -function TestBlur:testIntensityBoundaries() - local blur = Blur.new({quality = 5}) - - -- Test that various quality levels create valid blur instances - for quality = 1, 10 do - local b = Blur.new({quality = quality}) - luaunit.assertNotNil(b) - luaunit.assertNotNil(b.shader) - luaunit.assertTrue(b.taps % 2 == 1) -- Taps must be odd - end +function TestBlur:testCacheAccessMethods() + -- Test that Cache is accessible + luaunit.assertNotNil(Blur.Cache) + luaunit.assertNotNil(Blur.Cache.getCanvas) + luaunit.assertNotNil(Blur.Cache.releaseCanvas) + luaunit.assertNotNil(Blur.Cache.getQuad) + luaunit.assertNotNil(Blur.Cache.releaseQuad) + luaunit.assertNotNil(Blur.Cache.clear) +end +function TestBlur:testReleaseNonExistentCanvas() + -- Should not error when releasing canvas that's not in cache + local fakeCanvas = {} + Blur.Cache.releaseCanvas(fakeCanvas) luaunit.assertTrue(true) end +function TestBlur:testReleaseNonExistentQuad() + -- Should not error when releasing quad that's not in cache + local fakeQuad = {} + Blur.Cache.releaseQuad(fakeQuad) + luaunit.assertTrue(true) +end + +-- ============================================================================ +-- ShaderBuilder Edge Cases +-- ============================================================================ + +function TestBlur:testShaderBuilderAccessible() + luaunit.assertNotNil(Blur.ShaderBuilder) + luaunit.assertNotNil(Blur.ShaderBuilder.build) +end + +function TestBlur:testShaderBuilderWithMinimalTaps() + -- Should work with minimum taps (3) + local shader = Blur.ShaderBuilder.build(3, 1.0, "weighted", -1) + luaunit.assertNotNil(shader) +end + +function TestBlur:testShaderBuilderWithFractionalTaps() + -- Should floor fractional taps to nearest odd number + local shader = Blur.ShaderBuilder.build(4.7, 1.0, "weighted", -1) + luaunit.assertNotNil(shader) +end + +function TestBlur:testShaderBuilderWithCenterOffset() + -- Should work with center offset type + local shader = Blur.ShaderBuilder.build(7, 1.0, "center", -1) + luaunit.assertNotNil(shader) +end + +function TestBlur:testShaderBuilderWithZeroSigma() + -- Should clamp sigma to minimum 1 + local shader = Blur.ShaderBuilder.build(7, 1.0, "weighted", 0) + luaunit.assertNotNil(shader) +end + +function TestBlur:testShaderBuilderWithNegativeSigma() + -- Should auto-calculate sigma when negative + local shader = Blur.ShaderBuilder.build(7, 1.0, "weighted", -1) + luaunit.assertNotNil(shader) +end + +function TestBlur:testShaderBuilderWithLargeTaps() + -- Should work with large tap count + local shader = Blur.ShaderBuilder.build(21, 1.0, "weighted", -1) + luaunit.assertNotNil(shader) +end + +function TestBlur:testShaderBuilderWithZeroOffset() + -- Should work with zero offset + local shader = Blur.ShaderBuilder.build(7, 0.0, "weighted", -1) + luaunit.assertNotNil(shader) +end + +function TestBlur:testShaderBuilderWithLargeOffset() + -- Should work with large offset + local shader = Blur.ShaderBuilder.build(7, 10.0, "weighted", -1) + luaunit.assertNotNil(shader) +end + +-- ============================================================================ +-- Initialization Tests +-- ============================================================================ + +function TestBlur:testInitWithErrorHandler() + -- Should accept ErrorHandler dependency + Blur.init({ ErrorHandler = ErrorHandler }) + luaunit.assertNotNil(Blur._ErrorHandler) +end + +function TestBlur:testInitWithNilDeps() + -- Should handle nil deps gracefully + Blur.init(nil) + luaunit.assertTrue(true) -- Should not crash +end + +function TestBlur:testInitWithEmptyTable() + -- Should handle empty deps table + Blur.init({}) + luaunit.assertTrue(true) -- Should not crash +end + if not _G.RUNNING_ALL_TESTS then os.exit(luaunit.LuaUnit.run()) end diff --git a/testing/__tests__/element_test.lua b/testing/__tests__/element_test.lua index 71a28c9..af6272b 100644 --- a/testing/__tests__/element_test.lua +++ b/testing/__tests__/element_test.lua @@ -1282,18 +1282,6 @@ function TestElementUnhappyPaths:test_element_zero_dimensions() luaunit.assertNotNil(element) end --- Test: Element with extreme dimensions -function TestElementUnhappyPaths:test_element_extreme_dimensions() - local element = FlexLove.new({ - id = "huge", - x = 0, - y = 0, - width = 1000000, - height = 1000000, - }) - luaunit.assertNotNil(element) -end - -- Test: Element with invalid opacity values function TestElementUnhappyPaths:test_element_invalid_opacity() -- Opacity > 1 @@ -1524,19 +1512,6 @@ function TestElementUnhappyPaths:test_clear_children_twice() luaunit.assertEquals(#parent.children, 0) end --- Test: Element contains with extreme coordinates -function TestElementUnhappyPaths:test_contains_extreme_coordinates() - local element = FlexLove.new({ - id = "test", - x = 10, - y = 20, - width = 100, - height = 50, - }) - - luaunit.assertFalse(element:contains(math.huge, math.huge)) - luaunit.assertFalse(element:contains(-math.huge, -math.huge)) -end -- Test: Element contains with NaN coordinates function TestElementUnhappyPaths:test_contains_nan_coordinates() @@ -1567,23 +1542,6 @@ function TestElementUnhappyPaths:test_scroll_without_manager() luaunit.assertTrue(true) end --- Test: Element setScrollPosition with extreme values -function TestElementUnhappyPaths:test_scroll_extreme_values() - local element = FlexLove.new({ - id = "scrollable", - width = 200, - height = 200, - overflow = "scroll", - }) - - element:setScrollPosition(1000000, 1000000) -- Should clamp - luaunit.assertTrue(true) - - element:setScrollPosition(-1000000, -1000000) -- Should clamp to 0 - local scrollX, scrollY = element:getScrollPosition() - luaunit.assertEquals(scrollX, 0) - luaunit.assertEquals(scrollY, 0) -end -- Test: Element scrollBy with nil values function TestElementUnhappyPaths:test_scroll_by_nil() @@ -1776,15 +1734,6 @@ function TestElementUnhappyPaths:test_invalid_margin() margin = "invalid", }) luaunit.assertNotNil(element) - - -- Huge margin - element = FlexLove.new({ - id = "test2", - width = 100, - height = 100, - margin = { top = 1000000, left = 1000000, right = 1000000, bottom = 1000000 }, - }) - luaunit.assertNotNil(element) end -- Test: Element with invalid gap value @@ -1798,26 +1747,7 @@ function TestElementUnhappyPaths:test_invalid_gap() gap = -10, }) luaunit.assertNotNil(element) - - -- Huge gap - element = FlexLove.new({ - id = "test2", - width = 300, - height = 200, - positioning = "flex", - gap = 1000000, - }) - luaunit.assertNotNil(element) end - --- Test: Element with invalid grid properties -function TestElementUnhappyPaths:test_invalid_grid_properties() - -- Zero rows/columns - local element = FlexLove.new({ - id = "test", - width = 300, - height = 200, - positioning = "grid", gridRows = 0, gridColumns = 0, }) @@ -1860,19 +1790,6 @@ function TestElementUnhappyPaths:test_set_text_nil() luaunit.assertNil(element.text) end --- Test: Element setText with extreme length -function TestElementUnhappyPaths:test_set_text_extreme_length() - local element = FlexLove.new({ - id = "text", - width = 100, - height = 100, - text = "Initial", - }) - - local longText = string.rep("a", 100000) - element:setText(longText) - luaunit.assertEquals(element.text, longText) -end -- Test: Element with conflicting size constraints function TestElementUnhappyPaths:test_conflicting_size_constraints() @@ -1999,6 +1916,166 @@ function TestElementUnhappyPaths:test_max_length_negative() luaunit.assertNotNil(element) end +-- Test suite for convenience API features +TestConvenienceAPI = {} + +function TestConvenienceAPI:setUp() + FlexLove.beginFrame(1920, 1080) +end + +function TestConvenienceAPI:tearDown() + FlexLove.endFrame() +end + +-- Test: flexDirection "row" converts to "horizontal" +function TestConvenienceAPI:test_flexDirection_row_converts() + local element = FlexLove.new({ + id = "test_row", + width = 200, + height = 100, + positioning = "flex", + flexDirection = "row", + }) + + luaunit.assertNotNil(element) + luaunit.assertEquals(element.flexDirection, "horizontal") +end + +-- Test: flexDirection "column" converts to "vertical" +function TestConvenienceAPI:test_flexDirection_column_converts() + local element = FlexLove.new({ + id = "test_column", + width = 200, + height = 100, + positioning = "flex", + flexDirection = "column", + }) + + luaunit.assertNotNil(element) + luaunit.assertEquals(element.flexDirection, "vertical") +end + +-- Test: Single number padding expands to all sides +function TestConvenienceAPI:test_padding_single_number() + local element = FlexLove.new({ + id = "test_padding_num", + width = 200, + height = 100, + padding = 10, + }) + + luaunit.assertNotNil(element) + luaunit.assertEquals(element.padding.top, 10) + luaunit.assertEquals(element.padding.right, 10) + luaunit.assertEquals(element.padding.bottom, 10) + luaunit.assertEquals(element.padding.left, 10) +end + +-- Test: Single string padding expands to all sides +function TestConvenienceAPI:test_padding_single_string() + local element = FlexLove.new({ + id = "test_padding_str", + width = 200, + height = 100, + padding = "5%", + }) + + luaunit.assertNotNil(element) + -- All sides should be 5% of the element's dimensions + -- For width: 5% of 200 = 10, for height: 5% of 100 = 5 + luaunit.assertEquals(element.padding.left, 10) + luaunit.assertEquals(element.padding.right, 10) + luaunit.assertEquals(element.padding.top, 5) + luaunit.assertEquals(element.padding.bottom, 5) +end + +-- Test: Single number margin expands to all sides +function TestConvenienceAPI:test_margin_single_number() + local parent = FlexLove.new({ + id = "parent", + width = 400, + height = 300, + }) + + local element = FlexLove.new({ + id = "test_margin_num", + parent = parent, + width = 100, + height = 100, + margin = 15, + }) + + luaunit.assertNotNil(element) + luaunit.assertEquals(element.margin.top, 15) + luaunit.assertEquals(element.margin.right, 15) + luaunit.assertEquals(element.margin.bottom, 15) + luaunit.assertEquals(element.margin.left, 15) +end + +-- Test: Single string margin expands to all sides +function TestConvenienceAPI:test_margin_single_string() + local parent = FlexLove.new({ + id = "parent", + width = 400, + height = 300, + }) + + local element = FlexLove.new({ + id = "test_margin_str", + parent = parent, + width = 100, + height = 100, + margin = "10%", + }) + + luaunit.assertNotNil(element) + -- Margin percentages are relative to parent dimensions + -- 10% of parent width 400 = 40, 10% of parent height 300 = 30 + luaunit.assertEquals(element.margin.left, 40) + luaunit.assertEquals(element.margin.right, 40) + luaunit.assertEquals(element.margin.top, 30) + luaunit.assertEquals(element.margin.bottom, 30) +end + +-- Test: Table padding still works (backward compatibility) +function TestConvenienceAPI:test_padding_table_still_works() + local element = FlexLove.new({ + id = "test_padding_table", + width = 200, + height = 100, + padding = { top = 5, right = 10, bottom = 15, left = 20 }, + }) + + luaunit.assertNotNil(element) + luaunit.assertEquals(element.padding.top, 5) + luaunit.assertEquals(element.padding.right, 10) + luaunit.assertEquals(element.padding.bottom, 15) + luaunit.assertEquals(element.padding.left, 20) +end + +-- Test: Table margin still works (backward compatibility) +function TestConvenienceAPI:test_margin_table_still_works() + local parent = FlexLove.new({ + id = "parent", + width = 400, + height = 300, + }) + + local element = FlexLove.new({ + id = "test_margin_table", + parent = parent, + width = 100, + height = 100, + margin = { top = 5, right = 10, bottom = 15, left = 20 }, + }) + + luaunit.assertNotNil(element) + luaunit.assertEquals(element.margin.top, 5) + luaunit.assertEquals(element.margin.right, 10) + luaunit.assertEquals(element.margin.bottom, 15) + luaunit.assertEquals(element.margin.left, 20) +end + if not _G.RUNNING_ALL_TESTS then os.exit(luaunit.LuaUnit.run()) end diff --git a/testing/__tests__/flexlove_test.lua b/testing/__tests__/flexlove_test.lua index 2a78a65..e98865b 100644 --- a/testing/__tests__/flexlove_test.lua +++ b/testing/__tests__/flexlove_test.lua @@ -997,9 +997,6 @@ function TestFlexLoveUnhappyPaths:testNewWithInvalidPosition() local element = FlexLove.new({ x = -1000, y = -1000, width = 100, height = 100 }) luaunit.assertNotNil(element) - -- Extreme positions - element = FlexLove.new({ x = 1000000, y = 1000000, width = 100, height = 100 }) - luaunit.assertNotNil(element) end -- Test: new() with circular parent reference @@ -1131,12 +1128,6 @@ end function TestFlexLoveUnhappyPaths:testWheelMovedWithInvalidValues() FlexLove.setMode("retained") - -- Extreme values - FlexLove.wheelmoved(1000000, 1000000) - luaunit.assertTrue(true) - - FlexLove.wheelmoved(-1000000, -1000000) - luaunit.assertTrue(true) -- nil values local success = pcall(function() @@ -1207,9 +1198,6 @@ function TestFlexLoveUnhappyPaths:testGetElementAtPositionWithInvalidCoords() local element = FlexLove.getElementAtPosition(-100, -100) luaunit.assertNil(element) - -- Extreme coordinates - element = FlexLove.getElementAtPosition(1000000, 1000000) - luaunit.assertNil(element) -- nil coordinates local success = pcall(function() @@ -1278,17 +1266,6 @@ function TestFlexLoveUnhappyPaths:testStateOperationsInRetainedMode() end -- Test: Extreme z-index values -function TestFlexLoveUnhappyPaths:testExtremeZIndexValues() - FlexLove.setMode("retained") - - local element1 = FlexLove.new({ width = 100, height = 100, z = -1000000 }) - local element2 = FlexLove.new({ width = 100, height = 100, z = 1000000 }) - - luaunit.assertNotNil(element1) - luaunit.assertNotNil(element2) - - FlexLove.draw() -- Should not crash during z-index sorting -end -- Test: Creating deeply nested element hierarchy function TestFlexLoveUnhappyPaths:testDeeplyNestedHierarchy() diff --git a/testing/__tests__/scroll_manager_edge_cases_test.lua b/testing/__tests__/scroll_manager_edge_cases_test.lua new file mode 100644 index 0000000..e624125 --- /dev/null +++ b/testing/__tests__/scroll_manager_edge_cases_test.lua @@ -0,0 +1,1031 @@ +-- 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 warn but not crash + sm:detectOverflow() + -- No assertion - just ensure no crash +end + +function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithoutElement() + local sm = createScrollManager({}) + local dims = sm:calculateScrollbarDimensions() + luaunit.assertNotNil(dims) + luaunit.assertFalse(dims.vertical.visible) + luaunit.assertFalse(dims.horizontal.visible) +end + +function TestScrollManagerEdgeCases:testGetScrollbarAtPositionWithoutElement() + local sm = createScrollManager({}) + local result = sm:getScrollbarAtPosition(50, 50) + luaunit.assertNil(result) +end + +function TestScrollManagerEdgeCases:testHandleMousePressWithoutElement() + local sm = createScrollManager({}) + local consumed = sm:handleMousePress(50, 50, 1) + luaunit.assertFalse(consumed) +end + +-- ============================================================================ +-- detectOverflow Edge Cases +-- ============================================================================ + +function TestScrollManagerEdgeCases:testDetectOverflowWithNoChildren() + local sm = createScrollManager({ overflow = "auto" }) + local element = createMockElement(200, 300, {}) + sm:initialize(element) + sm:detectOverflow() + + 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:initialize(element) + sm:detectOverflow() + + 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) + sm:detectOverflow() + + -- 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) + sm:detectOverflow() + + 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) + sm:detectOverflow() + + -- 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) + sm:detectOverflow() + + -- 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) + sm:detectOverflow() + + local dims = sm:calculateScrollbarDimensions() + -- 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) + sm:detectOverflow() + + local dims = sm:calculateScrollbarDimensions() + -- 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) + sm:detectOverflow() + + local dims = sm:calculateScrollbarDimensions() + -- 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) + sm:detectOverflow() + + local dims = sm:calculateScrollbarDimensions() + 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) + sm:detectOverflow() + + local dims = sm:calculateScrollbarDimensions() + -- 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) + sm:detectOverflow() + + local result = sm:getScrollbarAtPosition(-100, -100) + luaunit.assertNil(result) +end + +function TestScrollManagerEdgeCases:testGetScrollbarAtPositionWithHiddenScrollbars() + local sm = createScrollManager({ overflow = "scroll", hideScrollbars = true }) + local element = createMockElement(200, 300, {}) + sm:initialize(element) + sm:detectOverflow() + + -- Even though scrollbar exists, it's hidden so shouldn't be detected + local dims = sm:calculateScrollbarDimensions() + local result = sm:getScrollbarAtPosition(190, 50) + luaunit.assertNil(result) +end + +function TestScrollManagerEdgeCases:testHandleMousePressWithRightButton() + local sm = createScrollManager({ overflow = "scroll" }) + local element = createMockElement(200, 300, {}) + sm:initialize(element) + sm:detectOverflow() + + local consumed = sm:handleMousePress(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) + sm:detectOverflow() + + local consumed = sm:handleMousePress(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) + sm:detectOverflow() + + local consumed = sm:handleMouseMove(50, 50) + luaunit.assertFalse(consumed) +end + +function TestScrollManagerEdgeCases:testHandleMouseReleaseWithoutDragging() + local sm = createScrollManager({ overflow = "scroll" }) + local element = createMockElement(200, 300, {}) + sm:initialize(element) + sm:detectOverflow() + + 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) + sm:detectOverflow() + + 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) + sm:detectOverflow() + + sm._scrollbarHoveredVertical = true + sm._scrollbarHoveredHorizontal = true + + sm:updateHoverState(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 + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/shorthand_syntax_test.lua b/testing/__tests__/shorthand_syntax_test.lua new file mode 100644 index 0000000..0a0c496 --- /dev/null +++ b/testing/__tests__/shorthand_syntax_test.lua @@ -0,0 +1,730 @@ +-- Tests for shorthand syntax features (flexDirection aliases, margin/padding shortcuts) +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 FlexLove = require("FlexLove") + +TestShorthandSyntax = {} + +function TestShorthandSyntax:setUp() + FlexLove.init() + FlexLove.setMode("immediate") + FlexLove.beginFrame() +end + +function TestShorthandSyntax:tearDown() + FlexLove.endFrame() + FlexLove.destroy() +end + +-- ============================================================================ +-- FlexDirection Aliases Tests +-- ============================================================================ + +function TestShorthandSyntax:testFlexDirectionRowEqualsHorizontal() + -- Create two containers: one with "row", one with "horizontal" + local containerRow = FlexLove.new({ + id = "container-row", + width = 400, + height = 200, + positioning = "flex", + flexDirection = "row", + }) + + local containerHorizontal = FlexLove.new({ + id = "container-horizontal", + width = 400, + height = 200, + positioning = "flex", + flexDirection = "horizontal", + }) + + -- Both should have the same internal flexDirection value + luaunit.assertEquals(containerRow.flexDirection, "horizontal") + luaunit.assertEquals(containerHorizontal.flexDirection, "horizontal") + luaunit.assertEquals(containerRow.flexDirection, containerHorizontal.flexDirection) +end + +function TestShorthandSyntax:testFlexDirectionColumnEqualsVertical() + -- Create two containers: one with "column", one with "vertical" + local containerColumn = FlexLove.new({ + id = "container-column", + width = 200, + height = 400, + positioning = "flex", + flexDirection = "column", + }) + + local containerVertical = FlexLove.new({ + id = "container-vertical", + width = 200, + height = 400, + positioning = "flex", + flexDirection = "vertical", + }) + + -- Both should have the same internal flexDirection value + luaunit.assertEquals(containerColumn.flexDirection, "vertical") + luaunit.assertEquals(containerVertical.flexDirection, "vertical") + luaunit.assertEquals(containerColumn.flexDirection, containerVertical.flexDirection) +end + +function TestShorthandSyntax:testFlexDirectionRowLayoutMatchesHorizontal() + -- Create two containers with children: one with "row", one with "horizontal" + local containerRow = FlexLove.new({ + id = "container-row", + width = 400, + height = 200, + positioning = "flex", + flexDirection = "row", + }) + + local containerHorizontal = FlexLove.new({ + id = "container-horizontal", + width = 400, + height = 200, + positioning = "flex", + flexDirection = "horizontal", + }) + + -- Add identical children to both + for i = 1, 3 do + FlexLove.new({ + id = "child-row-" .. i, + width = 100, + height = 50, + parent = containerRow, + }) + + FlexLove.new({ + id = "child-horizontal-" .. i, + width = 100, + height = 50, + parent = containerHorizontal, + }) + end + + -- Trigger layout + FlexLove.resize(800, 600) + + -- Children should be laid out identically + for i = 1, 3 do + local childRow = containerRow.children[i] + local childHorizontal = containerHorizontal.children[i] + + luaunit.assertEquals(childRow.x, childHorizontal.x, "Child " .. i .. " x position should match") + luaunit.assertEquals(childRow.y, childHorizontal.y, "Child " .. i .. " y position should match") + luaunit.assertEquals(childRow.width, childHorizontal.width, "Child " .. i .. " width should match") + luaunit.assertEquals(childRow.height, childHorizontal.height, "Child " .. i .. " height should match") + end +end + +function TestShorthandSyntax:testFlexDirectionColumnLayoutMatchesVertical() + -- Create two containers with children: one with "column", one with "vertical" + local containerColumn = FlexLove.new({ + id = "container-column", + width = 200, + height = 400, + positioning = "flex", + flexDirection = "column", + }) + + local containerVertical = FlexLove.new({ + id = "container-vertical", + width = 200, + height = 400, + positioning = "flex", + flexDirection = "vertical", + }) + + -- Add identical children to both + for i = 1, 3 do + FlexLove.new({ + id = "child-column-" .. i, + width = 100, + height = 50, + parent = containerColumn, + }) + + FlexLove.new({ + id = "child-vertical-" .. i, + width = 100, + height = 50, + parent = containerVertical, + }) + end + + -- Trigger layout + FlexLove.resize(800, 600) + + -- Children should be laid out identically + for i = 1, 3 do + local childColumn = containerColumn.children[i] + local childVertical = containerVertical.children[i] + + luaunit.assertEquals(childColumn.x, childVertical.x, "Child " .. i .. " x position should match") + luaunit.assertEquals(childColumn.y, childVertical.y, "Child " .. i .. " y position should match") + luaunit.assertEquals(childColumn.width, childVertical.width, "Child " .. i .. " width should match") + luaunit.assertEquals(childColumn.height, childVertical.height, "Child " .. i .. " height should match") + end +end + +function TestShorthandSyntax:testFlexDirectionRowWithJustifyContent() + -- Test that "row" works with justifyContent like "horizontal" does + local containerRow = FlexLove.new({ + id = "container-row", + width = 400, + height = 200, + positioning = "flex", + flexDirection = "row", + justifyContent = "space-between", + }) + + local containerHorizontal = FlexLove.new({ + id = "container-horizontal", + width = 400, + height = 200, + positioning = "flex", + flexDirection = "horizontal", + justifyContent = "space-between", + }) + + -- Add children + for i = 1, 3 do + FlexLove.new({ + id = "child-row-" .. i, + width = 80, + height = 50, + parent = containerRow, + }) + + FlexLove.new({ + id = "child-horizontal-" .. i, + width = 80, + height = 50, + parent = containerHorizontal, + }) + end + + FlexLove.resize(800, 600) + + -- Verify space-between worked the same way + for i = 1, 3 do + local childRow = containerRow.children[i] + local childHorizontal = containerHorizontal.children[i] + luaunit.assertEquals(childRow.x, childHorizontal.x, "space-between should work identically") + end +end + +function TestShorthandSyntax:testFlexDirectionColumnWithAlignItems() + -- Test that "column" works with alignItems like "vertical" does + local containerColumn = FlexLove.new({ + id = "container-column", + width = 200, + height = 400, + positioning = "flex", + flexDirection = "column", + alignItems = "center", + }) + + local containerVertical = FlexLove.new({ + id = "container-vertical", + width = 200, + height = 400, + positioning = "flex", + flexDirection = "vertical", + alignItems = "center", + }) + + -- Add children + for i = 1, 3 do + FlexLove.new({ + id = "child-column-" .. i, + width = 80, + height = 50, + parent = containerColumn, + }) + + FlexLove.new({ + id = "child-vertical-" .. i, + width = 80, + height = 50, + parent = containerVertical, + }) + end + + FlexLove.resize(800, 600) + + -- Verify center alignment worked the same way + for i = 1, 3 do + local childColumn = containerColumn.children[i] + local childVertical = containerVertical.children[i] + luaunit.assertEquals(childColumn.x, childVertical.x, "center alignment should work identically") + end +end + +-- ============================================================================ +-- Margin Shorthand Tests +-- ============================================================================ + +function TestShorthandSyntax:testMarginNumberEqualsMarginTable() + -- Create two elements: one with margin=10, one with margin={top=10,right=10,bottom=10,left=10} + local parent = FlexLove.new({ + id = "parent", + width = 400, + height = 400, + }) + + local elementShorthand = FlexLove.new({ + id = "element-shorthand", + width = 100, + height = 100, + margin = 10, + parent = parent, + }) + + local elementExplicit = FlexLove.new({ + id = "element-explicit", + width = 100, + height = 100, + margin = { top = 10, right = 10, bottom = 10, left = 10 }, + parent = parent, + }) + + -- Both should have the same margin values + luaunit.assertEquals(elementShorthand.margin.top, 10) + luaunit.assertEquals(elementShorthand.margin.right, 10) + luaunit.assertEquals(elementShorthand.margin.bottom, 10) + luaunit.assertEquals(elementShorthand.margin.left, 10) + + luaunit.assertEquals(elementShorthand.margin.top, elementExplicit.margin.top) + luaunit.assertEquals(elementShorthand.margin.right, elementExplicit.margin.right) + luaunit.assertEquals(elementShorthand.margin.bottom, elementExplicit.margin.bottom) + luaunit.assertEquals(elementShorthand.margin.left, elementExplicit.margin.left) +end + +function TestShorthandSyntax:testMarginShorthandLayoutMatchesExplicit() + -- Create container with two children in column layout + local container = FlexLove.new({ + id = "container", + width = 400, + height = 400, + positioning = "flex", + flexDirection = "column", + }) + + local elementShorthand = FlexLove.new({ + id = "element-shorthand", + width = 100, + height = 100, + margin = 20, + parent = container, + }) + + local elementExplicit = FlexLove.new({ + id = "element-explicit", + width = 100, + height = 100, + margin = { top = 20, right = 20, bottom = 20, left = 20 }, + parent = container, + }) + + FlexLove.resize(800, 600) + + -- The explicit element should be positioned 20px below the shorthand element + -- shorthand: y=20 (top margin), height=100, bottom margin=20 → next starts at 140 + -- explicit: y=140+20=160 + luaunit.assertEquals(elementShorthand.y, 20, "Shorthand element should have top margin applied") + luaunit.assertEquals(elementExplicit.y, 160, "Explicit element should be positioned after shorthand's bottom margin") +end + +function TestShorthandSyntax:testMarginZeroShorthand() + local element = FlexLove.new({ + id = "element", + width = 100, + height = 100, + margin = 0, + }) + + luaunit.assertEquals(element.margin.top, 0) + luaunit.assertEquals(element.margin.right, 0) + luaunit.assertEquals(element.margin.bottom, 0) + luaunit.assertEquals(element.margin.left, 0) +end + +function TestShorthandSyntax:testMarginLargeValueShorthand() + local element = FlexLove.new({ + id = "element", + width = 100, + height = 100, + margin = 100, + }) + + luaunit.assertEquals(element.margin.top, 100) + luaunit.assertEquals(element.margin.right, 100) + luaunit.assertEquals(element.margin.bottom, 100) + luaunit.assertEquals(element.margin.left, 100) +end + +function TestShorthandSyntax:testMarginDecimalShorthand() + local element = FlexLove.new({ + id = "element", + width = 100, + height = 100, + margin = 15.5, + }) + + luaunit.assertEquals(element.margin.top, 15.5) + luaunit.assertEquals(element.margin.right, 15.5) + luaunit.assertEquals(element.margin.bottom, 15.5) + luaunit.assertEquals(element.margin.left, 15.5) +end + +-- ============================================================================ +-- Padding Shorthand Tests +-- ============================================================================ + +function TestShorthandSyntax:testPaddingNumberEqualsPaddingTable() + -- Create two elements: one with padding=20, one with padding={top=20,right=20,bottom=20,left=20} + local elementShorthand = FlexLove.new({ + id = "element-shorthand", + width = 200, + height = 200, + padding = 20, + }) + + local elementExplicit = FlexLove.new({ + id = "element-explicit", + width = 200, + height = 200, + padding = { top = 20, right = 20, bottom = 20, left = 20 }, + }) + + -- Both should have the same padding values + luaunit.assertEquals(elementShorthand.padding.top, 20) + luaunit.assertEquals(elementShorthand.padding.right, 20) + luaunit.assertEquals(elementShorthand.padding.bottom, 20) + luaunit.assertEquals(elementShorthand.padding.left, 20) + + luaunit.assertEquals(elementShorthand.padding.top, elementExplicit.padding.top) + luaunit.assertEquals(elementShorthand.padding.right, elementExplicit.padding.right) + luaunit.assertEquals(elementShorthand.padding.bottom, elementExplicit.padding.bottom) + luaunit.assertEquals(elementShorthand.padding.left, elementExplicit.padding.left) +end + +function TestShorthandSyntax:testPaddingShorthandAffectsContentArea() + -- Create container with padding and a child + local containerShorthand = FlexLove.new({ + id = "container-shorthand", + width = 200, + height = 200, + padding = 30, + }) + + local containerExplicit = FlexLove.new({ + id = "container-explicit", + width = 200, + height = 200, + padding = { top = 30, right = 30, bottom = 30, left = 30 }, + }) + + -- Add children + local childShorthand = FlexLove.new({ + id = "child-shorthand", + width = "100%", + height = "100%", + parent = containerShorthand, + }) + + local childExplicit = FlexLove.new({ + id = "child-explicit", + width = "100%", + height = "100%", + parent = containerExplicit, + }) + + FlexLove.resize(800, 600) + + -- Children should have the same dimensions (200 - 30*2 = 140) + luaunit.assertEquals(childShorthand.width, 140) + luaunit.assertEquals(childShorthand.height, 140) + luaunit.assertEquals(childExplicit.width, 140) + luaunit.assertEquals(childExplicit.height, 140) + + luaunit.assertEquals(childShorthand.width, childExplicit.width) + luaunit.assertEquals(childShorthand.height, childExplicit.height) +end + +function TestShorthandSyntax:testPaddingZeroShorthand() + local element = FlexLove.new({ + id = "element", + width = 100, + height = 100, + padding = 0, + }) + + luaunit.assertEquals(element.padding.top, 0) + luaunit.assertEquals(element.padding.right, 0) + luaunit.assertEquals(element.padding.bottom, 0) + luaunit.assertEquals(element.padding.left, 0) +end + +function TestShorthandSyntax:testPaddingLargeValueShorthand() + local element = FlexLove.new({ + id = "element", + width = 300, + height = 300, + padding = 50, + }) + + luaunit.assertEquals(element.padding.top, 50) + luaunit.assertEquals(element.padding.right, 50) + luaunit.assertEquals(element.padding.bottom, 50) + luaunit.assertEquals(element.padding.left, 50) +end + +function TestShorthandSyntax:testPaddingDecimalShorthand() + local element = FlexLove.new({ + id = "element", + width = 100, + height = 100, + padding = 12.5, + }) + + luaunit.assertEquals(element.padding.top, 12.5) + luaunit.assertEquals(element.padding.right, 12.5) + luaunit.assertEquals(element.padding.bottom, 12.5) + luaunit.assertEquals(element.padding.left, 12.5) +end + +-- ============================================================================ +-- Combined Tests (FlexDirection + Margin/Padding) +-- ============================================================================ + +function TestShorthandSyntax:testRowWithMarginShorthand() + local container = FlexLove.new({ + id = "container", + width = 500, + height = 200, + flexDirection = "row", -- Alias for "horizontal" + }) + + for i = 1, 3 do + FlexLove.new({ + id = "child-" .. i, + width = 100, + height = 100, + margin = 10, -- Shorthand + parent = container, + }) + end + + FlexLove.resize(800, 600) + + -- First child: x=10 (left margin) + -- Second child: x=10+100+10 (first child's margin-right) + 10 (own margin-left) = 130 + -- Third child: x=130+100+10+10 = 250 + luaunit.assertEquals(container.children[1].x, 10) + luaunit.assertEquals(container.children[2].x, 130) + luaunit.assertEquals(container.children[3].x, 250) +end + +function TestShorthandSyntax:testColumnWithPaddingShorthand() + local container = FlexLove.new({ + id = "container", + width = 200, + height = 500, + flexDirection = "column", -- Alias for "vertical" + padding = 15, -- Shorthand + }) + + for i = 1, 3 do + FlexLove.new({ + id = "child-" .. i, + width = 100, + height = 50, + parent = container, + }) + end + + FlexLove.resize(800, 600) + + -- Children should start at y=15 (top padding) + -- First child: y=15 + -- Second child: y=15+50=65 + -- Third child: y=65+50=115 + luaunit.assertEquals(container.children[1].y, 15) + luaunit.assertEquals(container.children[2].y, 65) + luaunit.assertEquals(container.children[3].y, 115) +end + +function TestShorthandSyntax:testRowAndColumnAliasesWithAllShorthands() + -- Complex test: use all shorthands together + local container = FlexLove.new({ + id = "container", + width = 600, + height = 400, + flexDirection = "row", -- Alias + padding = 20, -- Shorthand + }) + + for i = 1, 2 do + FlexLove.new({ + id = "child-" .. i, + width = 150, + height = 100, + margin = 10, -- Shorthand + parent = container, + }) + end + + FlexLove.resize(800, 600) + + -- First child: x=20 (container padding) + 10 (own margin) = 30 + -- Second child: x=30 + 150 + 10 (first child's margin-right) + 10 (own margin-left) = 200 + luaunit.assertEquals(container.children[1].x, 30) + luaunit.assertEquals(container.children[2].x, 200) + + -- Both children should be at y=20 (container padding) + 10 (own margin) = 30 + luaunit.assertEquals(container.children[1].y, 30) + luaunit.assertEquals(container.children[2].y, 30) +end + +function TestShorthandSyntax:testNestedContainersWithShorthands() + -- Test nested containers with multiple shorthand usages + local outerContainer = FlexLove.new({ + id = "outer", + width = 500, + height = 500, + flexDirection = "column", -- Alias + padding = 25, -- Shorthand + }) + + local innerContainer = FlexLove.new({ + id = "inner", + width = 400, + height = 200, + flexDirection = "row", -- Alias + margin = 15, -- Shorthand + padding = 10, -- Shorthand + parent = outerContainer, + }) + + local child = FlexLove.new({ + id = "child", + width = 100, + height = 100, + margin = 5, -- Shorthand + parent = innerContainer, + }) + + FlexLove.resize(800, 600) + + -- Inner container position: y=25 (outer padding) + 15 (own margin) = 40 + luaunit.assertEquals(innerContainer.y, 40) + + -- Child position within inner: + -- x relative to inner = 10 (inner padding) + 5 (own margin) = 15 + -- y relative to inner = 10 (inner padding) + 5 (own margin) = 15 + local expectedChildX = innerContainer.x + 15 + local expectedChildY = innerContainer.y + 15 + luaunit.assertEquals(child.x, expectedChildX) + luaunit.assertEquals(child.y, expectedChildY) +end + +-- ============================================================================ +-- Edge Cases +-- ============================================================================ + +function TestShorthandSyntax:testFlexDirectionAliasDoesNotAffectOtherValues() + local element = FlexLove.new({ + id = "element", + width = 200, + height = 200, + positioning = "flex", + flexDirection = "row", + justifyContent = "center", + alignItems = "center", + }) + + -- Using alias shouldn't affect other properties + luaunit.assertEquals(element.justifyContent, "center") + luaunit.assertEquals(element.alignItems, "center") +end + +function TestShorthandSyntax:testMarginShorthandDoesNotAffectPadding() + local element = FlexLove.new({ + id = "element", + width = 200, + height = 200, + margin = 10, + padding = { top = 5, right = 5, bottom = 5, left = 5 }, + }) + + -- Margin shorthand shouldn't affect padding + luaunit.assertEquals(element.padding.top, 5) + luaunit.assertEquals(element.padding.right, 5) + luaunit.assertEquals(element.padding.bottom, 5) + luaunit.assertEquals(element.padding.left, 5) +end + +function TestShorthandSyntax:testPaddingShorthandDoesNotAffectMargin() + local element = FlexLove.new({ + id = "element", + width = 200, + height = 200, + padding = 20, + margin = { top = 10, right = 10, bottom = 10, left = 10 }, + }) + + -- Padding shorthand shouldn't affect margin + luaunit.assertEquals(element.margin.top, 10) + luaunit.assertEquals(element.margin.right, 10) + luaunit.assertEquals(element.margin.bottom, 10) + luaunit.assertEquals(element.margin.left, 10) +end + +function TestShorthandSyntax:testBothMarginAndPaddingShorthands() + local element = FlexLove.new({ + id = "element", + width = 200, + height = 200, + margin = 15, + padding = 25, + }) + + -- Both should be expanded correctly + luaunit.assertEquals(element.margin.top, 15) + luaunit.assertEquals(element.margin.right, 15) + luaunit.assertEquals(element.margin.bottom, 15) + luaunit.assertEquals(element.margin.left, 15) + + luaunit.assertEquals(element.padding.top, 25) + luaunit.assertEquals(element.padding.right, 25) + luaunit.assertEquals(element.padding.bottom, 25) + luaunit.assertEquals(element.padding.left, 25) +end + +function TestShorthandSyntax:testNegativeMarginShorthand() + -- Negative margins should work + local element = FlexLove.new({ + id = "element", + width = 100, + height = 100, + margin = -5, + }) + + luaunit.assertEquals(element.margin.top, -5) + luaunit.assertEquals(element.margin.right, -5) + luaunit.assertEquals(element.margin.bottom, -5) + luaunit.assertEquals(element.margin.left, -5) +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/text_editor_edge_cases_test.lua b/testing/__tests__/text_editor_edge_cases_test.lua new file mode 100644 index 0000000..1ce0852 --- /dev/null +++ b/testing/__tests__/text_editor_edge_cases_test.lua @@ -0,0 +1,610 @@ +-- Edge case and unhappy path tests for TextEditor 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 TextEditor = require("modules.TextEditor") +local Color = require("modules.Color") +local utils = require("modules.utils") + +TestTextEditorEdgeCases = {} + +-- Mock dependencies +local MockContext = { + _immediateMode = false, + _focusedElement = nil, +} + +local MockStateManager = { + getState = function(id) + return nil + end, + updateState = function(id, state) end, +} + +-- Helper to create TextEditor with dependencies +local function createTextEditor(config) + config = config or {} + return TextEditor.new(config, { + Context = MockContext, + StateManager = MockStateManager, + Color = Color, + utils = utils, + }) +end + +-- Helper to create mock element +local function createMockElement() + return { + _stateId = "test-element-1", + width = 200, + height = 30, + padding = {top = 0, right = 0, bottom = 0, left = 0}, + _renderer = { + getFont = function() + return { + getWidth = function(text) return #text * 8 end, + getHeight = function() return 16 end, + } + end, + wrapLine = function(element, line, maxWidth) + return {{text = line, startIdx = 0, endIdx = #line}} + end, + }, + } +end + +-- ============================================================================ +-- Constructor Edge Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testNewWithInvalidCursorBlinkRate() + -- Negative blink rate + local editor = createTextEditor({cursorBlinkRate = -1}) + luaunit.assertEquals(editor.cursorBlinkRate, -1) -- Should accept any value +end + +function TestTextEditorEdgeCases:testNewWithZeroCursorBlinkRate() + -- Zero blink rate (would cause rapid blinking) + local editor = createTextEditor({cursorBlinkRate = 0}) + luaunit.assertEquals(editor.cursorBlinkRate, 0) +end + +function TestTextEditorEdgeCases:testNewWithVeryLargeCursorBlinkRate() + -- Very large blink rate + local editor = createTextEditor({cursorBlinkRate = 1000}) + luaunit.assertEquals(editor.cursorBlinkRate, 1000) +end + +function TestTextEditorEdgeCases:testNewWithNegativeMaxLength() + -- Negative maxLength should be ignored + local editor = createTextEditor({maxLength = -10}) + luaunit.assertEquals(editor.maxLength, -10) -- Module doesn't validate, just stores +end + +function TestTextEditorEdgeCases:testNewWithZeroMaxLength() + -- Zero maxLength (no text allowed) + local editor = createTextEditor({maxLength = 0}) + editor:setText("test") + luaunit.assertEquals(editor:getText(), "") -- Should be empty +end + +function TestTextEditorEdgeCases:testNewWithInvalidInputType() + -- Invalid input type (not validated by constructor) + local editor = createTextEditor({inputType = "invalid"}) + luaunit.assertEquals(editor.inputType, "invalid") +end + +function TestTextEditorEdgeCases:testNewWithCustomSanitizerReturnsNil() + -- Custom sanitizer that returns nil + local editor = createTextEditor({ + customSanitizer = function(text) + return nil + end, + }) + + editor:setText("test") + -- Should fallback to original text when sanitizer returns nil + luaunit.assertEquals(editor:getText(), "test") +end + +function TestTextEditorEdgeCases:testNewWithCustomSanitizerThrowsError() + -- Custom sanitizer that throws error + local editor = createTextEditor({ + customSanitizer = function(text) + error("Intentional error") + end, + }) + + -- Should error when setting text + luaunit.assertErrorMsgContains("Intentional error", function() + editor:setText("test") + end) +end + +-- ============================================================================ +-- Text Buffer Edge Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testSetTextWithEmptyString() + local editor = createTextEditor() + editor:setText("") + luaunit.assertEquals(editor:getText(), "") +end + +function TestTextEditorEdgeCases:testSetTextWithNil() + local editor = createTextEditor({text = "initial"}) + editor:setText(nil) + luaunit.assertEquals(editor:getText(), "") -- Should default to empty string +end + + +function TestTextEditorEdgeCases:testInsertTextAtInvalidPosition() + local editor = createTextEditor({text = "Hello"}) + + -- Insert at negative position (should treat as 0) + editor:insertText("X", -10) + luaunit.assertStrContains(editor:getText(), "X") +end + +function TestTextEditorEdgeCases:testInsertTextBeyondLength() + local editor = createTextEditor({text = "Hello"}) + + -- Insert beyond text length + editor:insertText("X", 1000) + luaunit.assertStrContains(editor:getText(), "X") +end + +function TestTextEditorEdgeCases:testInsertTextWithEmptyString() + local editor = createTextEditor({text = "Hello"}) + editor:insertText("", 2) + luaunit.assertEquals(editor:getText(), "Hello") -- Should remain unchanged +end + +function TestTextEditorEdgeCases:testInsertTextWhenAtMaxLength() + local editor = createTextEditor({text = "Hello", maxLength = 5}) + editor:insertText("X", 5) + luaunit.assertEquals(editor:getText(), "Hello") -- Should not insert +end + +function TestTextEditorEdgeCases:testDeleteTextWithInvertedRange() + local editor = createTextEditor({text = "Hello World"}) + editor:deleteText(10, 2) -- End before start + -- Should swap and delete + luaunit.assertEquals(#editor:getText(), 3) -- Deleted 8 characters +end + +function TestTextEditorEdgeCases:testDeleteTextBeyondBounds() + local editor = createTextEditor({text = "Hello"}) + editor:deleteText(10, 20) -- Beyond text length + luaunit.assertEquals(editor:getText(), "Hello") -- Should clamp to bounds +end + +function TestTextEditorEdgeCases:testDeleteTextWithNegativePositions() + local editor = createTextEditor({text = "Hello"}) + editor:deleteText(-5, -1) -- Negative positions + luaunit.assertEquals(editor:getText(), "Hello") -- Should clamp to 0 +end + +function TestTextEditorEdgeCases:testReplaceTextWithEmptyString() + local editor = createTextEditor({text = "Hello World"}) + editor:replaceText(0, 5, "") + luaunit.assertEquals(editor:getText(), " World") -- Should just delete +end + +function TestTextEditorEdgeCases:testReplaceTextBeyondBounds() + local editor = createTextEditor({text = "Hello"}) + editor:replaceText(10, 20, "X") + luaunit.assertStrContains(editor:getText(), "X") +end + +-- ============================================================================ +-- UTF-8 Edge Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testSetTextWithUTF8Emoji() + local editor = createTextEditor() + editor:setText("Hello 👋 World 🌍") + luaunit.assertStrContains(editor:getText(), "👋") + luaunit.assertStrContains(editor:getText(), "🌍") +end + +function TestTextEditorEdgeCases:testInsertTextWithUTF8Characters() + local editor = createTextEditor({text = "Hello"}) + editor:insertText("世界", 5) -- Chinese characters + luaunit.assertStrContains(editor:getText(), "世界") +end + +function TestTextEditorEdgeCases:testCursorPositionWithUTF8() + local editor = createTextEditor({text = "Hello👋World"}) + -- Cursor positions should be in characters, not bytes + editor:setCursorPosition(6) -- After emoji + luaunit.assertEquals(editor:getCursorPosition(), 6) +end + +function TestTextEditorEdgeCases:testDeleteTextWithUTF8() + local editor = createTextEditor({text = "Hello👋World"}) + editor:deleteText(5, 6) -- Delete emoji + luaunit.assertEquals(editor:getText(), "HelloWorld") +end + +function TestTextEditorEdgeCases:testMaxLengthWithUTF8() + local editor = createTextEditor({maxLength = 10}) + editor:setText("Hello👋👋👋👋👋") -- 10 characters including emojis + luaunit.assertTrue(utf8.len(editor:getText()) <= 10) +end + +-- ============================================================================ +-- Cursor Edge Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testSetCursorPositionNegative() + local editor = createTextEditor({text = "Hello"}) + editor:setCursorPosition(-10) + luaunit.assertEquals(editor:getCursorPosition(), 0) -- Should clamp to 0 +end + +function TestTextEditorEdgeCases:testSetCursorPositionBeyondLength() + local editor = createTextEditor({text = "Hello"}) + editor:setCursorPosition(1000) + luaunit.assertEquals(editor:getCursorPosition(), 5) -- Should clamp to length +end + +function TestTextEditorEdgeCases:testSetCursorPositionWithNonNumber() + local editor = createTextEditor({text = "Hello"}) + editor._cursorPosition = "invalid" -- Corrupt state + editor:setCursorPosition(3) + luaunit.assertEquals(editor:getCursorPosition(), 3) -- Should validate and fix +end + +function TestTextEditorEdgeCases:testMoveCursorByZero() + local editor = createTextEditor({text = "Hello"}) + editor:setCursorPosition(2) + editor:moveCursorBy(0) + luaunit.assertEquals(editor:getCursorPosition(), 2) -- Should stay same +end + +function TestTextEditorEdgeCases:testMoveCursorByLargeNegative() + local editor = createTextEditor({text = "Hello"}) + editor:setCursorPosition(2) + editor:moveCursorBy(-1000) + luaunit.assertEquals(editor:getCursorPosition(), 0) -- Should clamp to 0 +end + +function TestTextEditorEdgeCases:testMoveCursorByLargePositive() + local editor = createTextEditor({text = "Hello"}) + editor:setCursorPosition(2) + editor:moveCursorBy(1000) + luaunit.assertEquals(editor:getCursorPosition(), 5) -- Should clamp to length +end + +function TestTextEditorEdgeCases:testMoveCursorToPreviousWordAtStart() + local editor = createTextEditor({text = "Hello World"}) + editor:moveCursorToStart() + editor:moveCursorToPreviousWord() + luaunit.assertEquals(editor:getCursorPosition(), 0) -- Should stay at start +end + +function TestTextEditorEdgeCases:testMoveCursorToNextWordAtEnd() + local editor = createTextEditor({text = "Hello World"}) + editor:moveCursorToEnd() + editor:moveCursorToNextWord() + luaunit.assertEquals(editor:getCursorPosition(), 11) -- Should stay at end +end + +function TestTextEditorEdgeCases:testMoveCursorWithEmptyBuffer() + local editor = createTextEditor({text = ""}) + editor:moveCursorToStart() + luaunit.assertEquals(editor:getCursorPosition(), 0) + editor:moveCursorToEnd() + luaunit.assertEquals(editor:getCursorPosition(), 0) +end + +-- ============================================================================ +-- Selection Edge Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testSetSelectionWithInvertedRange() + local editor = createTextEditor({text = "Hello World"}) + editor:setSelection(10, 2) -- End before start + local start, endPos = editor:getSelection() + luaunit.assertTrue(start <= endPos) -- Should be swapped +end + +function TestTextEditorEdgeCases:testSetSelectionBeyondBounds() + local editor = createTextEditor({text = "Hello"}) + editor:setSelection(0, 1000) + local start, endPos = editor:getSelection() + luaunit.assertEquals(endPos, 5) -- Should clamp to length +end + +function TestTextEditorEdgeCases:testSetSelectionWithNegativePositions() + local editor = createTextEditor({text = "Hello"}) + editor:setSelection(-5, -1) + local start, endPos = editor:getSelection() + luaunit.assertEquals(start, 0) -- Should clamp to 0 + luaunit.assertEquals(endPos, 0) +end + +function TestTextEditorEdgeCases:testSetSelectionWithSameStartEnd() + local editor = createTextEditor({text = "Hello"}) + editor:setSelection(2, 2) -- Same position + luaunit.assertFalse(editor:hasSelection()) -- Should be no selection +end + +function TestTextEditorEdgeCases:testGetSelectedTextWithNoSelection() + local editor = createTextEditor({text = "Hello"}) + luaunit.assertNil(editor:getSelectedText()) +end + +function TestTextEditorEdgeCases:testDeleteSelectionWithNoSelection() + local editor = createTextEditor({text = "Hello"}) + local deleted = editor:deleteSelection() + luaunit.assertFalse(deleted) -- Should return false + luaunit.assertEquals(editor:getText(), "Hello") -- Text unchanged +end + +function TestTextEditorEdgeCases:testSelectAllWithEmptyBuffer() + local editor = createTextEditor({text = ""}) + editor:selectAll() + luaunit.assertFalse(editor:hasSelection()) -- No selection on empty text +end + +function TestTextEditorEdgeCases:testGetSelectionRectsWithEmptyBuffer() + local editor = createTextEditor({text = ""}) + local mockElement = createMockElement() + editor:initialize(mockElement) + + editor:setSelection(0, 0) + local rects = editor:_getSelectionRects(0, 0) + luaunit.assertEquals(#rects, 0) -- No rects for empty selection +end + +-- ============================================================================ +-- Focus Edge Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testFocusWithoutElement() + local editor = createTextEditor() + -- Should not error + editor:focus() + luaunit.assertTrue(editor:isFocused()) +end + +function TestTextEditorEdgeCases:testBlurWithoutElement() + local editor = createTextEditor() + editor:focus() + editor:blur() + luaunit.assertFalse(editor:isFocused()) +end + +function TestTextEditorEdgeCases:testFocusTwice() + local editor = createTextEditor() + editor:focus() + editor:focus() -- Focus again + luaunit.assertTrue(editor:isFocused()) -- Should remain focused +end + +function TestTextEditorEdgeCases:testBlurTwice() + local editor = createTextEditor() + editor:focus() + editor:blur() + editor:blur() -- Blur again + luaunit.assertFalse(editor:isFocused()) -- Should remain blurred +end + +-- ============================================================================ +-- Mouse Input Edge Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testMouseToTextPositionWithoutElement() + local editor = createTextEditor({text = "Hello"}) + local pos = editor:mouseToTextPosition(10, 10) + luaunit.assertEquals(pos, 0) -- Should return 0 without element +end + +function TestTextEditorEdgeCases:testMouseToTextPositionWithNilBuffer() + local editor = createTextEditor() + local mockElement = createMockElement() + mockElement.x = 0 + mockElement.y = 0 + editor:initialize(mockElement) + editor._textBuffer = nil + + local pos = editor:mouseToTextPosition(10, 10) + luaunit.assertEquals(pos, 0) -- Should handle nil buffer +end + +function TestTextEditorEdgeCases:testMouseToTextPositionWithNegativeCoords() + local editor = createTextEditor({text = "Hello"}) + local mockElement = createMockElement() + mockElement.x = 100 + mockElement.y = 100 + editor:initialize(mockElement) + + local pos = editor:mouseToTextPosition(-10, -10) + luaunit.assertTrue(pos >= 0) -- Should clamp to valid position +end + +function TestTextEditorEdgeCases:testHandleTextClickWithoutFocus() + local editor = createTextEditor({text = "Hello"}) + editor:handleTextClick(10, 10, 1) + -- Should not error, but also won't do anything without focus + luaunit.assertTrue(true) +end + +function TestTextEditorEdgeCases:testHandleTextDragWithoutMouseDown() + local editor = createTextEditor({text = "Hello"}) + editor:focus() + editor:handleTextDrag(20, 10) -- Drag without mouseDownPosition + -- Should not error + luaunit.assertTrue(true) +end + +function TestTextEditorEdgeCases:testHandleTextClickWithZeroClickCount() + local editor = createTextEditor({text = "Hello"}) + editor:focus() + editor:handleTextClick(10, 10, 0) + -- Should not error + luaunit.assertTrue(true) +end + +-- ============================================================================ +-- Update Edge Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testUpdateWithoutFocus() + local editor = createTextEditor() + editor:update(1.0) -- Should not update cursor blink + luaunit.assertTrue(true) -- Should not error +end + +function TestTextEditorEdgeCases:testUpdateWithNegativeDt() + local editor = createTextEditor() + editor:focus() + editor:update(-1.0) -- Negative delta time + -- Should not error + luaunit.assertTrue(true) +end + +function TestTextEditorEdgeCases:testUpdateWithZeroDt() + local editor = createTextEditor() + editor:focus() + editor:update(0) -- Zero delta time + -- Should not error + luaunit.assertTrue(true) +end + + +-- ============================================================================ +-- Key Press Edge Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testHandleKeyPressWithoutFocus() + local editor = createTextEditor({text = "Hello"}) + editor:handleKeyPress("backspace", "backspace", false) + luaunit.assertEquals(editor:getText(), "Hello") -- Should not modify +end + +function TestTextEditorEdgeCases:testHandleKeyPressBackspaceAtStart() + local editor = createTextEditor({text = "Hello"}) + editor:focus() + editor:moveCursorToStart() + editor:handleKeyPress("backspace", "backspace", false) + luaunit.assertEquals(editor:getText(), "Hello") -- Should not delete +end + +function TestTextEditorEdgeCases:testHandleKeyPressDeleteAtEnd() + local editor = createTextEditor({text = "Hello"}) + editor:focus() + editor:moveCursorToEnd() + editor:handleKeyPress("delete", "delete", false) + luaunit.assertEquals(editor:getText(), "Hello") -- Should not delete +end + +function TestTextEditorEdgeCases:testHandleKeyPressWithUnknownKey() + local editor = createTextEditor({text = "Hello"}) + editor:focus() + editor:handleKeyPress("unknownkey", "unknownkey", false) + luaunit.assertEquals(editor:getText(), "Hello") -- Should ignore +end + +-- ============================================================================ +-- Text Input Edge Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testHandleTextInputWithoutFocus() + local editor = createTextEditor({text = "Hello"}) + editor:handleTextInput("X") + luaunit.assertEquals(editor:getText(), "Hello") -- Should not insert +end + +function TestTextEditorEdgeCases:testHandleTextInputWithEmptyString() + local editor = createTextEditor({text = "Hello"}) + editor:focus() + editor:handleTextInput("") + luaunit.assertEquals(editor:getText(), "Hello") -- Should not modify +end + +function TestTextEditorEdgeCases:testHandleTextInputWithNewlineInSingleLine() + local editor = createTextEditor({text = "Hello", multiline = false, allowNewlines = false}) + editor:focus() + editor:handleTextInput("\n") + -- Should sanitize newline in single-line mode + luaunit.assertFalse(editor:getText():find("\n") ~= nil) +end + +function TestTextEditorEdgeCases:testHandleTextInputCallbackReturnsFalse() + local editor = createTextEditor({ + text = "Hello", + onTextInput = function(element, text) + return false -- Reject input + end, + }) + local mockElement = createMockElement() + editor:initialize(mockElement) + editor:focus() + editor:handleTextInput("X") + luaunit.assertEquals(editor:getText(), "Hello") -- Should not insert +end + +-- ============================================================================ +-- Special Cases +-- ============================================================================ + +function TestTextEditorEdgeCases:testPasswordModeWithEmptyText() + local editor = createTextEditor({passwordMode = true, text = ""}) + luaunit.assertEquals(editor:getText(), "") +end + +function TestTextEditorEdgeCases:testMultilineWithMaxLines() + local editor = createTextEditor({multiline = true, maxLines = 2}) + editor:setText("Line1\nLine2\nLine3\nLine4") + -- MaxLines might not be enforced by setText, depends on implementation + luaunit.assertTrue(true) +end + +function TestTextEditorEdgeCases:testTextWrapWithZeroWidth() + local editor = createTextEditor({textWrap = true}) + local mockElement = createMockElement() + mockElement.width = 0 + editor:initialize(mockElement) + editor:setText("Hello World") + -- Should handle zero width gracefully + luaunit.assertTrue(true) +end + +function TestTextEditorEdgeCases:testAutoGrowWithoutElement() + local editor = createTextEditor({autoGrow = true, multiline = true}) + editor:updateAutoGrowHeight() + -- Should not error without element + luaunit.assertTrue(true) +end + +function TestTextEditorEdgeCases:testGetCursorScreenPositionWithoutElement() + local editor = createTextEditor({text = "Hello"}) + local x, y = editor:_getCursorScreenPosition() + luaunit.assertEquals(x, 0) + luaunit.assertEquals(y, 0) +end + +function TestTextEditorEdgeCases:testSelectWordAtPositionWithEmptyText() + local editor = createTextEditor({text = ""}) + editor:_selectWordAtPosition(0) + luaunit.assertFalse(editor:hasSelection()) +end + +function TestTextEditorEdgeCases:testSelectWordAtPositionOnWhitespace() + local editor = createTextEditor({text = "Hello World"}) + editor:_selectWordAtPosition(7) -- In whitespace + -- Behavior depends on implementation + luaunit.assertTrue(true) +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/runParallel.sh b/testing/runParallel.sh deleted file mode 100755 index c0dd8f3..0000000 --- a/testing/runParallel.sh +++ /dev/null @@ -1,190 +0,0 @@ -#!/bin/bash - -# Parallel Test Runner for FlexLove -# Runs tests in parallel to speed up execution - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR/.." - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Create temp directory for test results -TEMP_DIR=$(mktemp -d) -trap "rm -rf $TEMP_DIR" EXIT - -echo "========================================" -echo "Running tests in parallel..." -echo "========================================" - -# Get all test files -TEST_FILES=( - "testing/__tests__/animation_test.lua" - "testing/__tests__/animation_properties_test.lua" - "testing/__tests__/blur_test.lua" - "testing/__tests__/critical_failures_test.lua" - "testing/__tests__/easing_test.lua" - "testing/__tests__/element_test.lua" - "testing/__tests__/event_handler_test.lua" - "testing/__tests__/flexlove_test.lua" - "testing/__tests__/font_cache_test.lua" - "testing/__tests__/grid_test.lua" - "testing/__tests__/image_cache_test.lua" - "testing/__tests__/image_renderer_test.lua" - "testing/__tests__/image_scaler_test.lua" - "testing/__tests__/image_tiling_test.lua" - "testing/__tests__/input_event_test.lua" - "testing/__tests__/keyframe_animation_test.lua" - "testing/__tests__/layout_edge_cases_test.lua" - "testing/__tests__/layout_engine_test.lua" - "testing/__tests__/ninepatch_parser_test.lua" - "testing/__tests__/ninepatch_test.lua" - "testing/__tests__/overflow_test.lua" - "testing/__tests__/path_validation_test.lua" - "testing/__tests__/performance_instrumentation_test.lua" - "testing/__tests__/performance_warnings_test.lua" - "testing/__tests__/renderer_test.lua" - "testing/__tests__/roundedrect_test.lua" - "testing/__tests__/sanitization_test.lua" - "testing/__tests__/text_editor_test.lua" - "testing/__tests__/theme_test.lua" - "testing/__tests__/touch_events_test.lua" - "testing/__tests__/transform_test.lua" - "testing/__tests__/units_test.lua" - "testing/__tests__/utils_test.lua" -) - -# Number of parallel jobs (adjust based on CPU cores) -MAX_JOBS=${MAX_JOBS:-8} - -# Function to run a single test file -run_test() { - local test_file=$1 - local test_name=$(basename "$test_file" .lua) - local output_file="$TEMP_DIR/${test_name}.out" - local status_file="$TEMP_DIR/${test_name}.status" - - # Create a wrapper script that runs the test - cat > "$TEMP_DIR/${test_name}_runner.lua" << 'EOF' -package.path = package.path .. ";./?.lua;./game/?.lua;./game/utils/?.lua;./game/components/?.lua;./game/systems/?.lua" -_G.RUNNING_ALL_TESTS = true -local luaunit = require("testing.luaunit") -EOF - - echo "dofile('$test_file')" >> "$TEMP_DIR/${test_name}_runner.lua" - echo "os.exit(luaunit.LuaUnit.run())" >> "$TEMP_DIR/${test_name}_runner.lua" - - # Run the test and capture output - if lua "$TEMP_DIR/${test_name}_runner.lua" > "$output_file" 2>&1; then - echo "0" > "$status_file" - else - echo "1" > "$status_file" - fi -} - -export -f run_test -export TEMP_DIR - -# Run tests in parallel -printf '%s\n' "${TEST_FILES[@]}" | xargs -P $MAX_JOBS -I {} bash -c 'run_test "{}"' - -# Collect results -echo "" -echo "========================================" -echo "Test Results Summary" -echo "========================================" - -total_tests=0 -passed_tests=0 -failed_tests=0 -total_successes=0 -total_failures=0 -total_errors=0 - -for test_file in "${TEST_FILES[@]}"; do - test_name=$(basename "$test_file" .lua) - output_file="$TEMP_DIR/${test_name}.out" - status_file="$TEMP_DIR/${test_name}.status" - - if [ -f "$status_file" ]; then - status=$(cat "$status_file") - - # Extract test counts from output - if grep -q "Ran.*tests" "$output_file"; then - test_line=$(grep "Ran.*tests" "$output_file") - - # Parse: "Ran X tests in Y seconds, A successes, B failures, C errors" - if [[ $test_line =~ Ran\ ([0-9]+)\ tests.*,\ ([0-9]+)\ successes.*,\ ([0-9]+)\ failures.*,\ ([0-9]+)\ errors ]]; then - tests="${BASH_REMATCH[1]}" - successes="${BASH_REMATCH[2]}" - failures="${BASH_REMATCH[3]}" - errors="${BASH_REMATCH[4]}" - - total_tests=$((total_tests + tests)) - total_successes=$((total_successes + successes)) - total_failures=$((total_failures + failures)) - total_errors=$((total_errors + errors)) - - if [ "$status" = "0" ] && [ "$failures" = "0" ] && [ "$errors" = "0" ]; then - echo -e "${GREEN}✓${NC} $test_name: $tests tests, $successes passed" - passed_tests=$((passed_tests + 1)) - else - echo -e "${RED}✗${NC} $test_name: $tests tests, $successes passed, $failures failures, $errors errors" - failed_tests=$((failed_tests + 1)) - fi - fi - else - echo -e "${RED}✗${NC} $test_name: Failed to run" - failed_tests=$((failed_tests + 1)) - fi - else - echo -e "${RED}✗${NC} $test_name: No results" - failed_tests=$((failed_tests + 1)) - fi -done - -echo "" -echo "========================================" -echo "Overall Summary" -echo "========================================" -echo "Total test files: ${#TEST_FILES[@]}" -echo -e "${GREEN}Passed: $passed_tests${NC}" -echo -e "${RED}Failed: $failed_tests${NC}" -echo "" -echo "Total tests run: $total_tests" -echo -e "${GREEN}Successes: $total_successes${NC}" -echo -e "${YELLOW}Failures: $total_failures${NC}" -echo -e "${RED}Errors: $total_errors${NC}" -echo "" - -# Show detailed output for failed tests -if [ $failed_tests -gt 0 ]; then - echo "========================================" - echo "Failed Test Details" - echo "========================================" - - for test_file in "${TEST_FILES[@]}"; do - test_name=$(basename "$test_file" .lua) - output_file="$TEMP_DIR/${test_name}.out" - status_file="$TEMP_DIR/${test_name}.status" - - if [ -f "$status_file" ] && [ "$(cat "$status_file")" != "0" ]; then - echo "" - echo "--- $test_name ---" - # Show last 20 lines of output - tail -20 "$output_file" - fi - done -fi - -# Exit with error if any tests failed -if [ $failed_tests -gt 0 ] || [ $total_errors -gt 0 ]; then - exit 1 -else - exit 0 -fi