Replacing errors with warns in non-critical areas
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ docs/doc.json
|
|||||||
docs/doc.md
|
docs/doc.md
|
||||||
docs/node_modules
|
docs/node_modules
|
||||||
releases/
|
releases/
|
||||||
|
*.log*
|
||||||
|
|||||||
35
FlexLove.lua
35
FlexLove.lua
@@ -9,6 +9,7 @@ local utils = req("utils")
|
|||||||
local Units = req("Units")
|
local Units = req("Units")
|
||||||
local Context = req("Context")
|
local Context = req("Context")
|
||||||
local StateManager = req("StateManager")
|
local StateManager = req("StateManager")
|
||||||
|
local ErrorCodes = req("ErrorCodes")
|
||||||
local ErrorHandler = req("ErrorHandler")
|
local ErrorHandler = req("ErrorHandler")
|
||||||
local ImageRenderer = req("ImageRenderer")
|
local ImageRenderer = req("ImageRenderer")
|
||||||
local NinePatch = req("NinePatch")
|
local NinePatch = req("NinePatch")
|
||||||
@@ -58,12 +59,32 @@ Element.defaultDependencies = {
|
|||||||
---@class FlexLove
|
---@class FlexLove
|
||||||
local flexlove = Context
|
local flexlove = Context
|
||||||
|
|
||||||
|
-- Initialize ErrorHandler with ErrorCodes dependency
|
||||||
|
ErrorHandler.init({ ErrorCodes = ErrorCodes })
|
||||||
|
|
||||||
|
-- Initialize modules that use ErrorHandler via DI
|
||||||
|
local errorHandlerDeps = { ErrorHandler = ErrorHandler }
|
||||||
|
if ImageRenderer.init then ImageRenderer.init(errorHandlerDeps) end
|
||||||
|
if ImageScaler then
|
||||||
|
local ImageScaler = req("ImageScaler")
|
||||||
|
if ImageScaler.init then ImageScaler.init(errorHandlerDeps) end
|
||||||
|
end
|
||||||
|
if NinePatch.init then NinePatch.init(errorHandlerDeps) end
|
||||||
|
local ImageDataReader = req("ImageDataReader")
|
||||||
|
if ImageDataReader.init then ImageDataReader.init(errorHandlerDeps) end
|
||||||
|
|
||||||
-- Initialize Units module with Context dependency
|
-- Initialize Units module with Context dependency
|
||||||
Units.initialize(Context)
|
Units.initialize(Context)
|
||||||
Units.initializeErrorHandler(ErrorHandler)
|
Units.initializeErrorHandler(ErrorHandler)
|
||||||
|
|
||||||
|
-- Initialize ErrorHandler for Color module
|
||||||
|
Color.initializeErrorHandler(ErrorHandler)
|
||||||
|
|
||||||
|
-- Initialize ErrorHandler for utils
|
||||||
|
utils.initializeErrorHandler(ErrorHandler)
|
||||||
|
|
||||||
-- Add version and metadata
|
-- Add version and metadata
|
||||||
flexlove._VERSION = "0.2.1"
|
flexlove._VERSION = "0.2.2"
|
||||||
flexlove._DESCRIPTION = "0I Library for LÖVE Framework based on flexbox"
|
flexlove._DESCRIPTION = "0I Library for LÖVE Framework based on flexbox"
|
||||||
flexlove._URL = "https://github.com/mikefreno/FlexLove"
|
flexlove._URL = "https://github.com/mikefreno/FlexLove"
|
||||||
flexlove._LICENSE = [[
|
flexlove._LICENSE = [[
|
||||||
@@ -90,10 +111,20 @@ flexlove._LICENSE = [[
|
|||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean}
|
---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean, errorLogFile?: string, enableErrorLogging?: boolean}
|
||||||
function flexlove.init(config)
|
function flexlove.init(config)
|
||||||
config = config or {}
|
config = config or {}
|
||||||
|
|
||||||
|
-- Configure error logging
|
||||||
|
if config.errorLogFile then
|
||||||
|
ErrorHandler.setLogTarget("file")
|
||||||
|
ErrorHandler.setLogFile(config.errorLogFile)
|
||||||
|
elseif config.enableErrorLogging == true then
|
||||||
|
-- Use default log file if logging enabled but no path specified
|
||||||
|
ErrorHandler.setLogTarget("file")
|
||||||
|
ErrorHandler.setLogFile("flexlove-errors.log")
|
||||||
|
end
|
||||||
|
|
||||||
if config.baseScale then
|
if config.baseScale then
|
||||||
flexlove.baseScale = {
|
flexlove.baseScale = {
|
||||||
width = config.baseScale.width or 1920,
|
width = config.baseScale.width or 1920,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# FlexLöve v0.2.1
|
# FlexLöve v0.2.2
|
||||||
|
|
||||||
**A comprehensive UI library providing flexbox/grid layouts, theming, animations, and event handling for LÖVE2D games.**
|
**A comprehensive UI library providing flexbox/grid layouts, theming, animations, and event handling for LÖVE2D games.**
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
--- Standardized error message formatter
|
local ErrorHandler = nil
|
||||||
|
|
||||||
|
--- Initialize ErrorHandler dependency
|
||||||
|
---@param errorHandler table The ErrorHandler module
|
||||||
|
local function initializeErrorHandler(errorHandler)
|
||||||
|
ErrorHandler = errorHandler
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Standardized error message formatter (fallback for when ErrorHandler not available)
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||||
---@param message string
|
---@param message string
|
||||||
---@return string
|
---@return string
|
||||||
@@ -77,7 +85,14 @@ function Color.fromHex(hexWithTag)
|
|||||||
local g = tonumber("0x" .. hex:sub(3, 4))
|
local g = tonumber("0x" .. hex:sub(3, 4))
|
||||||
local b = tonumber("0x" .. hex:sub(5, 6))
|
local b = tonumber("0x" .. hex:sub(5, 6))
|
||||||
if not r or not g or not b then
|
if not r or not g or not b then
|
||||||
error(formatError("Color", string.format("Invalid hex string format: '%s'. Contains invalid hex digits", hexWithTag)))
|
if ErrorHandler then
|
||||||
|
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
||||||
|
input = hexWithTag,
|
||||||
|
issue = "invalid hex digits",
|
||||||
|
fallback = "white (#FFFFFF)"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
||||||
end
|
end
|
||||||
return Color.new(r / 255, g / 255, b / 255, 1)
|
return Color.new(r / 255, g / 255, b / 255, 1)
|
||||||
elseif #hex == 8 then
|
elseif #hex == 8 then
|
||||||
@@ -86,11 +101,26 @@ function Color.fromHex(hexWithTag)
|
|||||||
local b = tonumber("0x" .. hex:sub(5, 6))
|
local b = tonumber("0x" .. hex:sub(5, 6))
|
||||||
local a = tonumber("0x" .. hex:sub(7, 8))
|
local a = tonumber("0x" .. hex:sub(7, 8))
|
||||||
if not r or not g or not b or not a then
|
if not r or not g or not b or not a then
|
||||||
error(formatError("Color", string.format("Invalid hex string format: '%s'. Contains invalid hex digits", hexWithTag)))
|
if ErrorHandler then
|
||||||
|
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
||||||
|
input = hexWithTag,
|
||||||
|
issue = "invalid hex digits",
|
||||||
|
fallback = "white (#FFFFFFFF)"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
||||||
end
|
end
|
||||||
return Color.new(r / 255, g / 255, b / 255, a / 255)
|
return Color.new(r / 255, g / 255, b / 255, a / 255)
|
||||||
else
|
else
|
||||||
error(formatError("Color", string.format("Invalid hex string format: '%s'. Expected #RRGGBB or #RRGGBBAA", hexWithTag)))
|
if ErrorHandler then
|
||||||
|
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
||||||
|
input = hexWithTag,
|
||||||
|
expected = "#RRGGBB or #RRGGBBAA",
|
||||||
|
hexLength = #hex,
|
||||||
|
fallback = "white (#FFFFFF)"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -373,4 +403,7 @@ function Color.parse(value)
|
|||||||
return Color.sanitizeColor(value, Color.new(0, 0, 0, 1))
|
return Color.sanitizeColor(value, Color.new(0, 0, 0, 1))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Export ErrorHandler initializer
|
||||||
|
Color.initializeErrorHandler = initializeErrorHandler
|
||||||
|
|
||||||
return Color
|
return Color
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ Element.__index = Element
|
|||||||
---@return Element
|
---@return Element
|
||||||
function Element.new(props, deps)
|
function Element.new(props, deps)
|
||||||
if not deps then
|
if not deps then
|
||||||
error("[Element] deps parameter is required. Pass Element.defaultDependencies from FlexLove.")
|
-- Can't use ErrorHandler yet since deps contains it
|
||||||
|
error("[FlexLove - Element] Error: deps parameter is required. Pass Element.defaultDependencies from FlexLove.")
|
||||||
end
|
end
|
||||||
|
|
||||||
local self = setmetatable({}, Element)
|
local self = setmetatable({}, Element)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
--- Error code definitions for FlexLove
|
|
||||||
--- Provides centralized error codes, descriptions, and suggested fixes
|
|
||||||
---@class ErrorCodes
|
---@class ErrorCodes
|
||||||
local ErrorCodes = {}
|
local ErrorCodes = {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,509 @@
|
|||||||
-- modules/ErrorHandler.lua
|
|
||||||
local ErrorHandler = {}
|
local ErrorHandler = {}
|
||||||
|
local ErrorCodes = nil -- Will be injected via init
|
||||||
|
|
||||||
--- Format an error or warning message
|
local LOG_LEVELS = {
|
||||||
|
CRITICAL = 1,
|
||||||
|
ERROR = 2,
|
||||||
|
WARNING = 3,
|
||||||
|
INFO = 4,
|
||||||
|
DEBUG = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
local config = {
|
||||||
|
debugMode = false,
|
||||||
|
includeStackTrace = false,
|
||||||
|
logLevel = LOG_LEVELS.WARNING, -- Default: log errors and warnings
|
||||||
|
logTarget = "console", -- Options: "console", "file", "both", "none"
|
||||||
|
logFormat = "human", -- Options: "human", "json"
|
||||||
|
logFile = "flexlove-errors.log",
|
||||||
|
maxLogSize = 10 * 1024 * 1024, -- 10MB default
|
||||||
|
maxLogFiles = 5, -- Keep 5 rotated logs
|
||||||
|
enableRotation = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Internal state
|
||||||
|
local logFileHandle = nil
|
||||||
|
local currentLogSize = 0
|
||||||
|
|
||||||
|
--- Initialize ErrorHandler with dependencies
|
||||||
|
---@param deps table Dependencies table with ErrorCodes
|
||||||
|
function ErrorHandler.init(deps)
|
||||||
|
if deps and deps.ErrorCodes then
|
||||||
|
ErrorCodes = deps.ErrorCodes
|
||||||
|
else
|
||||||
|
-- Try to require if not provided (backward compatibility)
|
||||||
|
local success, module = pcall(require, "modules.ErrorCodes")
|
||||||
|
if success then
|
||||||
|
ErrorCodes = module
|
||||||
|
else
|
||||||
|
-- Create minimal stub if ErrorCodes not available
|
||||||
|
ErrorCodes = {
|
||||||
|
get = function() return nil end,
|
||||||
|
describe = function(code) return code end,
|
||||||
|
getSuggestion = function() return "" end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set debug mode (enables stack traces and verbose output)
|
||||||
|
---@param enabled boolean Enable debug mode
|
||||||
|
function ErrorHandler.setDebugMode(enabled)
|
||||||
|
config.debugMode = enabled
|
||||||
|
config.includeStackTrace = enabled
|
||||||
|
if enabled then
|
||||||
|
config.logLevel = LOG_LEVELS.DEBUG
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set whether to include stack traces
|
||||||
|
---@param enabled boolean Enable stack traces
|
||||||
|
function ErrorHandler.setStackTrace(enabled)
|
||||||
|
config.includeStackTrace = enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set log level (minimum level to log)
|
||||||
|
---@param level string|number Log level ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG") or number
|
||||||
|
function ErrorHandler.setLogLevel(level)
|
||||||
|
if type(level) == "string" then
|
||||||
|
config.logLevel = LOG_LEVELS[level:upper()] or LOG_LEVELS.WARNING
|
||||||
|
elseif type(level) == "number" then
|
||||||
|
config.logLevel = level
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set log target
|
||||||
|
---@param target string "console", "file", "both", or "none"
|
||||||
|
function ErrorHandler.setLogTarget(target)
|
||||||
|
config.logTarget = target
|
||||||
|
-- Note: File will be opened lazily on first write
|
||||||
|
if target == "console" or target == "none" then
|
||||||
|
-- Close log file if open
|
||||||
|
if logFileHandle then
|
||||||
|
logFileHandle:close()
|
||||||
|
logFileHandle = nil
|
||||||
|
currentLogSize = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set log format
|
||||||
|
---@param format string "human" or "json"
|
||||||
|
function ErrorHandler.setLogFormat(format)
|
||||||
|
config.logFormat = format
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set log file path
|
||||||
|
---@param path string Path to log file
|
||||||
|
function ErrorHandler.setLogFile(path)
|
||||||
|
-- Close existing log file
|
||||||
|
if logFileHandle then
|
||||||
|
logFileHandle:close()
|
||||||
|
logFileHandle = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
config.logFile = path
|
||||||
|
currentLogSize = 0
|
||||||
|
|
||||||
|
-- Note: File will be opened lazily on first write
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Enable/disable log rotation
|
||||||
|
---@param enabled boolean|table Enable rotation or config table
|
||||||
|
function ErrorHandler.enableLogRotation(enabled)
|
||||||
|
if type(enabled) == "boolean" then
|
||||||
|
config.enableRotation = enabled
|
||||||
|
elseif type(enabled) == "table" then
|
||||||
|
config.enableRotation = true
|
||||||
|
if enabled.maxSize then
|
||||||
|
config.maxLogSize = enabled.maxSize
|
||||||
|
end
|
||||||
|
if enabled.maxFiles then
|
||||||
|
config.maxLogFiles = enabled.maxFiles
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get current timestamp with milliseconds
|
||||||
|
---@return string Formatted timestamp
|
||||||
|
local function getTimestamp()
|
||||||
|
local time = os.time()
|
||||||
|
local date = os.date("%Y-%m-%d %H:%M:%S", time)
|
||||||
|
-- Note: Lua doesn't have millisecond precision by default, so we approximate
|
||||||
|
return date
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Rotate log file if needed
|
||||||
|
local function rotateLogIfNeeded()
|
||||||
|
if not config.enableRotation then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if currentLogSize < config.maxLogSize then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Close current log
|
||||||
|
if logFileHandle then
|
||||||
|
logFileHandle:close()
|
||||||
|
logFileHandle = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rotate existing logs
|
||||||
|
for i = config.maxLogFiles - 1, 1, -1 do
|
||||||
|
local oldName = config.logFile .. "." .. i
|
||||||
|
local newName = config.logFile .. "." .. (i + 1)
|
||||||
|
os.rename(oldName, newName) -- Will fail silently if file doesn't exist
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Move current log to .1
|
||||||
|
os.rename(config.logFile, config.logFile .. ".1")
|
||||||
|
|
||||||
|
-- Create new log file
|
||||||
|
logFileHandle = io.open(config.logFile, "a")
|
||||||
|
currentLogSize = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Escape string for JSON
|
||||||
|
---@param str string String to escape
|
||||||
|
---@return string Escaped string
|
||||||
|
local function escapeJson(str)
|
||||||
|
str = tostring(str)
|
||||||
|
str = str:gsub("\\", "\\\\")
|
||||||
|
str = str:gsub('"', '\\"')
|
||||||
|
str = str:gsub("\n", "\\n")
|
||||||
|
str = str:gsub("\r", "\\r")
|
||||||
|
str = str:gsub("\t", "\\t")
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Format details as JSON object
|
||||||
|
---@param details table|nil Details object
|
||||||
|
---@return string JSON string
|
||||||
|
local function formatDetailsJson(details)
|
||||||
|
if not details or type(details) ~= "table" then
|
||||||
|
return "{}"
|
||||||
|
end
|
||||||
|
|
||||||
|
local parts = {}
|
||||||
|
for key, value in pairs(details) do
|
||||||
|
local jsonKey = escapeJson(tostring(key))
|
||||||
|
local jsonValue = escapeJson(tostring(value))
|
||||||
|
table.insert(parts, string.format('"%s":"%s"', jsonKey, jsonValue))
|
||||||
|
end
|
||||||
|
|
||||||
|
return "{" .. table.concat(parts, ",") .. "}"
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Format details object as readable key-value pairs
|
||||||
|
---@param details table|nil Details object
|
||||||
|
---@return string Formatted details
|
||||||
|
local function formatDetails(details)
|
||||||
|
if not details or type(details) ~= "table" then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
for key, value in pairs(details) do
|
||||||
|
local formattedKey = tostring(key):gsub("^%l", string.upper)
|
||||||
|
local formattedValue = tostring(value)
|
||||||
|
-- Truncate very long values
|
||||||
|
if #formattedValue > 100 then
|
||||||
|
formattedValue = formattedValue:sub(1, 97) .. "..."
|
||||||
|
end
|
||||||
|
table.insert(lines, string.format(" %s: %s", formattedKey, formattedValue))
|
||||||
|
end
|
||||||
|
|
||||||
|
if #lines > 0 then
|
||||||
|
return "\n\nDetails:\n" .. table.concat(lines, "\n")
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Extract and format stack trace
|
||||||
|
---@param level number Stack level to start from
|
||||||
|
---@return string Formatted stack trace
|
||||||
|
local function formatStackTrace(level)
|
||||||
|
if not config.includeStackTrace then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
local currentLevel = level or 3
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local info = debug.getinfo(currentLevel, "Sl")
|
||||||
|
if not info then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Skip internal Lua files
|
||||||
|
if info.source:match("^@") and not info.source:match("loveStub") then
|
||||||
|
local source = info.source:sub(2) -- Remove @ prefix
|
||||||
|
local location = string.format("%s:%d", source, info.currentline)
|
||||||
|
table.insert(lines, " " .. location)
|
||||||
|
end
|
||||||
|
|
||||||
|
currentLevel = currentLevel + 1
|
||||||
|
if currentLevel > level + 10 then
|
||||||
|
break
|
||||||
|
end -- Limit depth
|
||||||
|
end
|
||||||
|
|
||||||
|
if #lines > 0 then
|
||||||
|
return "\n\nStack trace:\n" .. table.concat(lines, "\n")
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Format an error or warning message with optional error code
|
||||||
---@param module string The module name (e.g., "Element", "Units", "Theme")
|
---@param module string The module name (e.g., "Element", "Units", "Theme")
|
||||||
---@param level string "Error" or "Warning"
|
---@param level string "Error" or "Warning"
|
||||||
---@param message string The error/warning message
|
---@param codeOrMessage string Error code (e.g., "VAL_001") or message
|
||||||
|
---@param messageOrDetails string|table|nil Message or details object
|
||||||
|
---@param detailsOrSuggestion table|string|nil Details or suggestion
|
||||||
|
---@param suggestionOrNil string|nil Suggestion
|
||||||
---@return string Formatted message
|
---@return string Formatted message
|
||||||
local function formatMessage(module, level, message)
|
local function formatMessage(module, level, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestionOrNil)
|
||||||
return string.format("[FlexLove - %s] %s: %s", module, level, message)
|
local code = nil
|
||||||
|
local message = codeOrMessage
|
||||||
|
local details = nil
|
||||||
|
local suggestion = nil
|
||||||
|
|
||||||
|
-- Parse arguments (support multiple signatures)
|
||||||
|
if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then
|
||||||
|
-- Called with error code
|
||||||
|
code = codeOrMessage
|
||||||
|
message = messageOrDetails or ErrorCodes.describe(code)
|
||||||
|
|
||||||
|
if type(detailsOrSuggestion) == "table" then
|
||||||
|
details = detailsOrSuggestion
|
||||||
|
suggestion = suggestionOrNil or ErrorCodes.getSuggestion(code)
|
||||||
|
elseif type(detailsOrSuggestion) == "string" then
|
||||||
|
suggestion = detailsOrSuggestion
|
||||||
|
else
|
||||||
|
suggestion = ErrorCodes.getSuggestion(code)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Called with message only (backward compatibility)
|
||||||
|
message = codeOrMessage
|
||||||
|
if type(messageOrDetails) == "table" then
|
||||||
|
details = messageOrDetails
|
||||||
|
suggestion = detailsOrSuggestion
|
||||||
|
elseif type(messageOrDetails) == "string" then
|
||||||
|
suggestion = messageOrDetails
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build formatted message
|
||||||
|
local parts = {}
|
||||||
|
|
||||||
|
-- Header: [FlexLove - Module] Level [CODE]: Message
|
||||||
|
if code then
|
||||||
|
local codeInfo = ErrorCodes.get(code)
|
||||||
|
table.insert(parts, string.format("[FlexLove - %s] %s [%s]: %s", module, level, codeInfo.code, message))
|
||||||
|
else
|
||||||
|
table.insert(parts, string.format("[FlexLove - %s] %s: %s", module, level, message))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Details section
|
||||||
|
if details then
|
||||||
|
table.insert(parts, formatDetails(details))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Suggestion section
|
||||||
|
if suggestion and suggestion ~= "" then
|
||||||
|
table.insert(parts, string.format("\n\nSuggestion: %s", suggestion))
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(parts, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Write log entry to file and/or console
|
||||||
|
---@param level string Log level
|
||||||
|
---@param levelNum number Log level number
|
||||||
|
---@param module string Module name
|
||||||
|
---@param code string|nil Error code
|
||||||
|
---@param message string Message
|
||||||
|
---@param details table|nil Details
|
||||||
|
---@param suggestion string|nil Suggestion
|
||||||
|
local function writeLog(level, levelNum, module, code, message, details, suggestion)
|
||||||
|
-- Check if we should log this level
|
||||||
|
if levelNum > config.logLevel then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local timestamp = getTimestamp()
|
||||||
|
local logEntry
|
||||||
|
|
||||||
|
if config.logFormat == "json" then
|
||||||
|
-- JSON format
|
||||||
|
local jsonParts = {
|
||||||
|
string.format('"timestamp":"%s"', escapeJson(timestamp)),
|
||||||
|
string.format('"level":"%s"', level),
|
||||||
|
string.format('"module":"%s"', escapeJson(module)),
|
||||||
|
string.format('"message":"%s"', escapeJson(message)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if code then
|
||||||
|
table.insert(jsonParts, string.format('"code":"%s"', escapeJson(code)))
|
||||||
|
end
|
||||||
|
|
||||||
|
if details then
|
||||||
|
table.insert(jsonParts, string.format('"details":%s', formatDetailsJson(details)))
|
||||||
|
end
|
||||||
|
|
||||||
|
if suggestion then
|
||||||
|
table.insert(jsonParts, string.format('"suggestion":"%s"', escapeJson(suggestion)))
|
||||||
|
end
|
||||||
|
|
||||||
|
logEntry = "{" .. table.concat(jsonParts, ",") .. "}\n"
|
||||||
|
else
|
||||||
|
-- Human-readable format
|
||||||
|
local parts = {
|
||||||
|
string.format("[%s] [%s] [%s]", timestamp, level, module),
|
||||||
|
}
|
||||||
|
|
||||||
|
if code then
|
||||||
|
table.insert(parts, string.format("[%s]", code))
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(parts, message)
|
||||||
|
logEntry = table.concat(parts, " ") .. "\n"
|
||||||
|
|
||||||
|
if details then
|
||||||
|
logEntry = logEntry .. formatDetails(details):gsub("^\n\n", "") .. "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
if suggestion then
|
||||||
|
logEntry = logEntry .. "Suggestion: " .. suggestion .. "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
logEntry = logEntry .. "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write to console
|
||||||
|
if config.logTarget == "console" or config.logTarget == "both" then
|
||||||
|
io.write(logEntry)
|
||||||
|
io.flush()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write to file
|
||||||
|
if config.logTarget == "file" or config.logTarget == "both" then
|
||||||
|
-- Lazy file opening: open on first write
|
||||||
|
if not logFileHandle then
|
||||||
|
logFileHandle = io.open(config.logFile, "a")
|
||||||
|
if logFileHandle then
|
||||||
|
-- Get current file size
|
||||||
|
local currentPos = logFileHandle:seek("end")
|
||||||
|
currentLogSize = currentPos or 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if logFileHandle then
|
||||||
|
rotateLogIfNeeded()
|
||||||
|
|
||||||
|
-- Reopen if rotation closed it
|
||||||
|
if not logFileHandle then
|
||||||
|
logFileHandle = io.open(config.logFile, "a")
|
||||||
|
end
|
||||||
|
|
||||||
|
if logFileHandle then
|
||||||
|
logFileHandle:write(logEntry)
|
||||||
|
logFileHandle:flush()
|
||||||
|
currentLogSize = currentLogSize + #logEntry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Throw a critical error (stops execution)
|
--- Throw a critical error (stops execution)
|
||||||
---@param module string The module name
|
---@param module string The module name
|
||||||
---@param message string The error message
|
---@param codeOrMessage string Error code or message
|
||||||
function ErrorHandler.error(module, message)
|
---@param messageOrDetails string|table|nil Message or details
|
||||||
error(formatMessage(module, "Error", message), 2)
|
---@param detailsOrSuggestion table|string|nil Details or suggestion
|
||||||
|
---@param suggestion string|nil Suggestion
|
||||||
|
function ErrorHandler.error(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
|
||||||
|
local formattedMessage = formatMessage(module, "Error", codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
|
||||||
|
|
||||||
|
-- Parse arguments for logging
|
||||||
|
local code = nil
|
||||||
|
local message = codeOrMessage
|
||||||
|
local details = nil
|
||||||
|
local logSuggestion = nil
|
||||||
|
|
||||||
|
if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then
|
||||||
|
code = codeOrMessage
|
||||||
|
message = messageOrDetails or ErrorCodes.describe(code)
|
||||||
|
|
||||||
|
if type(detailsOrSuggestion) == "table" then
|
||||||
|
details = detailsOrSuggestion
|
||||||
|
logSuggestion = suggestion or ErrorCodes.getSuggestion(code)
|
||||||
|
elseif type(detailsOrSuggestion) == "string" then
|
||||||
|
logSuggestion = detailsOrSuggestion
|
||||||
|
else
|
||||||
|
logSuggestion = ErrorCodes.getSuggestion(code)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
message = codeOrMessage
|
||||||
|
if type(messageOrDetails) == "table" then
|
||||||
|
details = messageOrDetails
|
||||||
|
logSuggestion = detailsOrSuggestion
|
||||||
|
elseif type(messageOrDetails) == "string" then
|
||||||
|
logSuggestion = messageOrDetails
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Log the error
|
||||||
|
writeLog("ERROR", LOG_LEVELS.ERROR, module, code, message, details, logSuggestion)
|
||||||
|
|
||||||
|
-- Add stack trace if enabled
|
||||||
|
if config.includeStackTrace then
|
||||||
|
formattedMessage = formattedMessage .. formatStackTrace(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
error(formattedMessage, 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Print a warning (non-critical, continues execution)
|
--- Print a warning (non-critical, continues execution)
|
||||||
---@param module string The module name
|
---@param module string The module name
|
||||||
---@param message string The warning message
|
---@param codeOrMessage string Warning code or message
|
||||||
function ErrorHandler.warn(module, message)
|
---@param messageOrDetails string|table|nil Message or details
|
||||||
print(formatMessage(module, "Warning", message))
|
---@param detailsOrSuggestion table|string|nil Details or suggestion
|
||||||
|
---@param suggestion string|nil Suggestion
|
||||||
|
function ErrorHandler.warn(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
|
||||||
|
-- Parse arguments for logging
|
||||||
|
local code = nil
|
||||||
|
local message = codeOrMessage
|
||||||
|
local details = nil
|
||||||
|
local logSuggestion = nil
|
||||||
|
|
||||||
|
if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then
|
||||||
|
code = codeOrMessage
|
||||||
|
message = messageOrDetails or ErrorCodes.describe(code)
|
||||||
|
|
||||||
|
if type(detailsOrSuggestion) == "table" then
|
||||||
|
details = detailsOrSuggestion
|
||||||
|
logSuggestion = suggestion or ErrorCodes.getSuggestion(code)
|
||||||
|
elseif type(detailsOrSuggestion) == "string" then
|
||||||
|
logSuggestion = detailsOrSuggestion
|
||||||
|
else
|
||||||
|
logSuggestion = ErrorCodes.getSuggestion(code)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
message = codeOrMessage
|
||||||
|
if type(messageOrDetails) == "table" then
|
||||||
|
details = messageOrDetails
|
||||||
|
logSuggestion = detailsOrSuggestion
|
||||||
|
elseif type(messageOrDetails) == "string" then
|
||||||
|
logSuggestion = messageOrDetails
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Log the warning
|
||||||
|
writeLog("WARNING", LOG_LEVELS.WARNING, module, code, message, details, logSuggestion)
|
||||||
|
|
||||||
|
local formattedMessage = formatMessage(module, "Warning", codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
|
||||||
|
print(formattedMessage)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Validate that a value is not nil
|
--- Validate that a value is not nil
|
||||||
@@ -31,7 +513,9 @@ end
|
|||||||
---@return boolean True if valid
|
---@return boolean True if valid
|
||||||
function ErrorHandler.assertNotNil(module, value, paramName)
|
function ErrorHandler.assertNotNil(module, value, paramName)
|
||||||
if value == nil then
|
if value == nil then
|
||||||
ErrorHandler.error(module, string.format("Parameter '%s' cannot be nil", paramName))
|
ErrorHandler.error(module, "VAL_003", "Required parameter missing", {
|
||||||
|
parameter = paramName,
|
||||||
|
})
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
@@ -46,10 +530,11 @@ end
|
|||||||
function ErrorHandler.assertType(module, value, expectedType, paramName)
|
function ErrorHandler.assertType(module, value, expectedType, paramName)
|
||||||
local actualType = type(value)
|
local actualType = type(value)
|
||||||
if actualType ~= expectedType then
|
if actualType ~= expectedType then
|
||||||
ErrorHandler.error(module, string.format(
|
ErrorHandler.error(module, "VAL_001", "Invalid property type", {
|
||||||
"Parameter '%s' must be %s, got %s",
|
property = paramName,
|
||||||
paramName, expectedType, actualType
|
expected = expectedType,
|
||||||
))
|
got = actualType,
|
||||||
|
})
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
@@ -64,10 +549,12 @@ end
|
|||||||
---@return boolean True if valid
|
---@return boolean True if valid
|
||||||
function ErrorHandler.assertRange(module, value, min, max, paramName)
|
function ErrorHandler.assertRange(module, value, min, max, paramName)
|
||||||
if value < min or value > max then
|
if value < min or value > max then
|
||||||
ErrorHandler.error(module, string.format(
|
ErrorHandler.error(module, "VAL_002", "Property value out of range", {
|
||||||
"Parameter '%s' must be between %s and %s, got %s",
|
property = paramName,
|
||||||
paramName, tostring(min), tostring(max), tostring(value)
|
min = tostring(min),
|
||||||
))
|
max = tostring(max),
|
||||||
|
value = tostring(value),
|
||||||
|
})
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
@@ -78,10 +565,7 @@ end
|
|||||||
---@param oldName string The deprecated name
|
---@param oldName string The deprecated name
|
||||||
---@param newName string The new name to use
|
---@param newName string The new name to use
|
||||||
function ErrorHandler.warnDeprecated(module, oldName, newName)
|
function ErrorHandler.warnDeprecated(module, oldName, newName)
|
||||||
ErrorHandler.warn(module, string.format(
|
ErrorHandler.warn(module, string.format("'%s' is deprecated. Use '%s' instead", oldName, newName))
|
||||||
"'%s' is deprecated. Use '%s' instead",
|
|
||||||
oldName, newName
|
|
||||||
))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Warn about a common mistake
|
--- Warn about a common mistake
|
||||||
@@ -89,10 +573,7 @@ end
|
|||||||
---@param issue string Description of the issue
|
---@param issue string Description of the issue
|
||||||
---@param suggestion string Suggested fix
|
---@param suggestion string Suggested fix
|
||||||
function ErrorHandler.warnCommonMistake(module, issue, suggestion)
|
function ErrorHandler.warnCommonMistake(module, issue, suggestion)
|
||||||
ErrorHandler.warn(module, string.format(
|
ErrorHandler.warn(module, string.format("%s. Suggestion: %s", issue, suggestion))
|
||||||
"%s. Suggestion: %s",
|
|
||||||
issue, suggestion
|
|
||||||
))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return ErrorHandler
|
return ErrorHandler
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
--- Standardized error message formatter
|
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
|
||||||
---@param message string -- Error message
|
|
||||||
---@return string -- Formatted error message
|
|
||||||
local function formatError(module, message)
|
|
||||||
return string.format("[FlexLove.%s] %s", module, message)
|
|
||||||
end
|
|
||||||
|
|
||||||
local ImageDataReader = {}
|
local ImageDataReader = {}
|
||||||
|
|
||||||
|
-- ErrorHandler will be injected via init
|
||||||
|
local ErrorHandler = nil
|
||||||
|
|
||||||
|
--- Initialize ImageDataReader with dependencies
|
||||||
|
---@param deps table Dependencies table with ErrorHandler
|
||||||
|
function ImageDataReader.init(deps)
|
||||||
|
if deps and deps.ErrorHandler then
|
||||||
|
ErrorHandler = deps.ErrorHandler
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
---@param imagePath string
|
---@param imagePath string
|
||||||
---@return love.ImageData
|
---@return love.ImageData
|
||||||
function ImageDataReader.loadImageData(imagePath)
|
function ImageDataReader.loadImageData(imagePath)
|
||||||
if not imagePath then
|
if not imagePath then
|
||||||
error(formatError("ImageDataReader", "Image path cannot be nil"))
|
ErrorHandler.error("ImageDataReader", "VAL_001", "Image path cannot be nil")
|
||||||
end
|
end
|
||||||
|
|
||||||
local success, result = pcall(function()
|
local success, result = pcall(function()
|
||||||
@@ -20,7 +23,10 @@ function ImageDataReader.loadImageData(imagePath)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(formatError("ImageDataReader", "Failed to load image data from '" .. imagePath .. "': " .. tostring(result)))
|
ErrorHandler.error("ImageDataReader", "RES_001", "Failed to load image data from '" .. imagePath .. "': " .. tostring(result), {
|
||||||
|
imagePath = imagePath,
|
||||||
|
error = tostring(result),
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -32,14 +38,17 @@ end
|
|||||||
---@return table -- Array of {r, g, b, a} values (0-255 range)
|
---@return table -- Array of {r, g, b, a} values (0-255 range)
|
||||||
function ImageDataReader.getRow(imageData, rowIndex)
|
function ImageDataReader.getRow(imageData, rowIndex)
|
||||||
if not imageData then
|
if not imageData then
|
||||||
error(formatError("ImageDataReader", "ImageData cannot be nil"))
|
ErrorHandler.error("ImageDataReader", "VAL_001", "ImageData cannot be nil")
|
||||||
end
|
end
|
||||||
|
|
||||||
local width = imageData:getWidth()
|
local width = imageData:getWidth()
|
||||||
local height = imageData:getHeight()
|
local height = imageData:getHeight()
|
||||||
|
|
||||||
if rowIndex < 0 or rowIndex >= height then
|
if rowIndex < 0 or rowIndex >= height then
|
||||||
error(formatError("ImageDataReader", string.format("Row index %d out of bounds (height: %d)", rowIndex, height)))
|
ErrorHandler.error("ImageDataReader", "VAL_002", string.format("Row index %d out of bounds (height: %d)", rowIndex, height), {
|
||||||
|
rowIndex = rowIndex,
|
||||||
|
height = height,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local pixels = {}
|
local pixels = {}
|
||||||
@@ -62,14 +71,17 @@ end
|
|||||||
---@return table -- Array of {r, g, b, a} values (0-255 range)
|
---@return table -- Array of {r, g, b, a} values (0-255 range)
|
||||||
function ImageDataReader.getColumn(imageData, colIndex)
|
function ImageDataReader.getColumn(imageData, colIndex)
|
||||||
if not imageData then
|
if not imageData then
|
||||||
error(formatError("ImageDataReader", "ImageData cannot be nil"))
|
ErrorHandler.error("ImageDataReader", "VAL_001", "ImageData cannot be nil")
|
||||||
end
|
end
|
||||||
|
|
||||||
local width = imageData:getWidth()
|
local width = imageData:getWidth()
|
||||||
local height = imageData:getHeight()
|
local height = imageData:getHeight()
|
||||||
|
|
||||||
if colIndex < 0 or colIndex >= width then
|
if colIndex < 0 or colIndex >= width then
|
||||||
error(formatError("ImageDataReader", string.format("Column index %d out of bounds (width: %d)", colIndex, width)))
|
ErrorHandler.error("ImageDataReader", "VAL_002", string.format("Column index %d out of bounds (width: %d)", colIndex, width), {
|
||||||
|
colIndex = colIndex,
|
||||||
|
width = width,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local pixels = {}
|
local pixels = {}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
--- Standardized error message formatter
|
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
|
||||||
---@param message string -- Error message
|
|
||||||
---@return string -- Formatted error message
|
|
||||||
local function formatError(module, message)
|
|
||||||
return string.format("[FlexLove.%s] %s", module, message)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class ImageRenderer
|
---@class ImageRenderer
|
||||||
local ImageRenderer = {}
|
local ImageRenderer = {}
|
||||||
|
|
||||||
|
-- ErrorHandler will be injected via init
|
||||||
|
local ErrorHandler = nil
|
||||||
|
|
||||||
|
--- Initialize ImageRenderer with dependencies
|
||||||
|
---@param deps table Dependencies table with ErrorHandler
|
||||||
|
function ImageRenderer.init(deps)
|
||||||
|
if deps and deps.ErrorHandler then
|
||||||
|
ErrorHandler = deps.ErrorHandler
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Calculate rendering parameters for object-fit modes
|
--- Calculate rendering parameters for object-fit modes
|
||||||
--- Returns source and destination rectangles for rendering
|
--- Returns source and destination rectangles for rendering
|
||||||
---@param imageWidth number -- Natural width of the image
|
---@param imageWidth number -- Natural width of the image
|
||||||
@@ -23,7 +26,12 @@ function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, bounds
|
|||||||
objectPosition = objectPosition or "center center"
|
objectPosition = objectPosition or "center center"
|
||||||
|
|
||||||
if imageWidth <= 0 or imageHeight <= 0 or boundsWidth <= 0 or boundsHeight <= 0 then
|
if imageWidth <= 0 or imageHeight <= 0 or boundsWidth <= 0 or boundsHeight <= 0 then
|
||||||
error(formatError("ImageRenderer", "Dimensions must be positive"))
|
ErrorHandler.error("ImageRenderer", "VAL_002", "Dimensions must be positive", {
|
||||||
|
imageWidth = imageWidth,
|
||||||
|
imageHeight = imageHeight,
|
||||||
|
boundsWidth = boundsWidth,
|
||||||
|
boundsHeight = boundsHeight,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local result = {
|
local result = {
|
||||||
@@ -104,7 +112,12 @@ function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, bounds
|
|||||||
return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "contain", objectPosition)
|
return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "contain", objectPosition)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
error(formatError("ImageRenderer", string.format("Invalid fit mode: '%s'. Must be one of: fill, contain, cover, scale-down, none", tostring(fitMode))))
|
ErrorHandler.warn("ImageRenderer", "VAL_007", string.format("Invalid fit mode: '%s'. Must be one of: fill, contain, cover, scale-down, none", tostring(fitMode)), {
|
||||||
|
fitMode = fitMode,
|
||||||
|
fallback = "fill"
|
||||||
|
})
|
||||||
|
-- Use 'fill' as fallback
|
||||||
|
return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "fill", objectPosition)
|
||||||
end
|
end
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
--- Standardized error message formatter
|
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
|
||||||
---@param message string -- Error message
|
|
||||||
---@return string -- Formatted error message
|
|
||||||
local function formatError(module, message)
|
|
||||||
return string.format("[FlexLove.%s] %s", module, message)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
-- ImageScaler
|
-- ImageScaler
|
||||||
-- ====================
|
-- ====================
|
||||||
|
|
||||||
local ImageScaler = {}
|
local ImageScaler = {}
|
||||||
|
|
||||||
|
-- ErrorHandler will be injected via init
|
||||||
|
local ErrorHandler = nil
|
||||||
|
|
||||||
|
--- Initialize ImageScaler with dependencies
|
||||||
|
---@param deps table Dependencies table with ErrorHandler
|
||||||
|
function ImageScaler.init(deps)
|
||||||
|
if deps and deps.ErrorHandler then
|
||||||
|
ErrorHandler = deps.ErrorHandler
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Scale an ImageData region using nearest-neighbor sampling
|
--- Scale an ImageData region using nearest-neighbor sampling
|
||||||
--- Produces sharp, pixelated scaling - ideal for pixel art
|
--- Produces sharp, pixelated scaling - ideal for pixel art
|
||||||
---@param sourceImageData love.ImageData -- Source image data
|
---@param sourceImageData love.ImageData -- Source image data
|
||||||
@@ -24,11 +27,21 @@ local ImageScaler = {}
|
|||||||
---@return love.ImageData -- Scaled image data
|
---@return love.ImageData -- Scaled image data
|
||||||
function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
|
function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
|
||||||
if not sourceImageData then
|
if not sourceImageData then
|
||||||
error(formatError("ImageScaler", "Source ImageData cannot be nil"))
|
ErrorHandler.error("ImageScaler", "VAL_001", "Source ImageData cannot be nil")
|
||||||
end
|
end
|
||||||
|
|
||||||
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
|
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
|
||||||
error(formatError("ImageScaler", "Dimensions must be positive"))
|
ErrorHandler.warn("ImageScaler", "VAL_002", "Dimensions must be positive", {
|
||||||
|
srcW = srcW,
|
||||||
|
srcH = srcH,
|
||||||
|
destW = destW,
|
||||||
|
destH = destH,
|
||||||
|
fallback = "1x1 transparent image"
|
||||||
|
})
|
||||||
|
-- Return a minimal 1x1 transparent image as fallback
|
||||||
|
local fallbackImageData = love.image.newImageData(1, 1)
|
||||||
|
fallbackImageData:setPixel(0, 0, 0, 0, 0, 0)
|
||||||
|
return fallbackImageData
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create destination ImageData
|
-- Create destination ImageData
|
||||||
@@ -82,11 +95,21 @@ end
|
|||||||
---@return love.ImageData -- Scaled image data
|
---@return love.ImageData -- Scaled image data
|
||||||
function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
|
function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
|
||||||
if not sourceImageData then
|
if not sourceImageData then
|
||||||
error(formatError("ImageScaler", "Source ImageData cannot be nil"))
|
ErrorHandler.error("ImageScaler", "VAL_001", "Source ImageData cannot be nil")
|
||||||
end
|
end
|
||||||
|
|
||||||
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
|
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
|
||||||
error(formatError("ImageScaler", "Dimensions must be positive"))
|
ErrorHandler.warn("ImageScaler", "VAL_002", "Dimensions must be positive", {
|
||||||
|
srcW = srcW,
|
||||||
|
srcH = srcH,
|
||||||
|
destW = destW,
|
||||||
|
destH = destH,
|
||||||
|
fallback = "1x1 transparent image"
|
||||||
|
})
|
||||||
|
-- Return a minimal 1x1 transparent image as fallback
|
||||||
|
local fallbackImageData = love.image.newImageData(1, 1)
|
||||||
|
fallbackImageData:setPixel(0, 0, 0, 0, 0, 0)
|
||||||
|
return fallbackImageData
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create destination ImageData
|
-- Create destination ImageData
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
local modulePath = (...):match("(.-)[^%.]+$")
|
local modulePath = (...):match("(.-)[^%.]+$")
|
||||||
local ImageScaler = require(modulePath .. "ImageScaler")
|
local ImageScaler = require(modulePath .. "ImageScaler")
|
||||||
|
|
||||||
--- Standardized error message formatter
|
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
|
||||||
---@param message string -- Error message
|
|
||||||
---@return string -- Formatted error message
|
|
||||||
local function formatError(module, message)
|
|
||||||
return string.format("[FlexLove.%s] %s", module, message)
|
|
||||||
end
|
|
||||||
|
|
||||||
local NinePatch = {}
|
local NinePatch = {}
|
||||||
|
|
||||||
|
-- ErrorHandler will be injected via init
|
||||||
|
local ErrorHandler = nil
|
||||||
|
|
||||||
|
--- Initialize NinePatch with dependencies
|
||||||
|
---@param deps table Dependencies table with ErrorHandler
|
||||||
|
function NinePatch.init(deps)
|
||||||
|
if deps and deps.ErrorHandler then
|
||||||
|
ErrorHandler = deps.ErrorHandler
|
||||||
|
end
|
||||||
|
-- Also initialize ImageScaler since it's a dependency
|
||||||
|
if ImageScaler.init then
|
||||||
|
ImageScaler.init(deps)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Draw a 9-patch component using Android-style rendering
|
--- Draw a 9-patch component using Android-style rendering
|
||||||
--- Corners are scaled by scaleCorners multiplier, edges stretch in one dimension only
|
--- Corners are scaled by scaleCorners multiplier, edges stretch in one dimension only
|
||||||
---@param component ThemeComponent
|
---@param component ThemeComponent
|
||||||
@@ -92,7 +99,9 @@ function NinePatch.draw(component, atlas, x, y, width, height, opacity, elementS
|
|||||||
-- Get ImageData from component (stored during theme loading)
|
-- Get ImageData from component (stored during theme loading)
|
||||||
local atlasData = component._loadedAtlasData
|
local atlasData = component._loadedAtlasData
|
||||||
if not atlasData then
|
if not atlasData then
|
||||||
error(formatError("NinePatch", "No ImageData available for atlas. Image must be loaded with safeLoadImage."))
|
ErrorHandler.error("NinePatch", "REN_007", "No ImageData available for atlas. Image must be loaded with safeLoadImage.", {
|
||||||
|
componentType = component.type,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local scaledData
|
local scaledData
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
local Renderer = {}
|
local Renderer = {}
|
||||||
Renderer.__index = Renderer
|
Renderer.__index = Renderer
|
||||||
|
|
||||||
|
-- Lazy-loaded ErrorHandler
|
||||||
|
local ErrorHandler
|
||||||
|
|
||||||
--- Create a new Renderer instance
|
--- Create a new Renderer instance
|
||||||
---@param config table Configuration table with rendering properties
|
---@param config table Configuration table with rendering properties
|
||||||
---@param deps table Dependencies {Color, RoundedRect, NinePatch, ImageRenderer, ImageCache, Theme, Blur, utils}
|
---@param deps table Dependencies {Color, RoundedRect, NinePatch, ImageRenderer, ImageCache, Theme, Blur, utils}
|
||||||
@@ -309,7 +312,12 @@ function Renderer:draw(backdropCanvas)
|
|||||||
|
|
||||||
-- Element must be initialized before drawing
|
-- Element must be initialized before drawing
|
||||||
if not self._element then
|
if not self._element then
|
||||||
error("Renderer:draw() called before initialize(). Call renderer:initialize(element) first.")
|
if not ErrorHandler then
|
||||||
|
ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
end
|
||||||
|
ErrorHandler.error("Renderer", "SYS_002", "Method called before initialization", {
|
||||||
|
method = "draw"
|
||||||
|
}, "Call renderer:initialize(element) before rendering")
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
|
|||||||
@@ -26,9 +26,13 @@
|
|||||||
---@field _scrollbarPressHandled boolean -- Track if scrollbar press was handled this frame
|
---@field _scrollbarPressHandled boolean -- Track if scrollbar press was handled this frame
|
||||||
---@field _Color table
|
---@field _Color table
|
||||||
---@field _utils table
|
---@field _utils table
|
||||||
|
---@field _ErrorHandler table?
|
||||||
local ScrollManager = {}
|
local ScrollManager = {}
|
||||||
ScrollManager.__index = ScrollManager
|
ScrollManager.__index = ScrollManager
|
||||||
|
|
||||||
|
-- Lazy-loaded ErrorHandler
|
||||||
|
local ErrorHandler
|
||||||
|
|
||||||
--- Create a new ScrollManager instance
|
--- Create a new ScrollManager instance
|
||||||
---@param config table Configuration options
|
---@param config table Configuration options
|
||||||
---@param deps table Dependencies {Color: Color module, utils: utils module}
|
---@param deps table Dependencies {Color: Color module, utils: utils module}
|
||||||
@@ -92,7 +96,12 @@ end
|
|||||||
--- Detect if content overflows container bounds
|
--- Detect if content overflows container bounds
|
||||||
function ScrollManager:detectOverflow()
|
function ScrollManager:detectOverflow()
|
||||||
if not self._element then
|
if not self._element then
|
||||||
error("ScrollManager:detectOverflow() called before initialize()")
|
if not ErrorHandler then
|
||||||
|
ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
end
|
||||||
|
ErrorHandler.error("ScrollManager", "SYS_002", "Method called before initialization", {
|
||||||
|
method = "detectOverflow"
|
||||||
|
}, "Call scrollManager:initialize(element) before using scroll methods")
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
@@ -219,7 +228,12 @@ end
|
|||||||
---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
|
---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
|
||||||
function ScrollManager:calculateScrollbarDimensions()
|
function ScrollManager:calculateScrollbarDimensions()
|
||||||
if not self._element then
|
if not self._element then
|
||||||
error("ScrollManager:calculateScrollbarDimensions() called before initialize()")
|
if not ErrorHandler then
|
||||||
|
ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
end
|
||||||
|
ErrorHandler.error("ScrollManager", "SYS_002", "Method called before initialization", {
|
||||||
|
method = "calculateScrollbarDimensions"
|
||||||
|
}, "Call scrollManager:initialize(element) before using scroll methods")
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
@@ -312,7 +326,12 @@ end
|
|||||||
---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
|
---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
|
||||||
function ScrollManager:getScrollbarAtPosition(mouseX, mouseY)
|
function ScrollManager:getScrollbarAtPosition(mouseX, mouseY)
|
||||||
if not self._element then
|
if not self._element then
|
||||||
error("ScrollManager:getScrollbarAtPosition() called before initialize()")
|
if not ErrorHandler then
|
||||||
|
ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
end
|
||||||
|
ErrorHandler.error("ScrollManager", "SYS_002", "Method called before initialization", {
|
||||||
|
method = "getScrollbarAtPosition"
|
||||||
|
}, "Call scrollManager:initialize(element) before using scroll methods")
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
@@ -381,7 +400,12 @@ end
|
|||||||
---@return boolean -- True if event was consumed
|
---@return boolean -- True if event was consumed
|
||||||
function ScrollManager:handleMousePress(mouseX, mouseY, button)
|
function ScrollManager:handleMousePress(mouseX, mouseY, button)
|
||||||
if not self._element then
|
if not self._element then
|
||||||
error("ScrollManager:handleMousePress() called before initialize()")
|
if not ErrorHandler then
|
||||||
|
ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
end
|
||||||
|
ErrorHandler.error("ScrollManager", "SYS_002", "Method called before initialization", {
|
||||||
|
method = "handleMousePress"
|
||||||
|
}, "Call scrollManager:initialize(element) before using scroll methods")
|
||||||
end
|
end
|
||||||
|
|
||||||
if button ~= 1 then
|
if button ~= 1 then
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
---@class StateManager
|
---@class StateManager
|
||||||
local StateManager = {}
|
local StateManager = {}
|
||||||
|
|
||||||
|
-- Load error handler (loaded lazily since it's in a sibling module)
|
||||||
|
local ErrorHandler
|
||||||
|
|
||||||
-- State storage: ID -> state table
|
-- State storage: ID -> state table
|
||||||
local stateStore = {}
|
local stateStore = {}
|
||||||
|
|
||||||
@@ -181,7 +184,14 @@ end
|
|||||||
---@return table state State table for the element
|
---@return table state State table for the element
|
||||||
function StateManager.getState(id, defaultState)
|
function StateManager.getState(id, defaultState)
|
||||||
if not id then
|
if not id then
|
||||||
error("StateManager.getState: id is required")
|
-- Lazy load ErrorHandler
|
||||||
|
if not ErrorHandler then
|
||||||
|
ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
end
|
||||||
|
ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", {
|
||||||
|
parameter = "id",
|
||||||
|
value = "nil"
|
||||||
|
}, "Provide a valid non-nil ID string to getState()")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create state if it doesn't exist
|
-- Create state if it doesn't exist
|
||||||
@@ -303,7 +313,14 @@ end
|
|||||||
---@param state table State to store
|
---@param state table State to store
|
||||||
function StateManager.setState(id, state)
|
function StateManager.setState(id, state)
|
||||||
if not id then
|
if not id then
|
||||||
error("StateManager.setState: id is required")
|
-- Lazy load ErrorHandler
|
||||||
|
if not ErrorHandler then
|
||||||
|
ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
end
|
||||||
|
ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", {
|
||||||
|
parameter = "id",
|
||||||
|
value = "nil"
|
||||||
|
}, "Provide a valid non-nil ID string to setState()")
|
||||||
end
|
end
|
||||||
|
|
||||||
stateStore[id] = state
|
stateStore[id] = state
|
||||||
|
|||||||
@@ -6,14 +6,7 @@ end
|
|||||||
local NinePatchParser = req("NinePatchParser")
|
local NinePatchParser = req("NinePatchParser")
|
||||||
local Color = req("Color")
|
local Color = req("Color")
|
||||||
local utils = req("utils")
|
local utils = req("utils")
|
||||||
|
local ErrorHandler = req("ErrorHandler")
|
||||||
--- Standardized error message formatter
|
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
|
||||||
---@param message string -- Error message
|
|
||||||
---@return string -- Formatted error message
|
|
||||||
local function formatError(module, message)
|
|
||||||
return string.format("[FlexLove.%s] %s", module, message)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Auto-detect the base path where FlexLove is located
|
--- Auto-detect the base path where FlexLove is located
|
||||||
---@return string modulePath, string filesystemPath
|
---@return string modulePath, string filesystemPath
|
||||||
@@ -135,7 +128,9 @@ function Theme.new(definition)
|
|||||||
-- Validate theme definition
|
-- Validate theme definition
|
||||||
local valid, err = validateThemeDefinition(definition)
|
local valid, err = validateThemeDefinition(definition)
|
||||||
if not valid then
|
if not valid then
|
||||||
error("[FlexLove] Invalid theme definition: " .. tostring(err))
|
ErrorHandler.error("Theme", "THM_001", "Invalid theme definition", {
|
||||||
|
error = tostring(err)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local self = setmetatable({}, Theme)
|
local self = setmetatable({}, Theme)
|
||||||
@@ -150,7 +145,11 @@ function Theme.new(definition)
|
|||||||
self.atlas = image
|
self.atlas = image
|
||||||
self.atlasData = imageData
|
self.atlasData = imageData
|
||||||
else
|
else
|
||||||
print("[FlexLove] Warning: Failed to load global atlas for theme '" .. definition.name .. "'" .. "(" .. loaderr .. ")")
|
ErrorHandler.warn("Theme", "RES_001", "Failed to load global atlas", {
|
||||||
|
theme = definition.name,
|
||||||
|
path = resolvedPath,
|
||||||
|
error = loaderr
|
||||||
|
})
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
self.atlas = definition.atlas
|
self.atlas = definition.atlas
|
||||||
@@ -174,7 +173,11 @@ function Theme.new(definition)
|
|||||||
local contentHeight = srcHeight - 2
|
local contentHeight = srcHeight - 2
|
||||||
|
|
||||||
if contentWidth <= 0 or contentHeight <= 0 then
|
if contentWidth <= 0 or contentHeight <= 0 then
|
||||||
error(formatError("NinePatch", "Image too small to strip border"))
|
ErrorHandler.error("Theme", "RES_002", "Nine-patch image too small", {
|
||||||
|
width = srcWidth,
|
||||||
|
height = srcHeight,
|
||||||
|
reason = "Image must be larger than 2x2 pixels to have content after stripping 1px border"
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create new ImageData for content only
|
-- Create new ImageData for content only
|
||||||
@@ -204,7 +207,11 @@ function Theme.new(definition)
|
|||||||
comp.insets = parseResult.insets
|
comp.insets = parseResult.insets
|
||||||
comp._ninePatchData = parseResult
|
comp._ninePatchData = parseResult
|
||||||
else
|
else
|
||||||
print("[FlexLove] Warning: Failed to parse 9-patch " .. errorContext .. ": " .. tostring(parseErr))
|
ErrorHandler.warn("Theme", "RES_003", "Failed to parse nine-patch image", {
|
||||||
|
context = errorContext,
|
||||||
|
path = resolvedPath,
|
||||||
|
error = tostring(parseErr)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -221,7 +228,11 @@ function Theme.new(definition)
|
|||||||
comp._loadedAtlasData = imageData
|
comp._loadedAtlasData = imageData
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
print("[FlexLove] Warning: Failed to load atlas " .. errorContext .. ": " .. tostring(loaderr))
|
ErrorHandler.warn("Theme", "RES_001", "Failed to load atlas", {
|
||||||
|
context = errorContext,
|
||||||
|
path = resolvedPath,
|
||||||
|
error = tostring(loaderr)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -310,7 +321,13 @@ function Theme.load(path)
|
|||||||
if success then
|
if success then
|
||||||
definition = result
|
definition = result
|
||||||
else
|
else
|
||||||
error("Failed to load theme '" .. path .. "'\nTried: " .. themePath .. "\nError: " .. tostring(result))
|
ErrorHandler.warn("Theme", "RES_004", "Failed to load theme file", {
|
||||||
|
theme = path,
|
||||||
|
tried = themePath,
|
||||||
|
error = tostring(result),
|
||||||
|
fallback = "nil (no theme loaded)"
|
||||||
|
}, "Check that the theme file exists in the themes/ directory or provide a valid module path")
|
||||||
|
return nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -334,7 +351,12 @@ function Theme.setActive(themeOrName)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if not activeTheme then
|
if not activeTheme then
|
||||||
error("Failed to set active theme: " .. tostring(themeOrName))
|
ErrorHandler.warn("Theme", "THM_002", "Failed to set active theme", {
|
||||||
|
theme = tostring(themeOrName),
|
||||||
|
reason = "Theme not found or not loaded",
|
||||||
|
fallback = "current theme unchanged"
|
||||||
|
}, "Ensure the theme is loaded with Theme.load() before setting it active")
|
||||||
|
-- Keep current activeTheme unchanged (fallback behavior)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ function Units.parse(value)
|
|||||||
|
|
||||||
if type(value) ~= "string" then
|
if type(value) ~= "string" then
|
||||||
if ErrorHandler then
|
if ErrorHandler then
|
||||||
ErrorHandler.error("Units", string.format("Invalid unit value type. Expected string or number, got %s", type(value)))
|
ErrorHandler.warn("Units", "VAL_001", "Invalid property type", {
|
||||||
|
property = "unit value",
|
||||||
|
expected = "string or number",
|
||||||
|
got = type(value)
|
||||||
|
}, "Using fallback: 0px")
|
||||||
|
else
|
||||||
|
print(string.format("[FlexLove - Units] Warning: Invalid unit value type. Expected string or number, got %s. Using fallback: 0px", type(value)))
|
||||||
end
|
end
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
end
|
end
|
||||||
@@ -33,7 +39,12 @@ function Units.parse(value)
|
|||||||
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
|
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
|
||||||
if validUnits[value] then
|
if validUnits[value] then
|
||||||
if ErrorHandler then
|
if ErrorHandler then
|
||||||
ErrorHandler.error("Units", string.format("Missing numeric value before unit '%s'. Use format like '50%s' (e.g., '50px', '10%%', '2vw')", value, value))
|
ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
|
input = value,
|
||||||
|
expected = "number + unit (e.g., '50" .. value .. "')"
|
||||||
|
}, string.format("Add a numeric value before '%s', like '50%s'. Using fallback: 0px", value, value))
|
||||||
|
else
|
||||||
|
print(string.format("[FlexLove - Units] Warning: Missing numeric value before unit '%s'. Use format like '50%s'. Using fallback: 0px", value, value))
|
||||||
end
|
end
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
end
|
end
|
||||||
@@ -41,7 +52,12 @@ function Units.parse(value)
|
|||||||
-- Check for invalid format (space between number and unit)
|
-- Check for invalid format (space between number and unit)
|
||||||
if value:match("%d%s+%a") then
|
if value:match("%d%s+%a") then
|
||||||
if ErrorHandler then
|
if ErrorHandler then
|
||||||
ErrorHandler.error("Units", string.format("Invalid unit string '%s' (contains space). Use format like '50px' or '50%%'", value))
|
ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
|
input = value,
|
||||||
|
issue = "contains space between number and unit"
|
||||||
|
}, "Remove spaces: use '50px' not '50 px'. Using fallback: 0px")
|
||||||
|
else
|
||||||
|
print(string.format("[FlexLove - Units] Warning: Invalid unit string '%s' (contains space). Use format like '50px' or '50%%'. Using fallback: 0px", value))
|
||||||
end
|
end
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
end
|
end
|
||||||
@@ -50,7 +66,11 @@ function Units.parse(value)
|
|||||||
local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$")
|
local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$")
|
||||||
if not numStr then
|
if not numStr then
|
||||||
if ErrorHandler then
|
if ErrorHandler then
|
||||||
ErrorHandler.error("Units", string.format("Invalid unit format '%s'. Expected format: number + unit (e.g., '50px', '10%%', '2vw')", value))
|
ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
|
input = value
|
||||||
|
}, "Expected format: number + unit (e.g., '50px', '10%', '2vw'). Using fallback: 0px")
|
||||||
|
else
|
||||||
|
print(string.format("[FlexLove - Units] Warning: Invalid unit format '%s'. Expected format: number + unit (e.g., '50px', '10%%', '2vw'). Using fallback: 0px", value))
|
||||||
end
|
end
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
end
|
end
|
||||||
@@ -58,7 +78,12 @@ function Units.parse(value)
|
|||||||
local num = tonumber(numStr)
|
local num = tonumber(numStr)
|
||||||
if not num then
|
if not num then
|
||||||
if ErrorHandler then
|
if ErrorHandler then
|
||||||
ErrorHandler.error("Units", string.format("Invalid numeric value in '%s'", value))
|
ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
|
input = value,
|
||||||
|
issue = "numeric value cannot be parsed"
|
||||||
|
}, "Using fallback: 0px")
|
||||||
|
else
|
||||||
|
print(string.format("[FlexLove - Units] Warning: Invalid numeric value in '%s'. Using fallback: 0px", value))
|
||||||
end
|
end
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
end
|
end
|
||||||
@@ -71,7 +96,13 @@ function Units.parse(value)
|
|||||||
-- validUnits is already defined at the top of the function
|
-- validUnits is already defined at the top of the function
|
||||||
if not validUnits[unit] then
|
if not validUnits[unit] then
|
||||||
if ErrorHandler then
|
if ErrorHandler then
|
||||||
ErrorHandler.error("Units", string.format("Unknown unit '%s' in '%s'. Valid units: px, %%, vw, vh, ew, eh", unit, value))
|
ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
|
input = value,
|
||||||
|
unit = unit,
|
||||||
|
validUnits = "px, %, vw, vh, ew, eh"
|
||||||
|
}, string.format("Treating '%s' as pixels", value))
|
||||||
|
else
|
||||||
|
print(string.format("[FlexLove - Units] Warning: Unknown unit '%s' in '%s'. Valid units: px, %%, vw, vh, ew, eh. Treating as pixels", unit, value))
|
||||||
end
|
end
|
||||||
return num, "px"
|
return num, "px"
|
||||||
end
|
end
|
||||||
@@ -93,7 +124,10 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
|||||||
elseif unit == "%" then
|
elseif unit == "%" then
|
||||||
if not parentSize then
|
if not parentSize then
|
||||||
if ErrorHandler then
|
if ErrorHandler then
|
||||||
ErrorHandler.error("Units", "Percentage units require parent dimension. Element has no parent or parent dimension not available")
|
ErrorHandler.error("Units", "LAY_003", "Invalid dimensions", {
|
||||||
|
unit = "%",
|
||||||
|
issue = "parent dimension not available"
|
||||||
|
}, "Percentage units require a parent element with explicit dimensions")
|
||||||
else
|
else
|
||||||
error("Percentage units require parent dimension")
|
error("Percentage units require parent dimension")
|
||||||
end
|
end
|
||||||
@@ -105,7 +139,10 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
|||||||
return (value / 100) * viewportHeight
|
return (value / 100) * viewportHeight
|
||||||
else
|
else
|
||||||
if ErrorHandler then
|
if ErrorHandler then
|
||||||
ErrorHandler.error("Units", string.format("Unknown unit '%s'. Valid units: px, %%, vw, vh, ew, eh", unit))
|
ErrorHandler.error("Units", "VAL_005", "Invalid unit format", {
|
||||||
|
unit = unit,
|
||||||
|
validUnits = "px, %, vw, vh, ew, eh"
|
||||||
|
})
|
||||||
else
|
else
|
||||||
error(string.format("Unknown unit type: '%s'", unit))
|
error(string.format("Unknown unit type: '%s'", unit))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -36,11 +36,18 @@ if ! git diff-index --quiet HEAD --; then
|
|||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Get current version from latest git tag
|
||||||
|
CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//')
|
||||||
|
if [ -z "$CURRENT_VERSION" ]; then
|
||||||
|
echo -e "${YELLOW}Warning: No existing git tags found${NC}"
|
||||||
|
echo -e "${YELLOW}Attempting to read version from FlexLove.lua...${NC}"
|
||||||
CURRENT_VERSION=$(grep -m 1 "_VERSION" FlexLove.lua | sed -E 's/.*"([^"]+)".*/\1/')
|
CURRENT_VERSION=$(grep -m 1 "_VERSION" FlexLove.lua | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
if [ -z "$CURRENT_VERSION" ]; then
|
if [ -z "$CURRENT_VERSION" ]; then
|
||||||
echo -e "${RED}Error: Could not extract version from FlexLove.lua${NC}"
|
echo -e "${RED}Error: Could not extract version from git tags or FlexLove.lua${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
echo -e "${YELLOW}Using version from FlexLove.lua as fallback${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "${CYAN}Current version:${NC} ${GREEN}v${CURRENT_VERSION}${NC}"
|
echo -e "${CYAN}Current version:${NC} ${GREEN}v${CURRENT_VERSION}${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -158,12 +165,91 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Commit changes
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Ready to commit and create tag${NC}"
|
||||||
|
echo -e "${CYAN}Default commit message:${NC} v${NEW_VERSION} release"
|
||||||
|
echo ""
|
||||||
|
read -p "Add a custom commit message? (y/n) " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
read -p "Enter commit message: " CUSTOM_COMMIT_MSG
|
||||||
|
COMMIT_MSG="$CUSTOM_COMMIT_MSG"
|
||||||
|
else
|
||||||
|
COMMIT_MSG="v${NEW_VERSION} release"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}Commit message:${NC} ${COMMIT_MSG}"
|
||||||
|
echo -e "${CYAN}Tag:${NC} v${NEW_VERSION}"
|
||||||
|
echo ""
|
||||||
|
read -p "Commit changes and create tag? (y/n) " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${YELLOW}Changes staged but not committed${NC}"
|
||||||
|
echo "You can:"
|
||||||
|
echo " - Review changes: git diff --cached"
|
||||||
|
echo " - Commit manually: git commit -m 'v${NEW_VERSION} release'"
|
||||||
|
echo " - Unstage: git restore --staged FlexLove.lua README.md"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Commit changes
|
# Commit changes
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}[4/4]${NC} Committing and tagging..."
|
echo -e "${CYAN}[4/4]${NC} Committing and tagging..."
|
||||||
git commit -m "v${NEW_VERSION} release"
|
git commit -m "$COMMIT_MSG"
|
||||||
git tag -a "v${NEW_VERSION}" -m "Release version ${NEW_VERSION}"
|
git tag -a "v${NEW_VERSION}" -m "Release version ${NEW_VERSION}"
|
||||||
|
|
||||||
|
echo -e "${CYAN}Pushing commits...${NC}"
|
||||||
|
if ! git push 2>&1; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}═══════════════════════════════════════${NC}"
|
||||||
|
echo -e "${RED}✗ Push failed!${NC}"
|
||||||
|
echo -e "${RED}═══════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Common reasons:${NC}"
|
||||||
|
echo " • No network connection"
|
||||||
|
echo " • Authentication failed (check credentials/SSH keys)"
|
||||||
|
echo " • Branch protection rules preventing direct push"
|
||||||
|
echo " • Remote branch diverged (pull needed first)"
|
||||||
|
echo " • No push permissions for this repository"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}The commit and tag were created locally but not pushed.${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}To retry pushing:${NC}"
|
||||||
|
echo " git push"
|
||||||
|
echo " git push origin tag \"v${NEW_VERSION}\""
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}To undo the tag:${NC}"
|
||||||
|
echo " git tag -d \"v${NEW_VERSION}\""
|
||||||
|
echo " git reset --soft HEAD~1"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${CYAN}Pushing tags...${NC}"
|
||||||
|
if ! git push origin tag "v${NEW_VERSION}" 2>&1; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}═══════════════════════════════════════${NC}"
|
||||||
|
echo -e "${RED}✗ Tag push failed!${NC}"
|
||||||
|
echo -e "${RED}═══════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Common reasons:${NC}"
|
||||||
|
echo " • No network connection"
|
||||||
|
echo " • Authentication failed (check credentials/SSH keys)"
|
||||||
|
echo " • Tag already exists on remote"
|
||||||
|
echo " • No push permissions for this repository"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}The commit was pushed successfully, but the tag was not.${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}To retry pushing the tag:${NC}"
|
||||||
|
echo " git push origin tag \"v${NEW_VERSION}\""
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}To delete the local tag:${NC}"
|
||||||
|
echo " git tag -d \"v${NEW_VERSION}\""
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
||||||
echo -e "${GREEN}✓ Version bump complete!${NC}"
|
echo -e "${GREEN}✓ Version bump complete!${NC}"
|
||||||
|
|||||||
789
testing/__tests__/critical_failures_test.lua
Normal file
789
testing/__tests__/critical_failures_test.lua
Normal file
@@ -0,0 +1,789 @@
|
|||||||
|
-- Critical Failure Tests for FlexLove
|
||||||
|
-- These tests are designed to find ACTUAL BUGS:
|
||||||
|
-- 1. Memory leaks / garbage creation without cleanup
|
||||||
|
-- 2. Layout calculation bugs causing incorrect positioning
|
||||||
|
-- 3. Unsafe input access (nil dereference, division by zero, etc.)
|
||||||
|
|
||||||
|
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||||
|
require("testing.loveStub")
|
||||||
|
local luaunit = require("testing.luaunit")
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
|
TestCriticalFailures = {}
|
||||||
|
|
||||||
|
function TestCriticalFailures:setUp()
|
||||||
|
collectgarbage("collect")
|
||||||
|
FlexLove.destroy()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestCriticalFailures:tearDown()
|
||||||
|
FlexLove.destroy()
|
||||||
|
collectgarbage("collect")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- MEMORY LEAK TESTS - Find garbage that's not cleaned up
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Test: Canvas objects should be cleaned up on resize
|
||||||
|
function TestCriticalFailures:test_canvas_cleanup_on_resize()
|
||||||
|
FlexLove.init()
|
||||||
|
|
||||||
|
-- Create initial canvases
|
||||||
|
FlexLove.draw(function() end)
|
||||||
|
local canvas1 = FlexLove._gameCanvas
|
||||||
|
|
||||||
|
-- Resize should invalidate old canvases
|
||||||
|
FlexLove.resize()
|
||||||
|
|
||||||
|
-- Draw again to create new canvases
|
||||||
|
FlexLove.draw(function() end)
|
||||||
|
local canvas2 = FlexLove._gameCanvas
|
||||||
|
|
||||||
|
-- Old canvas should be replaced
|
||||||
|
luaunit.assertNotEquals(canvas1, canvas2)
|
||||||
|
|
||||||
|
-- Check canvas is actually nil after resize (before draw)
|
||||||
|
FlexLove.resize()
|
||||||
|
luaunit.assertNil(FlexLove._gameCanvas)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Elements should be cleaned up from topElements on destroy
|
||||||
|
function TestCriticalFailures:test_element_cleanup_from_top_elements()
|
||||||
|
local element1 = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
local element2 = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
|
||||||
|
luaunit.assertEquals(#FlexLove.topElements, 2)
|
||||||
|
|
||||||
|
element1:destroy()
|
||||||
|
luaunit.assertEquals(#FlexLove.topElements, 1)
|
||||||
|
|
||||||
|
element2:destroy()
|
||||||
|
luaunit.assertEquals(#FlexLove.topElements, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Child elements should be destroyed when parent is destroyed
|
||||||
|
function TestCriticalFailures:test_child_cleanup_on_parent_destroy()
|
||||||
|
local parent = FlexLove.new({ width = 200, height = 200 })
|
||||||
|
local child = FlexLove.new({ width = 50, height = 50, parent = parent })
|
||||||
|
|
||||||
|
luaunit.assertEquals(#parent.children, 1)
|
||||||
|
|
||||||
|
-- Destroy parent should also clear children
|
||||||
|
parent:destroy()
|
||||||
|
|
||||||
|
-- Child should have no parent reference (potential memory leak if not cleared)
|
||||||
|
luaunit.assertNil(child.parent)
|
||||||
|
luaunit.assertEquals(#parent.children, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Event handlers should be cleared on destroy (closure leak)
|
||||||
|
function TestCriticalFailures:test_event_handler_cleanup()
|
||||||
|
local captured_data = { large_array = {} }
|
||||||
|
for i = 1, 1000 do
|
||||||
|
captured_data.large_array[i] = i
|
||||||
|
end
|
||||||
|
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
onEvent = function(el, event)
|
||||||
|
-- This closure captures captured_data
|
||||||
|
print(captured_data.large_array[1])
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
element:destroy()
|
||||||
|
|
||||||
|
-- onEvent should be nil after destroy (prevent closure leak)
|
||||||
|
luaunit.assertNil(element.onEvent)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Immediate mode state should not grow unbounded
|
||||||
|
function TestCriticalFailures:test_immediate_mode_state_cleanup()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.init({ stateRetentionFrames = 2 })
|
||||||
|
|
||||||
|
-- Create elements for multiple frames
|
||||||
|
for frame = 1, 10 do
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
FlexLove.new({ id = "element_" .. frame, width = 100, height = 100 })
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- State count should be limited by stateRetentionFrames
|
||||||
|
local stateCount = FlexLove.getStateCount()
|
||||||
|
-- Should be much less than 10 due to cleanup
|
||||||
|
luaunit.assertTrue(stateCount < 10, "State count: " .. stateCount .. " (should be cleaned up)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- LAYOUT CALCULATION BUGS - Find incorrect positioning
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Test: Flex layout with overflow should not position children outside container
|
||||||
|
function TestCriticalFailures:test_flex_overflow_positioning()
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
flexWrap = "nowrap",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Add children that exceed parent width
|
||||||
|
local child1 = FlexLove.new({ width = 80, height = 50, parent = parent })
|
||||||
|
local child2 = FlexLove.new({ width = 80, height = 50, parent = parent })
|
||||||
|
|
||||||
|
-- Children should be positioned, even if they overflow
|
||||||
|
-- Check that x positions are at least valid numbers
|
||||||
|
luaunit.assertNotNil(child1.x)
|
||||||
|
luaunit.assertNotNil(child2.x)
|
||||||
|
luaunit.assertTrue(child1.x >= 0)
|
||||||
|
luaunit.assertTrue(child2.x > child1.x)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Percentage width with zero parent width (division by zero)
|
||||||
|
function TestCriticalFailures:test_percentage_width_zero_parent()
|
||||||
|
local parent = FlexLove.new({ width = 0, height = 100 })
|
||||||
|
|
||||||
|
-- This should not crash (division by zero in percentage calculation)
|
||||||
|
local success, child = pcall(function()
|
||||||
|
return FlexLove.new({ width = "50%", height = 50, parent = parent })
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should not crash with zero parent width")
|
||||||
|
if success then
|
||||||
|
-- Width should be 0 or handled gracefully
|
||||||
|
luaunit.assertTrue(child.width >= 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Auto-sizing with circular dependency
|
||||||
|
function TestCriticalFailures:test_autosizing_circular_dependency()
|
||||||
|
-- Parent auto-sizes to child, child uses percentage of parent
|
||||||
|
local parent = FlexLove.new({ height = 100 }) -- No width = auto
|
||||||
|
|
||||||
|
-- Child width is percentage of parent, but parent width depends on child
|
||||||
|
local success, child = pcall(function()
|
||||||
|
return FlexLove.new({ width = "50%", height = 50, parent = parent })
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should not crash with circular sizing")
|
||||||
|
|
||||||
|
-- Check that we don't get NaN or negative values
|
||||||
|
if success then
|
||||||
|
luaunit.assertFalse(parent.width ~= parent.width, "Parent width should not be NaN")
|
||||||
|
luaunit.assertFalse(child.width ~= child.width, "Child width should not be NaN")
|
||||||
|
luaunit.assertTrue(parent.width >= 0, "Parent width should be non-negative")
|
||||||
|
luaunit.assertTrue(child.width >= 0, "Child width should be non-negative")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Negative padding should not cause negative content dimensions
|
||||||
|
function TestCriticalFailures:test_negative_padding_content_dimensions()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
padding = { top = -50, left = -50, right = -50, bottom = -50 },
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Content width/height should never be negative
|
||||||
|
luaunit.assertTrue(element.width >= 0, "Content width should be non-negative: " .. element.width)
|
||||||
|
luaunit.assertTrue(element.height >= 0, "Content height should be non-negative: " .. element.height)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Grid layout with zero rows/columns (division by zero)
|
||||||
|
function TestCriticalFailures:test_grid_zero_dimensions()
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
width = 300,
|
||||||
|
height = 200,
|
||||||
|
positioning = "grid",
|
||||||
|
gridRows = 0,
|
||||||
|
gridColumns = 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- This should not crash when adding children
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({ width = 50, height = 50, parent = parent })
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should not crash with zero grid dimensions")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- UNSAFE INPUT ACCESS - Find nil dereference and type errors
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Test: setText with number should not crash (type coercion)
|
||||||
|
function TestCriticalFailures:test_set_text_with_number()
|
||||||
|
local element = FlexLove.new({ width = 100, height = 100, text = "initial" })
|
||||||
|
|
||||||
|
-- Many Lua APIs expect string but get number
|
||||||
|
local success = pcall(function()
|
||||||
|
element:setText(12345)
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should handle number text gracefully")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Image path with special characters should not crash file system
|
||||||
|
function TestCriticalFailures:test_image_path_special_characters()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
imagePath = "../../../etc/passwd", -- Path traversal attempt
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should handle malicious paths gracefully")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: onEvent callback that errors should not crash the system
|
||||||
|
function TestCriticalFailures:test_on_event_error_handling()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
onEvent = function(el, event)
|
||||||
|
error("Intentional error in callback")
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Simulate mouse click
|
||||||
|
local success = pcall(function()
|
||||||
|
local InputEvent = require("modules.InputEvent")
|
||||||
|
local event = InputEvent.new({ type = "pressed", button = 1 })
|
||||||
|
if element.onEvent then
|
||||||
|
element.onEvent(element, event)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Should error (no protection), but shouldn't leave system in bad state
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
|
||||||
|
-- Element should still be valid
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Text with null bytes should not cause buffer issues
|
||||||
|
function TestCriticalFailures:test_text_with_null_bytes()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 100,
|
||||||
|
text = "Hello\0World\0\0\0",
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should handle null bytes in text")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Extremely deep nesting should not cause stack overflow
|
||||||
|
function TestCriticalFailures:test_extreme_nesting_stack_overflow()
|
||||||
|
local parent = FlexLove.new({ width = 500, height = 500 })
|
||||||
|
local current = parent
|
||||||
|
|
||||||
|
-- Try to create 1000 levels of nesting
|
||||||
|
local success = pcall(function()
|
||||||
|
for i = 1, 1000 do
|
||||||
|
local child = FlexLove.new({
|
||||||
|
width = 10,
|
||||||
|
height = 10,
|
||||||
|
parent = current,
|
||||||
|
})
|
||||||
|
current = child
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- This might fail due to legitimate recursion limits
|
||||||
|
-- But it should fail gracefully, not segfault
|
||||||
|
if not success then
|
||||||
|
print("Deep nesting failed (expected for extreme depth)")
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(true) -- If we get here, no segfault
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Gap with NaN value
|
||||||
|
function TestCriticalFailures:test_gap_nan_value()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
width = 300,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
gap = 0 / 0, -- NaN
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- NaN in calculations can propagate and cause issues
|
||||||
|
luaunit.assertTrue(success, "Should handle NaN gap")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Border-box model with huge padding
|
||||||
|
function TestCriticalFailures:test_huge_padding_overflow()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
padding = { top = 1000000, left = 1000000, right = 1000000, bottom = 1000000 },
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Content dimensions should be clamped to 0, not underflow
|
||||||
|
luaunit.assertTrue(element.width >= 0, "Width should not underflow: " .. element.width)
|
||||||
|
luaunit.assertTrue(element.height >= 0, "Height should not underflow: " .. element.height)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Scroll position race condition in immediate mode
|
||||||
|
function TestCriticalFailures:test_scroll_position_race_immediate_mode()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
|
||||||
|
-- Create scrollable element
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "scroll_test",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
overflow = "scroll",
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Set scroll position
|
||||||
|
element:setScrollPosition(50, 50)
|
||||||
|
|
||||||
|
-- Create same element next frame (state should restore)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element2 = FlexLove.new({
|
||||||
|
id = "scroll_test",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
overflow = "scroll",
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Scroll position should persist (or at least not crash)
|
||||||
|
local scrollX, scrollY = element2:getScrollPosition()
|
||||||
|
luaunit.assertNotNil(scrollX)
|
||||||
|
luaunit.assertNotNil(scrollY)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Theme with missing required properties
|
||||||
|
function TestCriticalFailures:test_theme_missing_properties()
|
||||||
|
local Theme = require("modules.Theme")
|
||||||
|
|
||||||
|
-- Create theme with minimal properties
|
||||||
|
local success = pcall(function()
|
||||||
|
local theme = Theme.new({
|
||||||
|
name = "broken",
|
||||||
|
-- Missing components table
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.init({ theme = theme })
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Should handle gracefully or error clearly
|
||||||
|
luaunit.assertTrue(true) -- If we get here, no segfault
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Blur with zero or negative quality
|
||||||
|
function TestCriticalFailures:test_blur_invalid_quality()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
contentBlur = { intensity = 50, quality = 0 },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should handle zero blur quality")
|
||||||
|
|
||||||
|
success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
contentBlur = { intensity = 50, quality = -5 },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should handle negative blur quality")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- NIL DEREFERENCE BUGS - Target specific LSP warnings
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Test: 9-patch padding with corrupted theme state (Element.lua:752-755)
|
||||||
|
function TestCriticalFailures:test_ninepatch_padding_nil_dereference()
|
||||||
|
local Theme = require("modules.Theme")
|
||||||
|
|
||||||
|
-- Create a theme with 9-patch data
|
||||||
|
local theme = Theme.new({
|
||||||
|
name = "test_theme",
|
||||||
|
components = {
|
||||||
|
container = {
|
||||||
|
ninePatch = {
|
||||||
|
imagePath = "themes/metal.lua", -- Invalid path to trigger edge case
|
||||||
|
contentPadding = { top = 10, left = 10, right = 10, bottom = 10 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.init({ theme = theme })
|
||||||
|
|
||||||
|
-- Try to create element that uses 9-patch padding
|
||||||
|
-- If ninePatchContentPadding becomes nil but use9PatchPadding is true, this will crash
|
||||||
|
local success, err = pcall(function()
|
||||||
|
return FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
component = "container",
|
||||||
|
-- No explicit padding, should use 9-patch padding
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
print("ERROR: " .. tostring(err))
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should handle 9-patch padding gracefully")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Theme with malformed 9-patch data
|
||||||
|
function TestCriticalFailures:test_malformed_ninepatch_data()
|
||||||
|
local Theme = require("modules.Theme")
|
||||||
|
|
||||||
|
-- Create theme with incomplete 9-patch data
|
||||||
|
local success = pcall(function()
|
||||||
|
local theme = Theme.new({
|
||||||
|
name = "broken_nine_patch",
|
||||||
|
components = {
|
||||||
|
container = {
|
||||||
|
ninePatch = {
|
||||||
|
-- Missing imagePath
|
||||||
|
contentPadding = { top = 10, left = 10 }, -- Incomplete padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.init({ theme = theme })
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
component = "container",
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Should either succeed or fail with clear error (not nil dereference)
|
||||||
|
luaunit.assertTrue(true) -- If we get here, no segfault
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- INTEGRATION TESTS - Combine features in unexpected ways
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Test: Scrollable element with overflow content + immediate mode + state restoration
|
||||||
|
function TestCriticalFailures:test_scroll_overflow_immediate_mode_integration()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
|
||||||
|
for frame = 1, 3 do
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
local scrollContainer = FlexLove.new({
|
||||||
|
id = "scroll_container",
|
||||||
|
width = 200,
|
||||||
|
height = 150,
|
||||||
|
overflow = "scroll",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Add children that exceed container height
|
||||||
|
for i = 1, 10 do
|
||||||
|
FlexLove.new({
|
||||||
|
id = "child_" .. i,
|
||||||
|
width = 180,
|
||||||
|
height = 50,
|
||||||
|
parent = scrollContainer,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Scroll on second frame
|
||||||
|
if frame == 2 then
|
||||||
|
scrollContainer:setScrollPosition(0, 100)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check scroll position restored on third frame
|
||||||
|
if frame == 3 then
|
||||||
|
local scrollX, scrollY = scrollContainer:getScrollPosition()
|
||||||
|
luaunit.assertNotNil(scrollX, "Scroll X should be preserved")
|
||||||
|
luaunit.assertNotNil(scrollY, "Scroll Y should be preserved")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Grid layout with auto-sized children and percentage gaps
|
||||||
|
function TestCriticalFailures:test_grid_autosized_children_percentage_gap()
|
||||||
|
local grid = FlexLove.new({
|
||||||
|
width = 300,
|
||||||
|
height = 300,
|
||||||
|
positioning = "grid",
|
||||||
|
gridRows = 3,
|
||||||
|
gridColumns = 3,
|
||||||
|
gap = "5%", -- Percentage gap
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Add auto-sized children (no explicit dimensions)
|
||||||
|
for i = 1, 9 do
|
||||||
|
local child = FlexLove.new({
|
||||||
|
parent = grid,
|
||||||
|
text = "Cell " .. i,
|
||||||
|
-- Auto-sizing based on text
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Verify child dimensions are valid
|
||||||
|
luaunit.assertNotNil(child.width)
|
||||||
|
luaunit.assertNotNil(child.height)
|
||||||
|
luaunit.assertTrue(child.width >= 0)
|
||||||
|
luaunit.assertTrue(child.height >= 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Nested flex containers with conflicting alignment
|
||||||
|
function TestCriticalFailures:test_nested_flex_conflicting_alignment()
|
||||||
|
local outer = FlexLove.new({
|
||||||
|
width = 400,
|
||||||
|
height = 400,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
alignItems = "stretch",
|
||||||
|
justifyContent = "center",
|
||||||
|
})
|
||||||
|
|
||||||
|
local middle = FlexLove.new({
|
||||||
|
parent = outer,
|
||||||
|
height = 200,
|
||||||
|
-- Auto width (should stretch)
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
alignItems = "flex-end",
|
||||||
|
justifyContent = "space-between",
|
||||||
|
})
|
||||||
|
|
||||||
|
local inner1 = FlexLove.new({
|
||||||
|
parent = middle,
|
||||||
|
width = 50,
|
||||||
|
-- Auto height
|
||||||
|
text = "A",
|
||||||
|
})
|
||||||
|
|
||||||
|
local inner2 = FlexLove.new({
|
||||||
|
parent = middle,
|
||||||
|
width = 50,
|
||||||
|
height = 100,
|
||||||
|
text = "B",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Verify all elements have valid dimensions and positions
|
||||||
|
luaunit.assertTrue(outer.width > 0)
|
||||||
|
luaunit.assertTrue(middle.width > 0)
|
||||||
|
luaunit.assertTrue(inner1.height > 0)
|
||||||
|
luaunit.assertNotNil(inner1.x)
|
||||||
|
luaunit.assertNotNil(inner2.x)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with multiple conflicting size sources
|
||||||
|
function TestCriticalFailures:test_conflicting_size_sources()
|
||||||
|
-- Element with explicit size + auto-sizing content + parent constraints
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
alignItems = "stretch",
|
||||||
|
})
|
||||||
|
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
parent = parent,
|
||||||
|
width = 300, -- Exceeds parent width
|
||||||
|
height = "50%", -- Percentage height
|
||||||
|
text = "Very long text that should cause auto-sizing",
|
||||||
|
padding = { top = 50, left = 50, right = 50, bottom = 50 },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should handle conflicting size sources")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Image element with resize during load
|
||||||
|
function TestCriticalFailures:test_image_resize_during_load()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
imagePath = "nonexistent.png", -- Won't load, but should handle gracefully
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Simulate resize while "loading"
|
||||||
|
FlexLove.resize(1920, 1080)
|
||||||
|
|
||||||
|
-- Element should still be valid
|
||||||
|
luaunit.assertNotNil(element.width)
|
||||||
|
luaunit.assertNotNil(element.height)
|
||||||
|
luaunit.assertTrue(element.width > 0)
|
||||||
|
luaunit.assertTrue(element.height > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Rapid theme switching with active elements
|
||||||
|
function TestCriticalFailures:test_rapid_theme_switching()
|
||||||
|
local Theme = require("modules.Theme")
|
||||||
|
|
||||||
|
local theme1 = Theme.new({ name = "theme1", components = {} })
|
||||||
|
local theme2 = Theme.new({ name = "theme2", components = {} })
|
||||||
|
|
||||||
|
FlexLove.init({ theme = theme1 })
|
||||||
|
|
||||||
|
-- Create elements with theme1
|
||||||
|
local element1 = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
local element2 = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
|
||||||
|
-- Switch theme
|
||||||
|
FlexLove.destroy()
|
||||||
|
FlexLove.init({ theme = theme2 })
|
||||||
|
|
||||||
|
-- Old elements should be invalidated (accessing them might crash)
|
||||||
|
local success = pcall(function()
|
||||||
|
element1:setText("test")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- It's OK if this fails (element destroyed), but shouldn't segfault
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Update properties during layout calculation
|
||||||
|
function TestCriticalFailures:test_update_during_layout()
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
width = 300,
|
||||||
|
height = 300,
|
||||||
|
positioning = "flex",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
parent = parent,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Modify child properties immediately after creation (during layout)
|
||||||
|
child:setText("Modified during layout")
|
||||||
|
child.backgroundColor = { r = 1, g = 0, b = 0, a = 1 }
|
||||||
|
|
||||||
|
-- Trigger another layout
|
||||||
|
parent:resize(400, 400)
|
||||||
|
|
||||||
|
-- Everything should still be valid
|
||||||
|
luaunit.assertNotNil(child.text)
|
||||||
|
luaunit.assertEquals(child.text, "Modified during layout")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- STATE CORRUPTION SCENARIOS
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Test: Destroy element with active event listeners
|
||||||
|
function TestCriticalFailures:test_destroy_with_active_listeners()
|
||||||
|
local eventFired = false
|
||||||
|
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
onEvent = function(el, event)
|
||||||
|
eventFired = true
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Simulate an event via InputEvent
|
||||||
|
local InputEvent = require("modules.InputEvent")
|
||||||
|
local event = InputEvent.new({ type = "pressed", button = 1, x = 50, y = 50 })
|
||||||
|
|
||||||
|
if element.onEvent and element:contains(50, 50) then
|
||||||
|
element.onEvent(element, event)
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(eventFired, "Event should fire before destroy")
|
||||||
|
|
||||||
|
-- Destroy element
|
||||||
|
element:destroy()
|
||||||
|
|
||||||
|
-- onEvent should be nil after destroy
|
||||||
|
luaunit.assertNil(element.onEvent, "onEvent should be cleared after destroy")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Double destroy should be safe
|
||||||
|
function TestCriticalFailures:test_double_destroy_safety()
|
||||||
|
local element = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
|
||||||
|
element:destroy()
|
||||||
|
|
||||||
|
-- Second destroy should be safe (idempotent)
|
||||||
|
local success = pcall(function()
|
||||||
|
element:destroy()
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Double destroy should be safe")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Circular parent-child reference (should never happen, but test safety)
|
||||||
|
function TestCriticalFailures:test_circular_parent_child_reference()
|
||||||
|
local parent = FlexLove.new({ width = 200, height = 200 })
|
||||||
|
local child = FlexLove.new({ width = 100, height = 100, parent = parent })
|
||||||
|
|
||||||
|
-- Try to create circular reference (should be prevented)
|
||||||
|
local success = pcall(function()
|
||||||
|
parent.parent = child -- This should never be allowed
|
||||||
|
parent:layoutChildren() -- This would cause infinite recursion
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Even if we set circular reference, layout should not crash
|
||||||
|
luaunit.assertTrue(true) -- If we get here, no stack overflow
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Modify children array during iteration
|
||||||
|
function TestCriticalFailures:test_modify_children_during_iteration()
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
width = 300,
|
||||||
|
height = 300,
|
||||||
|
positioning = "flex",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Add several children
|
||||||
|
local children = {}
|
||||||
|
for i = 1, 5 do
|
||||||
|
children[i] = FlexLove.new({
|
||||||
|
width = 50,
|
||||||
|
height = 50,
|
||||||
|
parent = parent,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove child during layout (simulates user code modifying structure)
|
||||||
|
local success = pcall(function()
|
||||||
|
-- Trigger layout
|
||||||
|
parent:layoutChildren()
|
||||||
|
|
||||||
|
-- Remove a child (modifies children array)
|
||||||
|
children[3]:destroy()
|
||||||
|
|
||||||
|
-- Trigger layout again
|
||||||
|
parent:layoutChildren()
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(success, "Should handle children modification during layout")
|
||||||
|
end
|
||||||
|
|
||||||
|
if not _G.RUNNING_ALL_TESTS then
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
|
end
|
||||||
@@ -1223,6 +1223,771 @@ function TestElementAdditional:test_element_with_userdata()
|
|||||||
luaunit.assertEquals(element.userdata.count, 42)
|
luaunit.assertEquals(element.userdata.count, 42)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- UNHAPPY PATH TESTS
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
TestElementUnhappyPaths = {}
|
||||||
|
|
||||||
|
function TestElementUnhappyPaths:setUp()
|
||||||
|
FlexLove.beginFrame(1920, 1080)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementUnhappyPaths:tearDown()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with missing deps parameter
|
||||||
|
function TestElementUnhappyPaths:test_element_without_deps()
|
||||||
|
local Element = require("modules.Element")
|
||||||
|
local success = pcall(function()
|
||||||
|
Element.new({}, nil)
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success) -- Should error without deps
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with negative dimensions
|
||||||
|
function TestElementUnhappyPaths:test_element_negative_dimensions()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "negative",
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
width = -100,
|
||||||
|
height = -50,
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
-- Element should still be created (negative values handled)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with zero dimensions
|
||||||
|
function TestElementUnhappyPaths:test_element_zero_dimensions()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "zero",
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
width = 0,
|
||||||
|
height = 0,
|
||||||
|
})
|
||||||
|
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
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
id = "high_opacity",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
opacity = 2.5,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success) -- Should error (validateRange)
|
||||||
|
|
||||||
|
-- Negative opacity
|
||||||
|
success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
id = "negative_opacity",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
opacity = -0.5,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success) -- Should error (validateRange)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid imageOpacity values
|
||||||
|
function TestElementUnhappyPaths:test_element_invalid_image_opacity()
|
||||||
|
-- imageOpacity > 1
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
id = "high_img_opacity",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
imageOpacity = 3.0,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
|
||||||
|
-- Negative imageOpacity
|
||||||
|
success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
id = "negative_img_opacity",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
imageOpacity = -1.0,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid textSize
|
||||||
|
function TestElementUnhappyPaths:test_element_invalid_text_size()
|
||||||
|
-- Zero textSize
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
id = "zero_text",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
textSize = 0,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
|
||||||
|
-- Negative textSize
|
||||||
|
success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
id = "negative_text",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
textSize = -12,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid textAlign enum
|
||||||
|
function TestElementUnhappyPaths:test_element_invalid_text_align()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
id = "invalid_align",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
textAlign = "invalid_value",
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success) -- Should error (validateEnum)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid positioning enum
|
||||||
|
function TestElementUnhappyPaths:test_element_invalid_positioning()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
id = "invalid_pos",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
positioning = "invalid_positioning",
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success) -- Should error (validateEnum)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid flexDirection enum
|
||||||
|
function TestElementUnhappyPaths:test_element_invalid_flex_direction()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
id = "invalid_flex",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "diagonal",
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success) -- Should error (validateEnum)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid objectFit enum
|
||||||
|
function TestElementUnhappyPaths:test_element_invalid_object_fit()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.new({
|
||||||
|
id = "invalid_fit",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
objectFit = "stretch",
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success) -- Should error (validateEnum)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with nonexistent image path
|
||||||
|
function TestElementUnhappyPaths:test_element_nonexistent_image()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "no_image",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
imagePath = "/nonexistent/path/to/image.png",
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
luaunit.assertNil(element._loadedImage) -- Image should fail to load silently
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with passwordMode and multiline (conflicting)
|
||||||
|
function TestElementUnhappyPaths:test_element_password_multiline_conflict()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "conflict",
|
||||||
|
width = 200,
|
||||||
|
height = 100,
|
||||||
|
editable = true,
|
||||||
|
passwordMode = true,
|
||||||
|
multiline = true, -- Should be disabled by passwordMode
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
luaunit.assertFalse(element.multiline) -- multiline should be forced to false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element addChild with nil child
|
||||||
|
function TestElementUnhappyPaths:test_add_nil_child()
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
id = "parent",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
local success = pcall(function()
|
||||||
|
parent:addChild(nil)
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success) -- Should error
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element removeChild that doesn't exist
|
||||||
|
function TestElementUnhappyPaths:test_remove_nonexistent_child()
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
id = "parent",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
local notAChild = FlexLove.new({
|
||||||
|
id = "orphan",
|
||||||
|
width = 50,
|
||||||
|
height = 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
parent:removeChild(notAChild) -- Should not crash
|
||||||
|
luaunit.assertEquals(#parent.children, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element removeChild with nil
|
||||||
|
function TestElementUnhappyPaths:test_remove_nil_child()
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
id = "parent",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
parent:removeChild(nil) -- Should not crash
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element clearChildren on empty parent
|
||||||
|
function TestElementUnhappyPaths:test_clear_children_empty()
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
id = "parent",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
parent:clearChildren() -- Should not crash
|
||||||
|
luaunit.assertEquals(#parent.children, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element clearChildren called twice
|
||||||
|
function TestElementUnhappyPaths:test_clear_children_twice()
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
id = "parent",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child = FlexLove.new({
|
||||||
|
id = "child",
|
||||||
|
width = 50,
|
||||||
|
height = 50,
|
||||||
|
parent = parent,
|
||||||
|
})
|
||||||
|
|
||||||
|
parent:clearChildren()
|
||||||
|
parent:clearChildren() -- Call again
|
||||||
|
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()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
x = 10,
|
||||||
|
y = 20,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
local nan = 0 / 0
|
||||||
|
local result = element:contains(nan, nan)
|
||||||
|
-- NaN comparisons return false, so this should be false
|
||||||
|
luaunit.assertFalse(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element setScrollPosition without ScrollManager
|
||||||
|
function TestElementUnhappyPaths:test_scroll_without_manager()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "no_scroll",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
-- No overflow property, so no ScrollManager
|
||||||
|
})
|
||||||
|
|
||||||
|
element:setScrollPosition(50, 50) -- Should not crash
|
||||||
|
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()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "scrollable",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
overflow = "scroll",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:scrollBy(nil, nil) -- Should use current position
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element destroy on already destroyed element
|
||||||
|
function TestElementUnhappyPaths:test_destroy_twice()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "destroyable",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
element:destroy()
|
||||||
|
element:destroy() -- Call again - should not crash
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element destroy with circular reference (parent-child)
|
||||||
|
function TestElementUnhappyPaths:test_destroy_with_children()
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
id = "parent",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child = FlexLove.new({
|
||||||
|
id = "child",
|
||||||
|
width = 50,
|
||||||
|
height = 50,
|
||||||
|
parent = parent,
|
||||||
|
})
|
||||||
|
|
||||||
|
parent:destroy() -- Should destroy all children too
|
||||||
|
luaunit.assertEquals(#parent.children, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element update with nil dt
|
||||||
|
function TestElementUnhappyPaths:test_update_nil_dt()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
local success = pcall(function()
|
||||||
|
element:update(nil)
|
||||||
|
end)
|
||||||
|
-- May or may not error depending on implementation
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element update with negative dt
|
||||||
|
function TestElementUnhappyPaths:test_update_negative_dt()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
element:update(-0.016) -- Should not crash
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element draw with nil backdropCanvas
|
||||||
|
function TestElementUnhappyPaths:test_draw_nil_backdrop()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
element:draw(nil) -- Should not crash
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid cornerRadius types
|
||||||
|
function TestElementUnhappyPaths:test_invalid_corner_radius()
|
||||||
|
-- String cornerRadius
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
cornerRadius = "invalid",
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
|
||||||
|
-- Negative cornerRadius
|
||||||
|
element = FlexLove.new({
|
||||||
|
id = "test2",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
cornerRadius = -10,
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with partial cornerRadius table
|
||||||
|
function TestElementUnhappyPaths:test_partial_corner_radius()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
cornerRadius = {
|
||||||
|
topLeft = 10,
|
||||||
|
-- Missing other corners
|
||||||
|
},
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
luaunit.assertEquals(element.cornerRadius.topLeft, 10)
|
||||||
|
luaunit.assertEquals(element.cornerRadius.topRight, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid border types
|
||||||
|
function TestElementUnhappyPaths:test_invalid_border()
|
||||||
|
-- String border
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
border = "invalid",
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
|
||||||
|
-- Negative border
|
||||||
|
element = FlexLove.new({
|
||||||
|
id = "test2",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
border = -5,
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with partial border table
|
||||||
|
function TestElementUnhappyPaths:test_partial_border()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
border = {
|
||||||
|
top = 2,
|
||||||
|
left = 3,
|
||||||
|
-- Missing right and bottom
|
||||||
|
},
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
luaunit.assertEquals(element.border.top, 2)
|
||||||
|
luaunit.assertEquals(element.border.left, 3)
|
||||||
|
luaunit.assertFalse(element.border.right)
|
||||||
|
luaunit.assertFalse(element.border.bottom)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid padding types
|
||||||
|
function TestElementUnhappyPaths:test_invalid_padding()
|
||||||
|
-- String padding
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
padding = "invalid",
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
|
||||||
|
-- Negative padding
|
||||||
|
element = FlexLove.new({
|
||||||
|
id = "test2",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
padding = { top = -10, left = -10, right = -10, bottom = -10 },
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid margin types
|
||||||
|
function TestElementUnhappyPaths:test_invalid_margin()
|
||||||
|
-- String margin
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
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
|
||||||
|
function TestElementUnhappyPaths:test_invalid_gap()
|
||||||
|
-- Negative gap
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 300,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
|
||||||
|
-- Negative rows/columns
|
||||||
|
element = FlexLove.new({
|
||||||
|
id = "test2",
|
||||||
|
width = 300,
|
||||||
|
height = 200,
|
||||||
|
positioning = "grid",
|
||||||
|
gridRows = -5,
|
||||||
|
gridColumns = -5,
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element setText on non-text element
|
||||||
|
function TestElementUnhappyPaths:test_set_text_on_non_text()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "no_text",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
element:setText("New text") -- Should not crash
|
||||||
|
luaunit.assertEquals(element.text, "New text")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element setText with nil
|
||||||
|
function TestElementUnhappyPaths:test_set_text_nil()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "text",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
text = "Initial",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:setText(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()
|
||||||
|
-- Width less than padding
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "conflict",
|
||||||
|
width = 10,
|
||||||
|
height = 10,
|
||||||
|
padding = { top = 20, left = 20, right = 20, bottom = 20 },
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
-- Content width should be clamped to 0 or handled gracefully
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element textinput on non-editable element
|
||||||
|
function TestElementUnhappyPaths:test_textinput_non_editable()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "not_editable",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
editable = false,
|
||||||
|
})
|
||||||
|
|
||||||
|
local success = pcall(function()
|
||||||
|
element:textinput("a")
|
||||||
|
end)
|
||||||
|
-- Should either do nothing or handle gracefully
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element keypressed on non-editable element
|
||||||
|
function TestElementUnhappyPaths:test_keypressed_non_editable()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "not_editable",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
editable = false,
|
||||||
|
})
|
||||||
|
|
||||||
|
local success = pcall(function()
|
||||||
|
element:keypressed("return", "return", false)
|
||||||
|
end)
|
||||||
|
-- Should either do nothing or handle gracefully
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with invalid blur configuration
|
||||||
|
function TestElementUnhappyPaths:test_invalid_blur_config()
|
||||||
|
-- Negative intensity
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "blur",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
contentBlur = { intensity = -10, quality = 5 },
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
|
||||||
|
-- Intensity > 100
|
||||||
|
element = FlexLove.new({
|
||||||
|
id = "blur2",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
backdropBlur = { intensity = 150, quality = 5 },
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
|
||||||
|
-- Invalid quality
|
||||||
|
element = FlexLove.new({
|
||||||
|
id = "blur3",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
contentBlur = { intensity = 50, quality = 0 },
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element getAvailableContentWidth/Height on element with no padding
|
||||||
|
function TestElementUnhappyPaths:test_available_content_no_padding()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "test",
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
local availWidth = element:getAvailableContentWidth()
|
||||||
|
local availHeight = element:getAvailableContentHeight()
|
||||||
|
|
||||||
|
luaunit.assertEquals(availWidth, 100)
|
||||||
|
luaunit.assertEquals(availHeight, 100)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with maxLines but no multiline
|
||||||
|
function TestElementUnhappyPaths:test_max_lines_without_multiline()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "text",
|
||||||
|
width = 200,
|
||||||
|
height = 100,
|
||||||
|
editable = true,
|
||||||
|
multiline = false,
|
||||||
|
maxLines = 5, -- Should be ignored for single-line
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with maxLength 0
|
||||||
|
function TestElementUnhappyPaths:test_max_length_zero()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "text",
|
||||||
|
width = 200,
|
||||||
|
height = 40,
|
||||||
|
editable = true,
|
||||||
|
maxLength = 0,
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with negative maxLength
|
||||||
|
function TestElementUnhappyPaths:test_max_length_negative()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "text",
|
||||||
|
width = 200,
|
||||||
|
height = 40,
|
||||||
|
editable = true,
|
||||||
|
maxLength = -10,
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
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
|
||||||
|
|||||||
@@ -4,10 +4,25 @@ package.path = package.path .. ";./?.lua;./modules/?.lua"
|
|||||||
require("testing.loveStub")
|
require("testing.loveStub")
|
||||||
local luaunit = require("testing.luaunit")
|
local luaunit = require("testing.luaunit")
|
||||||
local ErrorHandler = require("modules.ErrorHandler")
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
local ErrorCodes = require("modules.ErrorCodes")
|
||||||
|
|
||||||
TestErrorHandler = {}
|
TestErrorHandler = {}
|
||||||
|
|
||||||
-- Test: error() throws with correct format
|
function TestErrorHandler:setUp()
|
||||||
|
-- Reset debug mode and logging before each test
|
||||||
|
ErrorHandler.setDebugMode(false)
|
||||||
|
ErrorHandler.setLogTarget("none") -- Disable logging during tests
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestErrorHandler:tearDown()
|
||||||
|
-- Clean up any test log files
|
||||||
|
os.remove("test-errors.log")
|
||||||
|
for i = 1, 5 do
|
||||||
|
os.remove("test-errors.log." .. i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: error() throws with correct format (backward compatibility)
|
||||||
function TestErrorHandler:test_error_throws_with_format()
|
function TestErrorHandler:test_error_throws_with_format()
|
||||||
local success, err = pcall(function()
|
local success, err = pcall(function()
|
||||||
ErrorHandler.error("TestModule", "Something went wrong")
|
ErrorHandler.error("TestModule", "Something went wrong")
|
||||||
@@ -17,7 +32,65 @@ function TestErrorHandler:test_error_throws_with_format()
|
|||||||
luaunit.assertStrContains(err, "[FlexLove - TestModule] Error: Something went wrong")
|
luaunit.assertStrContains(err, "[FlexLove - TestModule] Error: Something went wrong")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test: warn() prints with correct format
|
-- Test: error() with error code
|
||||||
|
function TestErrorHandler:test_error_with_code()
|
||||||
|
local success, err = pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Invalid property type")
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertFalse(success, "error() should throw")
|
||||||
|
luaunit.assertStrContains(err, "[FlexLove - TestModule] Error [FLEXLOVE_VAL_001]")
|
||||||
|
luaunit.assertStrContains(err, "Invalid property type")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: error() with error code and details
|
||||||
|
function TestErrorHandler:test_error_with_code_and_details()
|
||||||
|
local success, err = pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Invalid property type", {
|
||||||
|
property = "width",
|
||||||
|
expected = "number",
|
||||||
|
got = "string",
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertFalse(success, "error() should throw")
|
||||||
|
luaunit.assertStrContains(err, "[FLEXLOVE_VAL_001]")
|
||||||
|
luaunit.assertStrContains(err, "Details:")
|
||||||
|
luaunit.assertStrContains(err, "Property: width")
|
||||||
|
luaunit.assertStrContains(err, "Expected: number")
|
||||||
|
luaunit.assertStrContains(err, "Got: string")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: error() with error code, details, and custom suggestion
|
||||||
|
function TestErrorHandler:test_error_with_code_details_and_suggestion()
|
||||||
|
local success, err = pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Invalid property type", {
|
||||||
|
property = "width",
|
||||||
|
expected = "number",
|
||||||
|
got = "string",
|
||||||
|
}, "Use a number like width = 100")
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertFalse(success, "error() should throw")
|
||||||
|
luaunit.assertStrContains(err, "Suggestion: Use a number like width = 100")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: error() with code uses automatic suggestion
|
||||||
|
function TestErrorHandler:test_error_with_code_uses_auto_suggestion()
|
||||||
|
local success, err = pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Invalid property type", {
|
||||||
|
property = "width",
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertFalse(success, "error() should throw")
|
||||||
|
luaunit.assertStrContains(err, "Suggestion:")
|
||||||
|
-- Should contain suggestion from ErrorCodes
|
||||||
|
local suggestion = ErrorCodes.getSuggestion("VAL_001")
|
||||||
|
luaunit.assertStrContains(err, suggestion)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: warn() prints with correct format (backward compatibility)
|
||||||
function TestErrorHandler:test_warn_prints_with_format()
|
function TestErrorHandler:test_warn_prints_with_format()
|
||||||
-- Capture print output by mocking print
|
-- Capture print output by mocking print
|
||||||
local captured = nil
|
local captured = nil
|
||||||
@@ -34,20 +107,60 @@ function TestErrorHandler:test_warn_prints_with_format()
|
|||||||
luaunit.assertEquals(captured, "[FlexLove - TestModule] Warning: This is a warning")
|
luaunit.assertEquals(captured, "[FlexLove - TestModule] Warning: This is a warning")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Test: warn() with error code
|
||||||
|
function TestErrorHandler:test_warn_with_code()
|
||||||
|
local captured = nil
|
||||||
|
local originalPrint = print
|
||||||
|
print = function(msg)
|
||||||
|
captured = msg
|
||||||
|
end
|
||||||
|
|
||||||
|
ErrorHandler.warn("TestModule", "VAL_001", "Potentially invalid property")
|
||||||
|
|
||||||
|
print = originalPrint
|
||||||
|
|
||||||
|
luaunit.assertNotNil(captured, "warn() should print")
|
||||||
|
luaunit.assertStrContains(captured, "[FlexLove - TestModule] Warning [FLEXLOVE_VAL_001]")
|
||||||
|
luaunit.assertStrContains(captured, "Potentially invalid property")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: warn() with details
|
||||||
|
function TestErrorHandler:test_warn_with_details()
|
||||||
|
local captured = nil
|
||||||
|
local originalPrint = print
|
||||||
|
print = function(msg)
|
||||||
|
captured = msg
|
||||||
|
end
|
||||||
|
|
||||||
|
ErrorHandler.warn("TestModule", "VAL_001", "Check this property", {
|
||||||
|
property = "height",
|
||||||
|
value = "auto",
|
||||||
|
})
|
||||||
|
|
||||||
|
print = originalPrint
|
||||||
|
|
||||||
|
luaunit.assertNotNil(captured, "warn() should print")
|
||||||
|
luaunit.assertStrContains(captured, "Details:")
|
||||||
|
luaunit.assertStrContains(captured, "Property: height")
|
||||||
|
luaunit.assertStrContains(captured, "Value: auto")
|
||||||
|
end
|
||||||
|
|
||||||
-- Test: assertNotNil returns true for non-nil value
|
-- Test: assertNotNil returns true for non-nil value
|
||||||
function TestErrorHandler:test_assertNotNil_returns_true_for_valid()
|
function TestErrorHandler:test_assertNotNil_returns_true_for_valid()
|
||||||
local result = ErrorHandler.assertNotNil("TestModule", "some value", "testParam")
|
local result = ErrorHandler.assertNotNil("TestModule", "some value", "testParam")
|
||||||
luaunit.assertTrue(result, "assertNotNil should return true for non-nil value")
|
luaunit.assertTrue(result, "assertNotNil should return true for non-nil value")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test: assertNotNil throws for nil value
|
-- Test: assertNotNil throws for nil value (now uses error codes)
|
||||||
function TestErrorHandler:test_assertNotNil_throws_for_nil()
|
function TestErrorHandler:test_assertNotNil_throws_for_nil()
|
||||||
local success, err = pcall(function()
|
local success, err = pcall(function()
|
||||||
ErrorHandler.assertNotNil("TestModule", nil, "testParam")
|
ErrorHandler.assertNotNil("TestModule", nil, "testParam")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
luaunit.assertFalse(success, "assertNotNil should throw for nil")
|
luaunit.assertFalse(success, "assertNotNil should throw for nil")
|
||||||
luaunit.assertStrContains(err, "Parameter 'testParam' cannot be nil")
|
luaunit.assertStrContains(err, "[FLEXLOVE_VAL_003]")
|
||||||
|
luaunit.assertStrContains(err, "Required parameter missing")
|
||||||
|
luaunit.assertStrContains(err, "testParam")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test: assertType returns true for correct type
|
-- Test: assertType returns true for correct type
|
||||||
@@ -62,14 +175,16 @@ function TestErrorHandler:test_assertType_returns_true_for_valid()
|
|||||||
luaunit.assertTrue(result, "assertType should return true for table")
|
luaunit.assertTrue(result, "assertType should return true for table")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test: assertType throws for wrong type
|
-- Test: assertType throws for wrong type (now uses error codes)
|
||||||
function TestErrorHandler:test_assertType_throws_for_wrong_type()
|
function TestErrorHandler:test_assertType_throws_for_wrong_type()
|
||||||
local success, err = pcall(function()
|
local success, err = pcall(function()
|
||||||
ErrorHandler.assertType("TestModule", 123, "string", "testParam")
|
ErrorHandler.assertType("TestModule", 123, "string", "testParam")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
luaunit.assertFalse(success, "assertType should throw for wrong type")
|
luaunit.assertFalse(success, "assertType should throw for wrong type")
|
||||||
luaunit.assertStrContains(err, "Parameter 'testParam' must be string, got number")
|
luaunit.assertStrContains(err, "[FLEXLOVE_VAL_001]")
|
||||||
|
luaunit.assertStrContains(err, "Invalid property type")
|
||||||
|
luaunit.assertStrContains(err, "testParam")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test: assertRange returns true for value in range
|
-- Test: assertRange returns true for value in range
|
||||||
@@ -84,24 +199,27 @@ function TestErrorHandler:test_assertRange_returns_true_for_valid()
|
|||||||
luaunit.assertTrue(result, "assertRange should accept max boundary")
|
luaunit.assertTrue(result, "assertRange should accept max boundary")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test: assertRange throws for value below min
|
-- Test: assertRange throws for value below min (now uses error codes)
|
||||||
function TestErrorHandler:test_assertRange_throws_for_below_min()
|
function TestErrorHandler:test_assertRange_throws_for_below_min()
|
||||||
local success, err = pcall(function()
|
local success, err = pcall(function()
|
||||||
ErrorHandler.assertRange("TestModule", -1, 0, 10, "testParam")
|
ErrorHandler.assertRange("TestModule", -1, 0, 10, "testParam")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
luaunit.assertFalse(success, "assertRange should throw for value below min")
|
luaunit.assertFalse(success, "assertRange should throw for value below min")
|
||||||
luaunit.assertStrContains(err, "Parameter 'testParam' must be between 0 and 10, got -1")
|
luaunit.assertStrContains(err, "[FLEXLOVE_VAL_002]")
|
||||||
|
luaunit.assertStrContains(err, "Property value out of range")
|
||||||
|
luaunit.assertStrContains(err, "testParam")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test: assertRange throws for value above max
|
-- Test: assertRange throws for value above max (now uses error codes)
|
||||||
function TestErrorHandler:test_assertRange_throws_for_above_max()
|
function TestErrorHandler:test_assertRange_throws_for_above_max()
|
||||||
local success, err = pcall(function()
|
local success, err = pcall(function()
|
||||||
ErrorHandler.assertRange("TestModule", 11, 0, 10, "testParam")
|
ErrorHandler.assertRange("TestModule", 11, 0, 10, "testParam")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
luaunit.assertFalse(success, "assertRange should throw for value above max")
|
luaunit.assertFalse(success, "assertRange should throw for value above max")
|
||||||
luaunit.assertStrContains(err, "Parameter 'testParam' must be between 0 and 10, got 11")
|
luaunit.assertStrContains(err, "[FLEXLOVE_VAL_002]")
|
||||||
|
luaunit.assertStrContains(err, "Property value out of range")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test: warnDeprecated prints deprecation warning
|
-- Test: warnDeprecated prints deprecation warning
|
||||||
@@ -136,6 +254,181 @@ function TestErrorHandler:test_warnCommonMistake_prints_message()
|
|||||||
luaunit.assertStrContains(captured, "Width is zero. Suggestion: Set width to positive value")
|
luaunit.assertStrContains(captured, "Width is zero. Suggestion: Set width to positive value")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Test: debug mode enables stack traces
|
||||||
|
function TestErrorHandler:test_debug_mode_enables_stack_trace()
|
||||||
|
ErrorHandler.setDebugMode(true)
|
||||||
|
|
||||||
|
local success, err = pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Test error")
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertFalse(success, "error() should throw")
|
||||||
|
luaunit.assertStrContains(err, "Stack trace:")
|
||||||
|
|
||||||
|
ErrorHandler.setDebugMode(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: setStackTrace independently
|
||||||
|
function TestErrorHandler:test_set_stack_trace()
|
||||||
|
ErrorHandler.setStackTrace(true)
|
||||||
|
|
||||||
|
local success, err = pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Test error")
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertFalse(success, "error() should throw")
|
||||||
|
luaunit.assertStrContains(err, "Stack trace:")
|
||||||
|
|
||||||
|
ErrorHandler.setStackTrace(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: error code validation
|
||||||
|
function TestErrorHandler:test_invalid_error_code_fallback()
|
||||||
|
local success, err = pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "INVALID_CODE", "This is a message")
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertFalse(success, "error() should throw")
|
||||||
|
-- Should treat as message (backward compatibility)
|
||||||
|
luaunit.assertStrContains(err, "INVALID_CODE")
|
||||||
|
luaunit.assertStrContains(err, "This is a message")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: details formatting with long values
|
||||||
|
function TestErrorHandler:test_details_with_long_values()
|
||||||
|
local longValue = string.rep("x", 150)
|
||||||
|
local success, err = pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Test", {
|
||||||
|
shortValue = "short",
|
||||||
|
longValue = longValue,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertFalse(success, "error() should throw")
|
||||||
|
luaunit.assertStrContains(err, "ShortValue: short")
|
||||||
|
-- Long value should be truncated
|
||||||
|
luaunit.assertStrContains(err, "...")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: file logging
|
||||||
|
function TestErrorHandler:test_file_logging()
|
||||||
|
ErrorHandler.setLogTarget("file")
|
||||||
|
ErrorHandler.setLogFile("test-errors.log")
|
||||||
|
|
||||||
|
-- Trigger an error (will be caught)
|
||||||
|
local success = pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Test file logging")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Check file was created and contains log
|
||||||
|
local file = io.open("test-errors.log", "r")
|
||||||
|
luaunit.assertNotNil(file, "Log file should be created")
|
||||||
|
|
||||||
|
if file then
|
||||||
|
local content = file:read("*all")
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
luaunit.assertStrContains(content, "ERROR")
|
||||||
|
luaunit.assertStrContains(content, "TestModule")
|
||||||
|
luaunit.assertStrContains(content, "Test file logging")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cleanup
|
||||||
|
ErrorHandler.setLogTarget("none")
|
||||||
|
os.remove("test-errors.log")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: log level filtering
|
||||||
|
function TestErrorHandler:test_log_level_filtering()
|
||||||
|
ErrorHandler.setLogTarget("file")
|
||||||
|
ErrorHandler.setLogFile("test-errors.log")
|
||||||
|
ErrorHandler.setLogLevel("ERROR") -- Only log errors, not warnings
|
||||||
|
|
||||||
|
-- Trigger a warning (should not be logged)
|
||||||
|
ErrorHandler.warn("TestModule", "VAL_001", "Test warning")
|
||||||
|
|
||||||
|
-- Trigger an error (should be logged)
|
||||||
|
pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Test error")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Check file
|
||||||
|
local file = io.open("test-errors.log", "r")
|
||||||
|
if file then
|
||||||
|
local content = file:read("*all")
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
luaunit.assertStrContains(content, "Test error")
|
||||||
|
luaunit.assertFalse(content:find("Test warning") ~= nil, "Warning should not be logged")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cleanup
|
||||||
|
ErrorHandler.setLogTarget("none")
|
||||||
|
ErrorHandler.setLogLevel("WARNING")
|
||||||
|
os.remove("test-errors.log")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: JSON format
|
||||||
|
function TestErrorHandler:test_json_format()
|
||||||
|
ErrorHandler.setLogTarget("file")
|
||||||
|
ErrorHandler.setLogFile("test-errors.log")
|
||||||
|
ErrorHandler.setLogFormat("json")
|
||||||
|
|
||||||
|
pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Test JSON", {
|
||||||
|
property = "width",
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
local file = io.open("test-errors.log", "r")
|
||||||
|
if file then
|
||||||
|
local content = file:read("*all")
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
-- Should be valid JSON-like
|
||||||
|
luaunit.assertStrContains(content, '"level":"ERROR"')
|
||||||
|
luaunit.assertStrContains(content, '"module":"TestModule"')
|
||||||
|
luaunit.assertStrContains(content, '"message":"Test JSON"')
|
||||||
|
luaunit.assertStrContains(content, '"details":')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cleanup
|
||||||
|
ErrorHandler.setLogTarget("none")
|
||||||
|
ErrorHandler.setLogFormat("human")
|
||||||
|
os.remove("test-errors.log")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: log rotation
|
||||||
|
function TestErrorHandler:test_log_rotation()
|
||||||
|
ErrorHandler.setLogTarget("file")
|
||||||
|
ErrorHandler.setLogFile("test-errors.log")
|
||||||
|
ErrorHandler.enableLogRotation({ maxSize = 100, maxFiles = 2 }) -- Very small for testing
|
||||||
|
|
||||||
|
-- Write multiple errors to trigger rotation
|
||||||
|
for i = 1, 10 do
|
||||||
|
pcall(function()
|
||||||
|
ErrorHandler.error("TestModule", "VAL_001", "Test rotation error number " .. i)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check that rotation occurred (main file should exist)
|
||||||
|
local file = io.open("test-errors.log", "r")
|
||||||
|
luaunit.assertNotNil(file, "Main log file should exist")
|
||||||
|
if file then
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check that rotated files might exist (depending on log size)
|
||||||
|
-- We won't assert this as it depends on exact message size
|
||||||
|
|
||||||
|
-- Cleanup
|
||||||
|
ErrorHandler.setLogTarget("none")
|
||||||
|
ErrorHandler.enableLogRotation(true) -- Reset to defaults
|
||||||
|
os.remove("test-errors.log")
|
||||||
|
os.remove("test-errors.log.1")
|
||||||
|
os.remove("test-errors.log.2")
|
||||||
|
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
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ end
|
|||||||
function TestFlexLove:testModuleLoads()
|
function TestFlexLove:testModuleLoads()
|
||||||
luaunit.assertNotNil(FlexLove)
|
luaunit.assertNotNil(FlexLove)
|
||||||
luaunit.assertNotNil(FlexLove._VERSION)
|
luaunit.assertNotNil(FlexLove._VERSION)
|
||||||
luaunit.assertEquals(FlexLove._VERSION, "0.2.0")
|
luaunit.assertEquals(FlexLove._VERSION, "0.2.2")
|
||||||
luaunit.assertNotNil(FlexLove._DESCRIPTION)
|
luaunit.assertNotNil(FlexLove._DESCRIPTION)
|
||||||
luaunit.assertNotNil(FlexLove._URL)
|
luaunit.assertNotNil(FlexLove._URL)
|
||||||
luaunit.assertNotNil(FlexLove._LICENSE)
|
luaunit.assertNotNil(FlexLove._LICENSE)
|
||||||
@@ -44,8 +44,8 @@ function TestFlexLove:testInitWithBaseScale()
|
|||||||
FlexLove.init({
|
FlexLove.init({
|
||||||
baseScale = {
|
baseScale = {
|
||||||
width = 1920,
|
width = 1920,
|
||||||
height = 1080
|
height = 1080,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
luaunit.assertNotNil(FlexLove.baseScale)
|
luaunit.assertNotNil(FlexLove.baseScale)
|
||||||
@@ -56,7 +56,7 @@ end
|
|||||||
-- Test: init() with partial baseScale (uses defaults)
|
-- Test: init() with partial baseScale (uses defaults)
|
||||||
function TestFlexLove:testInitWithPartialBaseScale()
|
function TestFlexLove:testInitWithPartialBaseScale()
|
||||||
FlexLove.init({
|
FlexLove.init({
|
||||||
baseScale = {}
|
baseScale = {},
|
||||||
})
|
})
|
||||||
|
|
||||||
luaunit.assertNotNil(FlexLove.baseScale)
|
luaunit.assertNotNil(FlexLove.baseScale)
|
||||||
@@ -69,13 +69,13 @@ function TestFlexLove:testInitWithStringTheme()
|
|||||||
-- Pre-register a theme
|
-- Pre-register a theme
|
||||||
local theme = Theme.new({
|
local theme = Theme.new({
|
||||||
name = "test",
|
name = "test",
|
||||||
components = {}
|
components = {},
|
||||||
})
|
})
|
||||||
|
|
||||||
-- init() tries to load and then set active, which may fail if theme path doesn't exist
|
-- init() tries to load and then set active, which may fail if theme path doesn't exist
|
||||||
-- Just check that it doesn't crash
|
-- Just check that it doesn't crash
|
||||||
FlexLove.init({
|
FlexLove.init({
|
||||||
theme = "test"
|
theme = "test",
|
||||||
})
|
})
|
||||||
|
|
||||||
-- The theme setting may fail silently, so just check it doesn't crash
|
-- The theme setting may fail silently, so just check it doesn't crash
|
||||||
@@ -87,8 +87,8 @@ function TestFlexLove:testInitWithTableTheme()
|
|||||||
FlexLove.init({
|
FlexLove.init({
|
||||||
theme = {
|
theme = {
|
||||||
name = "custom",
|
name = "custom",
|
||||||
components = {}
|
components = {},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
luaunit.assertEquals(FlexLove.defaultTheme, "custom")
|
luaunit.assertEquals(FlexLove.defaultTheme, "custom")
|
||||||
@@ -97,7 +97,7 @@ end
|
|||||||
-- Test: init() with invalid theme (should not crash)
|
-- Test: init() with invalid theme (should not crash)
|
||||||
function TestFlexLove:testInitWithInvalidTheme()
|
function TestFlexLove:testInitWithInvalidTheme()
|
||||||
FlexLove.init({
|
FlexLove.init({
|
||||||
theme = "nonexistent-theme"
|
theme = "nonexistent-theme",
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Should not crash, just print warning
|
-- Should not crash, just print warning
|
||||||
@@ -107,7 +107,7 @@ end
|
|||||||
-- Test: init() with immediateMode = true
|
-- Test: init() with immediateMode = true
|
||||||
function TestFlexLove:testInitWithImmediateMode()
|
function TestFlexLove:testInitWithImmediateMode()
|
||||||
FlexLove.init({
|
FlexLove.init({
|
||||||
immediateMode = true
|
immediateMode = true,
|
||||||
})
|
})
|
||||||
|
|
||||||
luaunit.assertEquals(FlexLove.getMode(), "immediate")
|
luaunit.assertEquals(FlexLove.getMode(), "immediate")
|
||||||
@@ -116,7 +116,7 @@ end
|
|||||||
-- Test: init() with immediateMode = false
|
-- Test: init() with immediateMode = false
|
||||||
function TestFlexLove:testInitWithRetainedMode()
|
function TestFlexLove:testInitWithRetainedMode()
|
||||||
FlexLove.init({
|
FlexLove.init({
|
||||||
immediateMode = false
|
immediateMode = false,
|
||||||
})
|
})
|
||||||
|
|
||||||
luaunit.assertEquals(FlexLove.getMode(), "retained")
|
luaunit.assertEquals(FlexLove.getMode(), "retained")
|
||||||
@@ -125,7 +125,7 @@ end
|
|||||||
-- Test: init() with autoFrameManagement
|
-- Test: init() with autoFrameManagement
|
||||||
function TestFlexLove:testInitWithAutoFrameManagement()
|
function TestFlexLove:testInitWithAutoFrameManagement()
|
||||||
FlexLove.init({
|
FlexLove.init({
|
||||||
autoFrameManagement = true
|
autoFrameManagement = true,
|
||||||
})
|
})
|
||||||
|
|
||||||
luaunit.assertEquals(FlexLove._autoFrameManagement, true)
|
luaunit.assertEquals(FlexLove._autoFrameManagement, true)
|
||||||
@@ -135,7 +135,7 @@ end
|
|||||||
function TestFlexLove:testInitWithStateConfig()
|
function TestFlexLove:testInitWithStateConfig()
|
||||||
FlexLove.init({
|
FlexLove.init({
|
||||||
stateRetentionFrames = 5,
|
stateRetentionFrames = 5,
|
||||||
maxStateEntries = 100
|
maxStateEntries = 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
luaunit.assertTrue(true) -- Should configure StateManager
|
luaunit.assertTrue(true) -- Should configure StateManager
|
||||||
@@ -229,7 +229,7 @@ function TestFlexLove:testNewImmediateMode()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
id = "test-element",
|
id = "test-element",
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100
|
height = 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
luaunit.assertNotNil(element)
|
luaunit.assertNotNil(element)
|
||||||
@@ -246,7 +246,7 @@ function TestFlexLove:testNewAutoBeginFrame()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
id = "auto-begin-test",
|
id = "auto-begin-test",
|
||||||
width = 50,
|
width = 50,
|
||||||
height = 50
|
height = 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
luaunit.assertNotNil(element)
|
luaunit.assertNotNil(element)
|
||||||
@@ -307,10 +307,11 @@ function TestFlexLove:testDrawWithBothFuncs()
|
|||||||
local gameCalled = false
|
local gameCalled = false
|
||||||
local postCalled = false
|
local postCalled = false
|
||||||
|
|
||||||
FlexLove.draw(
|
FlexLove.draw(function()
|
||||||
function() gameCalled = true end,
|
gameCalled = true
|
||||||
function() postCalled = true end
|
end, function()
|
||||||
)
|
postCalled = true
|
||||||
|
end)
|
||||||
|
|
||||||
luaunit.assertTrue(gameCalled)
|
luaunit.assertTrue(gameCalled)
|
||||||
luaunit.assertTrue(postCalled)
|
luaunit.assertTrue(postCalled)
|
||||||
@@ -323,7 +324,7 @@ function TestFlexLove:testDrawWithElements()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100,
|
height = 100,
|
||||||
backgroundColor = Color.new(1, 1, 1, 1)
|
backgroundColor = Color.new(1, 1, 1, 1),
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove.draw()
|
FlexLove.draw()
|
||||||
@@ -338,7 +339,7 @@ function TestFlexLove:testDrawAutoEndFrame()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
id = "auto-end-test",
|
id = "auto-end-test",
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100
|
height = 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- draw() should call endFrame() if _autoBeganFrame is true
|
-- draw() should call endFrame() if _autoBeganFrame is true
|
||||||
@@ -361,7 +362,7 @@ function TestFlexLove:testUpdateRetainedMode()
|
|||||||
|
|
||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100
|
height = 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove.update(0.016)
|
FlexLove.update(0.016)
|
||||||
@@ -377,7 +378,7 @@ function TestFlexLove:testUpdateImmediateMode()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
id = "update-test",
|
id = "update-test",
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100
|
height = 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove.endFrame()
|
FlexLove.endFrame()
|
||||||
@@ -399,8 +400,8 @@ function TestFlexLove:testResizeWithBaseScale()
|
|||||||
FlexLove.init({
|
FlexLove.init({
|
||||||
baseScale = {
|
baseScale = {
|
||||||
width = 1920,
|
width = 1920,
|
||||||
height = 1080
|
height = 1080,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove.resize()
|
FlexLove.resize()
|
||||||
@@ -414,7 +415,7 @@ function TestFlexLove:testResizeWithElements()
|
|||||||
|
|
||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100
|
height = 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove.resize()
|
FlexLove.resize()
|
||||||
@@ -428,7 +429,7 @@ function TestFlexLove:testDestroy()
|
|||||||
|
|
||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100
|
height = 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove.destroy()
|
FlexLove.destroy()
|
||||||
@@ -453,7 +454,7 @@ function TestFlexLove:testTextInputWithFocus()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100,
|
height = 100,
|
||||||
editable = true
|
editable = true,
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove._focusedElement = element
|
FlexLove._focusedElement = element
|
||||||
@@ -477,7 +478,7 @@ function TestFlexLove:testKeyPressedWithFocus()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100,
|
height = 100,
|
||||||
editable = true
|
editable = true,
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove._focusedElement = element
|
FlexLove._focusedElement = element
|
||||||
@@ -502,7 +503,7 @@ function TestFlexLove:testWheelMovedImmediate()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
id = "wheel-test",
|
id = "wheel-test",
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100
|
height = 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove.endFrame()
|
FlexLove.endFrame()
|
||||||
@@ -527,7 +528,7 @@ function TestFlexLove:testGetStateCountImmediate()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
id = "state-test",
|
id = "state-test",
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100
|
height = 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove.endFrame()
|
FlexLove.endFrame()
|
||||||
@@ -552,7 +553,7 @@ function TestFlexLove:testClearStateImmediate()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
id = "clear-test",
|
id = "clear-test",
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100
|
height = 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
FlexLove.endFrame()
|
FlexLove.endFrame()
|
||||||
@@ -615,7 +616,7 @@ function TestFlexLove:testGetElementAtPosition()
|
|||||||
y = 0,
|
y = 0,
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100,
|
height = 100,
|
||||||
onEvent = function() end
|
onEvent = function() end,
|
||||||
})
|
})
|
||||||
|
|
||||||
local found = FlexLove.getElementAtPosition(50, 50)
|
local found = FlexLove.getElementAtPosition(50, 50)
|
||||||
@@ -631,7 +632,7 @@ function TestFlexLove:testGetElementAtPositionOutside()
|
|||||||
y = 0,
|
y = 0,
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 100,
|
height = 100,
|
||||||
onEvent = function() end
|
onEvent = function() end,
|
||||||
})
|
})
|
||||||
|
|
||||||
local found = FlexLove.getElementAtPosition(200, 200)
|
local found = FlexLove.getElementAtPosition(200, 200)
|
||||||
@@ -653,4 +654,586 @@ function TestFlexLove:testEnumsAccessible()
|
|||||||
luaunit.assertNotNil(FlexLove.enums.AlignItems)
|
luaunit.assertNotNil(FlexLove.enums.AlignItems)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- UNHAPPY PATH TESTS
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
TestFlexLoveUnhappyPaths = {}
|
||||||
|
|
||||||
|
function TestFlexLoveUnhappyPaths:setUp()
|
||||||
|
FlexLove.destroy()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestFlexLoveUnhappyPaths:tearDown()
|
||||||
|
FlexLove.destroy()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: init() with invalid config types
|
||||||
|
function TestFlexLoveUnhappyPaths:testInitWithInvalidConfigTypes()
|
||||||
|
-- nil and false should work (become {} via `config or {}`)
|
||||||
|
FlexLove.init(nil)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
FlexLove.init(false)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- String and number will error when trying to index them (config.errorLogFile, config.baseScale, etc.)
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.init("invalid")
|
||||||
|
end)
|
||||||
|
-- String indexing may work in Lua (returns nil), check actual behavior
|
||||||
|
-- Actually strings can be indexed but will return nil for most keys
|
||||||
|
-- So this might not error! Let's just verify it doesn't crash
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
success = pcall(function()
|
||||||
|
FlexLove.init(123)
|
||||||
|
end)
|
||||||
|
-- Numbers can't be indexed, should error
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: init() with invalid baseScale values
|
||||||
|
function TestFlexLoveUnhappyPaths:testInitWithInvalidBaseScale()
|
||||||
|
-- Negative width/height (should work, just unusual)
|
||||||
|
FlexLove.init({
|
||||||
|
baseScale = {
|
||||||
|
width = -1920,
|
||||||
|
height = -1080,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
luaunit.assertTrue(true) -- Should not crash
|
||||||
|
|
||||||
|
-- Zero width/height (division by zero risk)
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.init({
|
||||||
|
baseScale = {
|
||||||
|
width = 0,
|
||||||
|
height = 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
-- May or may not error depending on implementation
|
||||||
|
|
||||||
|
-- Non-numeric values (should error)
|
||||||
|
success = pcall(function()
|
||||||
|
FlexLove.init({
|
||||||
|
baseScale = {
|
||||||
|
width = "invalid",
|
||||||
|
height = "invalid",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success) -- Should error on division
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: init() with invalid theme types
|
||||||
|
function TestFlexLoveUnhappyPaths:testInitWithInvalidThemeTypes()
|
||||||
|
-- Numeric theme
|
||||||
|
FlexLove.init({ theme = 123 })
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Boolean theme
|
||||||
|
FlexLove.init({ theme = true })
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Function theme
|
||||||
|
FlexLove.init({ theme = function() end })
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: init() with invalid immediateMode values
|
||||||
|
function TestFlexLoveUnhappyPaths:testInitWithInvalidImmediateMode()
|
||||||
|
FlexLove.init({ immediateMode = "yes" })
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
FlexLove.init({ immediateMode = 1 })
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
FlexLove.init({ immediateMode = {} })
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: init() with invalid state config values
|
||||||
|
function TestFlexLoveUnhappyPaths:testInitWithInvalidStateConfig()
|
||||||
|
-- Negative values
|
||||||
|
FlexLove.init({
|
||||||
|
stateRetentionFrames = -5,
|
||||||
|
maxStateEntries = -100,
|
||||||
|
})
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Non-numeric values
|
||||||
|
FlexLove.init({
|
||||||
|
stateRetentionFrames = "five",
|
||||||
|
maxStateEntries = "hundred",
|
||||||
|
})
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Zero values
|
||||||
|
FlexLove.init({
|
||||||
|
stateRetentionFrames = 0,
|
||||||
|
maxStateEntries = 0,
|
||||||
|
})
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: setMode() with nil
|
||||||
|
function TestFlexLoveUnhappyPaths:testSetModeNil()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.setMode(nil)
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: setMode() with number
|
||||||
|
function TestFlexLoveUnhappyPaths:testSetModeNumber()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.setMode(123)
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: setMode() with table
|
||||||
|
function TestFlexLoveUnhappyPaths:testSetModeTable()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.setMode({ mode = "immediate" })
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: setMode() with empty string
|
||||||
|
function TestFlexLoveUnhappyPaths:testSetModeEmptyString()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.setMode("")
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: setMode() with case-sensitive variation
|
||||||
|
function TestFlexLoveUnhappyPaths:testSetModeCaseSensitive()
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.setMode("Immediate")
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
|
||||||
|
success = pcall(function()
|
||||||
|
FlexLove.setMode("RETAINED")
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: beginFrame() multiple times without endFrame()
|
||||||
|
function TestFlexLoveUnhappyPaths:testBeginFrameMultipleTimes()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local frameNum1 = FlexLove._frameNumber
|
||||||
|
|
||||||
|
FlexLove.beginFrame() -- Call again without ending
|
||||||
|
local frameNum2 = FlexLove._frameNumber
|
||||||
|
|
||||||
|
-- Frame number should increment each time
|
||||||
|
luaunit.assertTrue(frameNum2 > frameNum1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: endFrame() without beginFrame()
|
||||||
|
function TestFlexLoveUnhappyPaths:testEndFrameWithoutBegin()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.endFrame() -- Should not crash
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: endFrame() multiple times
|
||||||
|
function TestFlexLoveUnhappyPaths:testEndFrameMultipleTimes()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
FlexLove.endFrame()
|
||||||
|
FlexLove.endFrame() -- Call again
|
||||||
|
luaunit.assertTrue(true) -- Should not crash
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: new() with nil props
|
||||||
|
function TestFlexLoveUnhappyPaths:testNewWithNilProps()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
local element = FlexLove.new(nil)
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: new() with invalid width/height
|
||||||
|
function TestFlexLoveUnhappyPaths:testNewWithInvalidDimensions()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- Negative dimensions
|
||||||
|
local element = FlexLove.new({ width = -100, height = -50 })
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
|
||||||
|
-- Zero dimensions
|
||||||
|
element = FlexLove.new({ width = 0, height = 0 })
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
|
||||||
|
-- Invalid string dimensions (now returns fallback 0px with warning)
|
||||||
|
local success, element = pcall(function()
|
||||||
|
return FlexLove.new({ width = "invalid", height = "invalid" })
|
||||||
|
end)
|
||||||
|
luaunit.assertTrue(success) -- Units.parse returns fallback (0, "px") instead of erroring
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: new() with invalid position
|
||||||
|
function TestFlexLoveUnhappyPaths:testNewWithInvalidPosition()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- Negative positions
|
||||||
|
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
|
||||||
|
function TestFlexLoveUnhappyPaths:testNewWithCircularParent()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local parent = FlexLove.new({ width = 200, height = 200 })
|
||||||
|
local child = FlexLove.new({ width = 100, height = 100, parent = parent })
|
||||||
|
|
||||||
|
-- Try to make parent a child of child (circular reference)
|
||||||
|
-- This should be prevented by the design
|
||||||
|
luaunit.assertNotEquals(parent.parent, child)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: new() in immediate mode without frame
|
||||||
|
function TestFlexLoveUnhappyPaths:testNewImmediateModeNoFrame()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
-- Don't call beginFrame()
|
||||||
|
|
||||||
|
local element = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
luaunit.assertTrue(FlexLove._autoBeganFrame)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: draw() with invalid function types
|
||||||
|
function TestFlexLoveUnhappyPaths:testDrawWithInvalidFunctions()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- Non-function gameDrawFunc
|
||||||
|
FlexLove.draw("not a function", nil)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
FlexLove.draw(123, nil)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
FlexLove.draw({}, nil)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Non-function postDrawFunc
|
||||||
|
FlexLove.draw(nil, "not a function")
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
FlexLove.draw(nil, 456)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: draw() with function that errors
|
||||||
|
function TestFlexLoveUnhappyPaths:testDrawWithErroringFunction()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.draw(function()
|
||||||
|
error("Intentional error")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: update() with invalid dt
|
||||||
|
function TestFlexLoveUnhappyPaths:testUpdateWithInvalidDt()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- Negative dt
|
||||||
|
FlexLove.update(-0.016)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Zero dt
|
||||||
|
FlexLove.update(0)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Huge dt
|
||||||
|
FlexLove.update(1000)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- nil dt
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.update(nil)
|
||||||
|
end)
|
||||||
|
-- May or may not error depending on implementation
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: textinput() with invalid text
|
||||||
|
function TestFlexLoveUnhappyPaths:testTextInputWithInvalidText()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- nil text
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.textinput(nil)
|
||||||
|
end)
|
||||||
|
-- Should handle gracefully
|
||||||
|
|
||||||
|
-- Number
|
||||||
|
FlexLove.textinput(123)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Empty string
|
||||||
|
FlexLove.textinput("")
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Very long string
|
||||||
|
FlexLove.textinput(string.rep("a", 10000))
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: keypressed() with invalid keys
|
||||||
|
function TestFlexLoveUnhappyPaths:testKeyPressedWithInvalidKeys()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- nil key
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.keypressed(nil, nil, false)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Empty strings
|
||||||
|
FlexLove.keypressed("", "", false)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Invalid key names
|
||||||
|
FlexLove.keypressed("invalidkey", "invalidscancode", false)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Non-boolean isrepeat
|
||||||
|
FlexLove.keypressed("a", "a", "yes")
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: wheelmoved() with invalid values
|
||||||
|
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()
|
||||||
|
FlexLove.wheelmoved(nil, nil)
|
||||||
|
end)
|
||||||
|
-- May or may not error
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: resize() repeatedly in quick succession
|
||||||
|
function TestFlexLoveUnhappyPaths:testResizeRapidly()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local element = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
|
||||||
|
for i = 1, 100 do
|
||||||
|
FlexLove.resize()
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(true) -- Should not crash
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: destroy() twice
|
||||||
|
function TestFlexLoveUnhappyPaths:testDestroyTwice()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local element = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
|
||||||
|
FlexLove.destroy()
|
||||||
|
FlexLove.destroy() -- Call again
|
||||||
|
|
||||||
|
luaunit.assertTrue(true) -- Should not crash
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: clearState() with invalid ID types
|
||||||
|
function TestFlexLoveUnhappyPaths:testClearStateWithInvalidIds()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
|
||||||
|
-- nil ID (should error)
|
||||||
|
local success = pcall(function()
|
||||||
|
FlexLove.clearState(nil)
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
|
|
||||||
|
-- Number ID (should work, gets converted to string)
|
||||||
|
FlexLove.clearState(123)
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Table ID (may error)
|
||||||
|
success = pcall(function()
|
||||||
|
FlexLove.clearState({})
|
||||||
|
end)
|
||||||
|
-- May or may not work depending on tostring implementation
|
||||||
|
|
||||||
|
-- Empty string (should work)
|
||||||
|
FlexLove.clearState("")
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Non-existent ID (should work, just does nothing)
|
||||||
|
FlexLove.clearState("nonexistent-id-12345")
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: getElementAtPosition() with invalid coordinates
|
||||||
|
function TestFlexLoveUnhappyPaths:testGetElementAtPositionWithInvalidCoords()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- Negative coordinates
|
||||||
|
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()
|
||||||
|
FlexLove.getElementAtPosition(nil, nil)
|
||||||
|
end)
|
||||||
|
-- May or may not error
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Creating element with conflicting properties
|
||||||
|
function TestFlexLoveUnhappyPaths:testNewWithConflictingProperties()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- Both width auto and explicit
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
autosizing = { width = true },
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
|
||||||
|
-- Conflicting positioning
|
||||||
|
element = FlexLove.new({
|
||||||
|
positioning = "flex",
|
||||||
|
x = 100, -- Absolute position with flex
|
||||||
|
y = 100,
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Multiple mode switches
|
||||||
|
function TestFlexLoveUnhappyPaths:testMultipleModeSwitches()
|
||||||
|
for i = 1, 10 do
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
end
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Creating elements during draw
|
||||||
|
function TestFlexLoveUnhappyPaths:testCreatingElementsDuringDraw()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local drawCalled = false
|
||||||
|
FlexLove.draw(function()
|
||||||
|
-- Try to create element during draw
|
||||||
|
local element = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
luaunit.assertNotNil(element)
|
||||||
|
drawCalled = true
|
||||||
|
end)
|
||||||
|
|
||||||
|
luaunit.assertTrue(drawCalled)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: State operations in retained mode (should do nothing)
|
||||||
|
function TestFlexLoveUnhappyPaths:testStateOperationsInRetainedMode()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local count = FlexLove.getStateCount()
|
||||||
|
luaunit.assertEquals(count, 0)
|
||||||
|
|
||||||
|
FlexLove.clearState("any-id")
|
||||||
|
FlexLove.clearAllStates()
|
||||||
|
|
||||||
|
local stats = FlexLove.getStateStats()
|
||||||
|
luaunit.assertEquals(stats.stateCount, 0)
|
||||||
|
luaunit.assertEquals(stats.frameNumber, 0)
|
||||||
|
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()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local parent = FlexLove.new({ width = 500, height = 500 })
|
||||||
|
local current = parent
|
||||||
|
|
||||||
|
-- Create 100 levels of nesting
|
||||||
|
for i = 1, 100 do
|
||||||
|
local child = FlexLove.new({
|
||||||
|
width = 10,
|
||||||
|
height = 10,
|
||||||
|
parent = current,
|
||||||
|
})
|
||||||
|
current = child
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(true) -- Should not crash
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Error logging configuration edge cases
|
||||||
|
function TestFlexLoveUnhappyPaths:testErrorLoggingEdgeCases()
|
||||||
|
-- Empty error log file path
|
||||||
|
FlexLove.init({ errorLogFile = "" })
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Invalid path characters
|
||||||
|
FlexLove.init({ errorLogFile = "/invalid/path/\0/file.log" })
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Both enableErrorLogging and errorLogFile
|
||||||
|
FlexLove.init({
|
||||||
|
enableErrorLogging = true,
|
||||||
|
errorLogFile = "test.log",
|
||||||
|
})
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Immediate mode frame management edge cases
|
||||||
|
function TestFlexLoveUnhappyPaths:testImmediateModeFrameEdgeCases()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
|
||||||
|
-- Begin, draw (should auto-end), then end again
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
FlexLove.draw()
|
||||||
|
FlexLove.endFrame() -- Extra end
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
|
||||||
|
-- Multiple draws without frames
|
||||||
|
FlexLove.draw()
|
||||||
|
FlexLove.draw()
|
||||||
|
FlexLove.draw()
|
||||||
|
luaunit.assertTrue(true)
|
||||||
|
end
|
||||||
|
|
||||||
return TestFlexLove
|
return TestFlexLove
|
||||||
|
|||||||
@@ -65,9 +65,12 @@ function TestImageRenderer:testCalculateFitWithNegativeBoundsHeight()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function TestImageRenderer:testCalculateFitWithInvalidFitMode()
|
function TestImageRenderer:testCalculateFitWithInvalidFitMode()
|
||||||
luaunit.assertError(function()
|
-- Now uses 'fill' fallback with warning instead of error
|
||||||
ImageRenderer.calculateFit(100, 100, 200, 200, "invalid-mode")
|
local result = ImageRenderer.calculateFit(100, 100, 200, 200, "invalid-mode")
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
-- Should fall back to 'fill' mode behavior (scales to fill bounds)
|
||||||
|
luaunit.assertEquals(result.scaleX, 2)
|
||||||
|
luaunit.assertEquals(result.scaleY, 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageRenderer:testCalculateFitWithNilFitMode()
|
function TestImageRenderer:testCalculateFitWithNilFitMode()
|
||||||
@@ -208,9 +211,10 @@ function TestImageRenderer:testDrawWithOpacityGreaterThanOne()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function TestImageRenderer:testDrawWithInvalidFitMode()
|
function TestImageRenderer:testDrawWithInvalidFitMode()
|
||||||
luaunit.assertError(function()
|
-- Now uses 'fill' fallback with warning instead of error
|
||||||
|
-- Should not throw an error, just use fill mode
|
||||||
ImageRenderer.draw(self.mockImage, 0, 0, 100, 100, "invalid")
|
ImageRenderer.draw(self.mockImage, 0, 0, 100, 100, "invalid")
|
||||||
end)
|
luaunit.assertTrue(true) -- If we reach here, no error was thrown
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageRenderer:testCalculateFitWithVerySmallBounds()
|
function TestImageRenderer:testCalculateFitWithVerySmallBounds()
|
||||||
|
|||||||
@@ -24,51 +24,67 @@ function TestImageScaler:testScaleNearestWithNilSource()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleNearestWithZeroSourceWidth()
|
function TestImageScaler:testScaleNearestWithZeroSourceWidth()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 0, 10, 20, 20)
|
local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 0, 10, 20, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleNearestWithZeroSourceHeight()
|
function TestImageScaler:testScaleNearestWithZeroSourceHeight()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 0, 20, 20)
|
local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 0, 20, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleNearestWithNegativeSourceWidth()
|
function TestImageScaler:testScaleNearestWithNegativeSourceWidth()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleNearest(self.mockImageData, 0, 0, -10, 10, 20, 20)
|
local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, -10, 10, 20, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleNearestWithNegativeSourceHeight()
|
function TestImageScaler:testScaleNearestWithNegativeSourceHeight()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, -10, 20, 20)
|
local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, -10, 20, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleNearestWithZeroDestWidth()
|
function TestImageScaler:testScaleNearestWithZeroDestWidth()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 0, 20)
|
local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 0, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleNearestWithZeroDestHeight()
|
function TestImageScaler:testScaleNearestWithZeroDestHeight()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, 0)
|
local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, 0)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleNearestWithNegativeDestWidth()
|
function TestImageScaler:testScaleNearestWithNegativeDestWidth()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, -20, 20)
|
local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, -20, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleNearestWithNegativeDestHeight()
|
function TestImageScaler:testScaleNearestWithNegativeDestHeight()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, -20)
|
local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, -20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Unhappy path tests for scaleBilinear
|
-- Unhappy path tests for scaleBilinear
|
||||||
@@ -80,51 +96,67 @@ function TestImageScaler:testScaleBilinearWithNilSource()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleBilinearWithZeroSourceWidth()
|
function TestImageScaler:testScaleBilinearWithZeroSourceWidth()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 0, 10, 20, 20)
|
local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 0, 10, 20, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleBilinearWithZeroSourceHeight()
|
function TestImageScaler:testScaleBilinearWithZeroSourceHeight()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 0, 20, 20)
|
local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 0, 20, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleBilinearWithNegativeSourceWidth()
|
function TestImageScaler:testScaleBilinearWithNegativeSourceWidth()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, -10, 10, 20, 20)
|
local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, -10, 10, 20, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleBilinearWithNegativeSourceHeight()
|
function TestImageScaler:testScaleBilinearWithNegativeSourceHeight()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, -10, 20, 20)
|
local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, -10, 20, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleBilinearWithZeroDestWidth()
|
function TestImageScaler:testScaleBilinearWithZeroDestWidth()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 0, 20)
|
local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 0, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleBilinearWithZeroDestHeight()
|
function TestImageScaler:testScaleBilinearWithZeroDestHeight()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, 0)
|
local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, 0)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleBilinearWithNegativeDestWidth()
|
function TestImageScaler:testScaleBilinearWithNegativeDestWidth()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, -20, 20)
|
local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, -20, 20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestImageScaler:testScaleBilinearWithNegativeDestHeight()
|
function TestImageScaler:testScaleBilinearWithNegativeDestHeight()
|
||||||
luaunit.assertError(function()
|
-- Now returns 1x1 transparent fallback with warning instead of error
|
||||||
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, -20)
|
local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, -20)
|
||||||
end)
|
luaunit.assertNotNil(result)
|
||||||
|
luaunit.assertEquals(result:getWidth(), 1)
|
||||||
|
luaunit.assertEquals(result:getHeight(), 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Edge case tests
|
-- Edge case tests
|
||||||
|
|||||||
@@ -161,13 +161,15 @@ end
|
|||||||
-- Test: new() with imagePath (successful load via cache)
|
-- Test: new() with imagePath (successful load via cache)
|
||||||
function TestRenderer:testNewWithImagePathSuccessfulLoad()
|
function TestRenderer:testNewWithImagePathSuccessfulLoad()
|
||||||
local mockImage = {
|
local mockImage = {
|
||||||
getDimensions = function() return 50, 50 end
|
getDimensions = function()
|
||||||
|
return 50, 50
|
||||||
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Pre-populate the cache so load succeeds
|
-- Pre-populate the cache so load succeeds
|
||||||
ImageCache._cache["test/image.png"] = {
|
ImageCache._cache["test/image.png"] = {
|
||||||
image = mockImage,
|
image = mockImage,
|
||||||
imageData = nil
|
imageData = nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
local renderer = Renderer.new({
|
local renderer = Renderer.new({
|
||||||
|
|||||||
@@ -89,20 +89,33 @@ function TestUnitsParse:testParseZero()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsParse:testParseInvalidType()
|
function TestUnitsParse:testParseInvalidType()
|
||||||
luaunit.assertErrorMsgContains("Invalid unit value type", Units.parse, nil)
|
-- Now returns fallback value (0, "px") with warning instead of error
|
||||||
|
local value, unit = Units.parse(nil)
|
||||||
|
luaunit.assertEquals(value, 0)
|
||||||
|
luaunit.assertEquals(unit, "px")
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsParse:testParseInvalidString()
|
function TestUnitsParse:testParseInvalidString()
|
||||||
luaunit.assertErrorMsgContains("Invalid unit format", Units.parse, "abc")
|
-- Now returns fallback value (0, "px") with warning instead of error
|
||||||
|
local value, unit = Units.parse("abc")
|
||||||
|
luaunit.assertEquals(value, 0)
|
||||||
|
luaunit.assertEquals(unit, "px")
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsParse:testParseInvalidUnit()
|
function TestUnitsParse:testParseInvalidUnit()
|
||||||
luaunit.assertErrorMsgContains("Unknown unit", Units.parse, "100xyz")
|
-- Now extracts the number and treats as pixels with warning instead of error
|
||||||
|
-- "100xyz" -> extracts 100, ignores invalid unit "xyz", treats as "100px"
|
||||||
|
local value, unit = Units.parse("100xyz")
|
||||||
|
luaunit.assertEquals(value, 100)
|
||||||
|
luaunit.assertEquals(unit, "px")
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsParse:testParseWithSpace()
|
function TestUnitsParse:testParseWithSpace()
|
||||||
-- Spaces between number and unit should be invalid
|
-- Spaces between number and unit should be invalid
|
||||||
luaunit.assertErrorMsgContains("contains space", Units.parse, "100 px")
|
-- Now returns fallback value (0, "px") with warning instead of error
|
||||||
|
local value, unit = Units.parse("100 px")
|
||||||
|
luaunit.assertEquals(value, 0)
|
||||||
|
luaunit.assertEquals(unit, "px")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test suite for Units.resolve()
|
-- Test suite for Units.resolve()
|
||||||
@@ -369,11 +382,17 @@ function TestUnitsEdgeCases:testResolveZeroParentSize()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsEdgeCases:testParseEmptyString()
|
function TestUnitsEdgeCases:testParseEmptyString()
|
||||||
luaunit.assertErrorMsgContains("Invalid unit format", Units.parse, "")
|
-- Now returns fallback value (0, "px") with warning instead of error
|
||||||
|
local value, unit = Units.parse("")
|
||||||
|
luaunit.assertEquals(value, 0)
|
||||||
|
luaunit.assertEquals(unit, "px")
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsEdgeCases:testParseOnlyUnit()
|
function TestUnitsEdgeCases:testParseOnlyUnit()
|
||||||
luaunit.assertErrorMsgContains("Missing numeric value before unit", Units.parse, "px")
|
-- Now returns fallback value (0, "px") with warning instead of error
|
||||||
|
local value, unit = Units.parse("px")
|
||||||
|
luaunit.assertEquals(value, 0)
|
||||||
|
luaunit.assertEquals(unit, "px")
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsEdgeCases:testResolveNegativePercentage()
|
function TestUnitsEdgeCases:testResolveNegativePercentage()
|
||||||
|
|||||||
Reference in New Issue
Block a user