trying to get coverage analysis to reasonable time

This commit is contained in:
Michael Freno
2025-11-20 14:27:34 -05:00
parent d0357672db
commit 92068d5315
17 changed files with 3011 additions and 380 deletions

12
.luacov
View File

@@ -13,6 +13,13 @@ return {
"tasks", "tasks",
"themes", "themes",
"luarocks", "luarocks",
"loveStub", -- Exclude LÖVE stub from coverage
},
-- Include patterns - focus coverage on core modules
include = {
"modules/",
"FlexLove%.lua",
}, },
-- Run reporter by default -- Run reporter by default
@@ -21,6 +28,7 @@ return {
-- Delete stats file after reporting -- Delete stats file after reporting
deletestats = false, deletestats = false,
-- Tick options -- Tick options - enable for better line-by-line tracking
tick = true -- Note: With cluacov this is faster than pure Lua luacov
tick = true,
} }

View File

@@ -1,3 +1,6 @@
-- Lua 5.2+ compatibility for unpack
local unpack = table.unpack or unpack
local Cache = { local Cache = {
canvases = {}, canvases = {},
quads = {}, quads = {},
@@ -368,8 +371,10 @@ end
--- Initialize Blur module with dependencies --- Initialize Blur module with dependencies
---@param deps table Dependencies: { ErrorHandler = ErrorHandler? } ---@param deps table Dependencies: { ErrorHandler = ErrorHandler? }
function Blur.init(deps) function Blur.init(deps)
if type(deps) == "table" then
Blur._ErrorHandler = deps.ErrorHandler Blur._ErrorHandler = deps.ErrorHandler
end end
end
Blur.Cache = Cache Blur.Cache = Cache
Blur.ShaderBuilder = ShaderBuilder Blur.ShaderBuilder = ShaderBuilder

View File

@@ -225,6 +225,35 @@ function Element.new(props)
Color = Element._Color, 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.children = {}
self.onEvent = props.onEvent self.onEvent = props.onEvent

View File

@@ -737,7 +737,7 @@ end
---@param suggestion string|nil Suggestion ---@param suggestion string|nil Suggestion
function ErrorHandler:_writeLog(level, levelNum, module, code, message, details, suggestion) function ErrorHandler:_writeLog(level, levelNum, module, code, message, details, suggestion)
-- Check if we should log this level -- 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 return
end end

View File

@@ -50,8 +50,8 @@ local AnimationProps = {}
---@field backgroundColor Color? -- Background color (default: transparent) ---@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 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 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 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 {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 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 text string? -- Text content to display (default: nil)
---@field textAlign TextAlign? -- Alignment of the text content (default: START) ---@field textAlign TextAlign? -- Alignment of the text content (default: START)
---@field textColor Color? -- Color of the text content (default: black or theme text color) ---@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 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 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 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 justifyContent JustifyContent? -- Alignment of items along main axis (default: FLEX_START)
---@field alignItems AlignItems? -- Alignment of items along cross axis (default: STRETCH) ---@field alignItems AlignItems? -- Alignment of items along cross axis (default: STRETCH)
---@field alignContent AlignContent? -- Alignment of lines in multi-line flex containers (default: STRETCH) ---@field alignContent AlignContent? -- Alignment of lines in multi-line flex containers (default: STRETCH)

View File

@@ -194,11 +194,29 @@ function profile.draw()
love.graphics.print("Press - to remove 10 animated elements", 10, love.graphics.getHeight() - 45) love.graphics.print("Press - to remove 10 animated elements", 10, love.graphics.getHeight() - 45)
end end
function profile.keypressed(key) function profile.keypressed(key, profiler)
if key == "=" or key == "+" then 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.animationCount = math.min(profile.maxAnimations, profile.animationCount + 10)
profile.buildLayout() profile.buildLayout()
elseif key == "-" or key == "_" then 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.animationCount = math.max(profile.minAnimations, profile.animationCount - 10)
profile.buildLayout() profile.buildLayout()
end end

View File

@@ -127,11 +127,29 @@ function profile.draw()
love.graphics.print("Press - to remove 50 elements", 10, love.graphics.getHeight() - 45) love.graphics.print("Press - to remove 50 elements", 10, love.graphics.getHeight() - 45)
end end
function profile.keypressed(key) function profile.keypressed(key, profiler)
if key == "=" or key == "+" then 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.elementCount = math.min(profile.maxElements, profile.elementCount + 50)
profile.buildLayout() profile.buildLayout()
elseif key == "-" or key == "_" then 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.elementCount = math.max(10, profile.elementCount - 50)
profile.buildLayout() profile.buildLayout()
end end

View File

@@ -152,11 +152,41 @@ function profile.draw()
love.graphics.print("Press R/T/L to toggle features", 10, love.graphics.getHeight() - 50) love.graphics.print("Press R/T/L to toggle features", 10, love.graphics.getHeight() - 50)
end end
function profile.keypressed(key) function profile.keypressed(key, profiler)
if key == "=" or key == "+" then 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.elementCount = math.min(profile.maxElements, profile.elementCount + 50)
profile.buildLayout() profile.buildLayout()
elseif key == "-" or key == "_" then 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.elementCount = math.max(profile.minElements, profile.elementCount - 50)
profile.buildLayout() profile.buildLayout()
elseif key == "r" then elseif key == "r" then

View File

@@ -335,7 +335,7 @@ function love.keypressed(key)
if state.currentProfile and type(state.currentProfile.keypressed) == "function" then if state.currentProfile and type(state.currentProfile.keypressed) == "function" then
pcall(function() pcall(function()
state.currentProfile.keypressed(key) state.currentProfile.keypressed(key, state.profiler)
end) end)
end end
end end

View File

@@ -9,6 +9,8 @@
---@field _currentFrameStart number? ---@field _currentFrameStart number?
---@field _maxHistorySize number ---@field _maxHistorySize number
---@field _lastGcCount number ---@field _lastGcCount number
---@field _snapshots table
---@field _currentSnapshot table?
local PerformanceProfiler = {} local PerformanceProfiler = {}
PerformanceProfiler.__index = PerformanceProfiler PerformanceProfiler.__index = PerformanceProfiler
@@ -29,6 +31,8 @@ function PerformanceProfiler.new(config)
self._markers = {} self._markers = {}
self._currentFrameStart = nil self._currentFrameStart = nil
self._lastGcCount = collectgarbage("count") self._lastGcCount = collectgarbage("count")
self._snapshots = {}
self._currentSnapshot = nil
return self return self
end end
@@ -379,6 +383,42 @@ function PerformanceProfiler:reset()
self._markers = {} self._markers = {}
self._currentFrameStart = nil self._currentFrameStart = nil
self._lastGcCount = collectgarbage("count") 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 end
---@return string ---@return string
@@ -571,6 +611,45 @@ function PerformanceProfiler:_saveWithIO(filepath, profileName)
end end
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, "---")
table.insert(lines, "") table.insert(lines, "")

View File

@@ -1,7 +1,14 @@
local luaunit = require("testing.luaunit") package.path = package.path .. ";./?.lua;./modules/?.lua"
require("testing.loveStub") require("testing.loveStub")
local luaunit = require("testing.luaunit")
local Blur = require("modules.Blur") local Blur = require("modules.Blur")
local ErrorHandler = require("modules.ErrorHandler")
-- Initialize ErrorHandler
ErrorHandler.init({})
Blur.init({ ErrorHandler = ErrorHandler })
TestBlur = {} TestBlur = {}
@@ -10,9 +17,11 @@ function TestBlur:setUp()
Blur.clearCache() Blur.clearCache()
end end
-- Unhappy path tests for Blur.new({quality = ) -- ============================================================================
-- Constructor Tests: Blur.new()
-- ============================================================================
function TestBlur:testNewWithNilQuality(}) function TestBlur:testNewWithNilQuality()
-- Should default to quality 5 -- Should default to quality 5
local blur = Blur.new({quality = nil}) local blur = Blur.new({quality = nil})
luaunit.assertNotNil(blur) luaunit.assertNotNil(blur)
@@ -62,19 +71,35 @@ function TestBlur:testNewEnsuresOddTaps()
end end
end end
-- Unhappy path tests for Blur.applyToRegion() function TestBlur:testNewWithEmptyProps()
-- Should work with no props table
function TestBlur:testApplyToRegionWithNilBlurInstance() local blur = Blur.new()
local called = false luaunit.assertNotNil(blur)
local drawFunc = function() luaunit.assertEquals(blur.quality, 5)
called = true
end end
luaunit.assertError(function() function TestBlur:testNewWithNilProps()
Blur.applyToRegion(nil, 50, 0, 0, 100, 100, drawFunc) -- Should work with explicit nil
end) local blur = Blur.new(nil)
luaunit.assertNotNil(blur)
luaunit.assertEquals(blur.quality, 5)
end 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() function TestBlur:testApplyToRegionWithZeroIntensity()
local blur = Blur.new({quality = 5}) local blur = Blur.new({quality = 5})
local called = false local called = false
@@ -83,7 +108,7 @@ function TestBlur:testApplyToRegionWithZeroIntensity()
end end
-- Should just call drawFunc and return early -- 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) luaunit.assertTrue(called)
end end
@@ -95,7 +120,7 @@ function TestBlur:testApplyToRegionWithNegativeIntensity()
end end
-- Should just call drawFunc and return early -- 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) luaunit.assertTrue(called)
end end
@@ -107,7 +132,7 @@ function TestBlur:testApplyToRegionWithZeroWidth()
end end
-- Should just call drawFunc and return early -- 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) luaunit.assertTrue(called)
end end
@@ -119,7 +144,7 @@ function TestBlur:testApplyToRegionWithZeroHeight()
end end
-- Should just call drawFunc and return early -- 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) luaunit.assertTrue(called)
end end
@@ -131,7 +156,7 @@ function TestBlur:testApplyToRegionWithNegativeWidth()
end end
-- Should just call drawFunc and return early -- 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) luaunit.assertTrue(called)
end end
@@ -143,54 +168,111 @@ function TestBlur:testApplyToRegionWithNegativeHeight()
end end
-- Should just call drawFunc and return early -- 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) luaunit.assertTrue(called)
end end
function TestBlur:testApplyToRegionWithIntensityOver100() function TestBlur:testApplyToRegionWithIntensityOver100()
local blur = Blur.new({quality = 5}) 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 called = false
local drawFunc = function() local drawFunc = function()
called = true called = true
end end
-- For small dimensions, we test that it doesn't error -- Should clamp intensity to 100
-- We can't fully test the rendering without full LÖVE graphics blur:applyToRegion(150, 0, 0, 100, 100, drawFunc)
luaunit.assertNotNil(blur) luaunit.assertTrue(called)
luaunit.assertTrue(true) 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 end
function TestBlur:testApplyToRegionWithNilDrawFunc() function TestBlur:testApplyToRegionWithNilDrawFunc()
local blur = Blur.new({quality = 5}) local blur = Blur.new({quality = 5})
luaunit.assertError(function() -- Should not error but warn through ErrorHandler
Blur.applyToRegion(blur, 50, 0, 0, 100, 100, nil) blur:applyToRegion(50, 0, 0, 100, 100, nil)
end) luaunit.assertTrue(true) -- Should reach here without crash
end end
-- Unhappy path tests for Blur.applyBackdrop() function TestBlur:testApplyToRegionWithNegativeCoordinates()
local blur = Blur.new({quality = 5})
function TestBlur:testApplyBackdropWithNilBlurInstance() local called = false
local mockCanvas = { local drawFunc = function()
getDimensions = function() called = true
return 100, 100
end,
}
luaunit.assertError(function()
Blur.applyBackdrop(nil, 50, 0, 0, 100, 100, mockCanvas)
end)
end 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() function TestBlur:testApplyBackdropWithZeroIntensity()
local blur = Blur.new({quality = 5}) local blur = Blur.new({quality = 5})
local mockCanvas = { local mockCanvas = {
@@ -200,7 +282,7 @@ function TestBlur:testApplyBackdropWithZeroIntensity()
} }
-- Should return early without error -- 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) luaunit.assertTrue(true)
end end
@@ -213,7 +295,7 @@ function TestBlur:testApplyBackdropWithNegativeIntensity()
} }
-- Should return early without error -- 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) luaunit.assertTrue(true)
end end
@@ -226,7 +308,7 @@ function TestBlur:testApplyBackdropWithZeroWidth()
} }
-- Should return early without error -- 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) luaunit.assertTrue(true)
end end
@@ -239,16 +321,16 @@ function TestBlur:testApplyBackdropWithZeroHeight()
} }
-- Should return early without error -- 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) luaunit.assertTrue(true)
end end
function TestBlur:testApplyBackdropWithNilCanvas() function TestBlur:testApplyBackdropWithNilCanvas()
local blur = Blur.new({quality = 5}) local blur = Blur.new({quality = 5})
luaunit.assertError(function() -- Should not error but warn through ErrorHandler
Blur.applyBackdrop(blur, 50, 0, 0, 100, 100, nil) blur:applyBackdrop(50, 0, 0, 100, 100, nil)
end) luaunit.assertTrue(true) -- Should reach here without crash
end end
function TestBlur:testApplyBackdropWithIntensityOver100() function TestBlur:testApplyBackdropWithIntensityOver100()
@@ -259,13 +341,22 @@ function TestBlur:testApplyBackdropWithIntensityOver100()
end, end,
} }
-- We can't fully test rendering without complete LÖVE graphics -- Should clamp intensity to 100
luaunit.assertNotNil(blur) blur:applyBackdrop(200, 0, 0, 100, 100, mockCanvas)
luaunit.assertNotNil(mockCanvas)
luaunit.assertTrue(true) luaunit.assertTrue(true)
end 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 blur = Blur.new({quality = 5})
local mockCanvas = { local mockCanvas = {
getDimensions = function() getDimensions = function()
@@ -273,12 +364,43 @@ function TestBlur:testApplyBackdropWithSmallDimensions()
end, end,
} }
-- We can't fully test rendering without complete LÖVE graphics -- Region starts beyond canvas bounds
luaunit.assertNotNil(blur) blur:applyBackdrop(50, 150, 150, 100, 100, mockCanvas)
luaunit.assertTrue(true) luaunit.assertTrue(true)
end 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() function TestBlur:testClearCacheDoesNotError()
-- Create some blur instances to populate cache -- Create some blur instances to populate cache
@@ -297,22 +419,109 @@ function TestBlur:testClearCacheMultipleTimes()
luaunit.assertTrue(true) luaunit.assertTrue(true)
end end
-- Edge case: intensity boundaries function TestBlur:testCacheAccessMethods()
-- Test that Cache is accessible
function TestBlur:testIntensityBoundaries() luaunit.assertNotNil(Blur.Cache)
local blur = Blur.new({quality = 5}) luaunit.assertNotNil(Blur.Cache.getCanvas)
luaunit.assertNotNil(Blur.Cache.releaseCanvas)
-- Test that various quality levels create valid blur instances luaunit.assertNotNil(Blur.Cache.getQuad)
for quality = 1, 10 do luaunit.assertNotNil(Blur.Cache.releaseQuad)
local b = Blur.new({quality = quality}) luaunit.assertNotNil(Blur.Cache.clear)
luaunit.assertNotNil(b)
luaunit.assertNotNil(b.shader)
luaunit.assertTrue(b.taps % 2 == 1) -- Taps must be odd
end end
function TestBlur:testReleaseNonExistentCanvas()
-- Should not error when releasing canvas that's not in cache
local fakeCanvas = {}
Blur.Cache.releaseCanvas(fakeCanvas)
luaunit.assertTrue(true) luaunit.assertTrue(true)
end 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 if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run()) os.exit(luaunit.LuaUnit.run())
end end

View File

@@ -1282,18 +1282,6 @@ function TestElementUnhappyPaths:test_element_zero_dimensions()
luaunit.assertNotNil(element) luaunit.assertNotNil(element)
end 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 -- Test: Element with invalid opacity values
function TestElementUnhappyPaths:test_element_invalid_opacity() function TestElementUnhappyPaths:test_element_invalid_opacity()
-- Opacity > 1 -- Opacity > 1
@@ -1524,19 +1512,6 @@ function TestElementUnhappyPaths:test_clear_children_twice()
luaunit.assertEquals(#parent.children, 0) luaunit.assertEquals(#parent.children, 0)
end 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 -- Test: Element contains with NaN coordinates
function TestElementUnhappyPaths:test_contains_nan_coordinates() function TestElementUnhappyPaths:test_contains_nan_coordinates()
@@ -1567,23 +1542,6 @@ function TestElementUnhappyPaths:test_scroll_without_manager()
luaunit.assertTrue(true) luaunit.assertTrue(true)
end 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 -- Test: Element scrollBy with nil values
function TestElementUnhappyPaths:test_scroll_by_nil() function TestElementUnhappyPaths:test_scroll_by_nil()
@@ -1776,15 +1734,6 @@ function TestElementUnhappyPaths:test_invalid_margin()
margin = "invalid", margin = "invalid",
}) })
luaunit.assertNotNil(element) 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 end
-- Test: Element with invalid gap value -- Test: Element with invalid gap value
@@ -1798,26 +1747,7 @@ function TestElementUnhappyPaths:test_invalid_gap()
gap = -10, gap = -10,
}) })
luaunit.assertNotNil(element) luaunit.assertNotNil(element)
-- Huge gap
element = FlexLove.new({
id = "test2",
width = 300,
height = 200,
positioning = "flex",
gap = 1000000,
})
luaunit.assertNotNil(element)
end 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, gridRows = 0,
gridColumns = 0, gridColumns = 0,
}) })
@@ -1860,19 +1790,6 @@ function TestElementUnhappyPaths:test_set_text_nil()
luaunit.assertNil(element.text) luaunit.assertNil(element.text)
end 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 -- Test: Element with conflicting size constraints
function TestElementUnhappyPaths:test_conflicting_size_constraints() function TestElementUnhappyPaths:test_conflicting_size_constraints()
@@ -1999,6 +1916,166 @@ function TestElementUnhappyPaths:test_max_length_negative()
luaunit.assertNotNil(element) luaunit.assertNotNil(element)
end 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 if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run()) os.exit(luaunit.LuaUnit.run())
end end

View File

@@ -997,9 +997,6 @@ function TestFlexLoveUnhappyPaths:testNewWithInvalidPosition()
local element = FlexLove.new({ x = -1000, y = -1000, width = 100, height = 100 }) local element = FlexLove.new({ x = -1000, y = -1000, width = 100, height = 100 })
luaunit.assertNotNil(element) luaunit.assertNotNil(element)
-- Extreme positions
element = FlexLove.new({ x = 1000000, y = 1000000, width = 100, height = 100 })
luaunit.assertNotNil(element)
end end
-- Test: new() with circular parent reference -- Test: new() with circular parent reference
@@ -1131,12 +1128,6 @@ end
function TestFlexLoveUnhappyPaths:testWheelMovedWithInvalidValues() function TestFlexLoveUnhappyPaths:testWheelMovedWithInvalidValues()
FlexLove.setMode("retained") FlexLove.setMode("retained")
-- Extreme values
FlexLove.wheelmoved(1000000, 1000000)
luaunit.assertTrue(true)
FlexLove.wheelmoved(-1000000, -1000000)
luaunit.assertTrue(true)
-- nil values -- nil values
local success = pcall(function() local success = pcall(function()
@@ -1207,9 +1198,6 @@ function TestFlexLoveUnhappyPaths:testGetElementAtPositionWithInvalidCoords()
local element = FlexLove.getElementAtPosition(-100, -100) local element = FlexLove.getElementAtPosition(-100, -100)
luaunit.assertNil(element) luaunit.assertNil(element)
-- Extreme coordinates
element = FlexLove.getElementAtPosition(1000000, 1000000)
luaunit.assertNil(element)
-- nil coordinates -- nil coordinates
local success = pcall(function() local success = pcall(function()
@@ -1278,17 +1266,6 @@ function TestFlexLoveUnhappyPaths:testStateOperationsInRetainedMode()
end end
-- Test: Extreme z-index values -- 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 -- Test: Creating deeply nested element hierarchy
function TestFlexLoveUnhappyPaths:testDeeplyNestedHierarchy() function TestFlexLoveUnhappyPaths:testDeeplyNestedHierarchy()

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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