start testing

This commit is contained in:
Michael Freno
2025-11-14 20:59:40 -05:00
parent a218b4abed
commit 1dab1a197e
18 changed files with 4886 additions and 11 deletions

View File

@@ -0,0 +1,514 @@
-- Import test framework
package.path = package.path .. ";../../?.lua"
local luaunit = require("testing.luaunit")
-- Set up LÖVE stub environment
require("testing.loveStub")
-- Import the Color module
local Color = require("modules.Color")
-- Test Suite for Color Validation
TestColorValidation = {}
-- === validateColorChannel Tests ===
function TestColorValidation:test_validateColorChannel_valid_0to1()
local valid, clamped = Color.validateColorChannel(0.5, 1)
luaunit.assertTrue(valid)
luaunit.assertEquals(clamped, 0.5)
end
function TestColorValidation:test_validateColorChannel_valid_0to255()
local valid, clamped = Color.validateColorChannel(128, 255)
luaunit.assertTrue(valid)
luaunit.assertAlmostEquals(clamped, 128/255, 0.001)
end
function TestColorValidation:test_validateColorChannel_clamp_below_min()
local valid, clamped = Color.validateColorChannel(-0.5, 1)
luaunit.assertTrue(valid)
luaunit.assertEquals(clamped, 0)
end
function TestColorValidation:test_validateColorChannel_clamp_above_max()
local valid, clamped = Color.validateColorChannel(1.5, 1)
luaunit.assertTrue(valid)
luaunit.assertEquals(clamped, 1)
end
function TestColorValidation:test_validateColorChannel_clamp_above_255()
local valid, clamped = Color.validateColorChannel(300, 255)
luaunit.assertTrue(valid)
luaunit.assertEquals(clamped, 1)
end
function TestColorValidation:test_validateColorChannel_nan()
local valid, clamped = Color.validateColorChannel(0/0, 1)
luaunit.assertFalse(valid)
luaunit.assertNil(clamped)
end
function TestColorValidation:test_validateColorChannel_infinity()
local valid, clamped = Color.validateColorChannel(math.huge, 1)
luaunit.assertFalse(valid)
luaunit.assertNil(clamped)
end
function TestColorValidation:test_validateColorChannel_negative_infinity()
local valid, clamped = Color.validateColorChannel(-math.huge, 1)
luaunit.assertFalse(valid)
luaunit.assertNil(clamped)
end
function TestColorValidation:test_validateColorChannel_non_number()
local valid, clamped = Color.validateColorChannel("0.5", 1)
luaunit.assertFalse(valid)
luaunit.assertNil(clamped)
end
-- === validateHexColor Tests ===
function TestColorValidation:test_validateHexColor_valid_6digit()
local valid, err = Color.validateHexColor("#FF0000")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateHexColor_valid_6digit_no_hash()
local valid, err = Color.validateHexColor("FF0000")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateHexColor_valid_8digit()
local valid, err = Color.validateHexColor("#FF0000AA")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateHexColor_valid_3digit()
local valid, err = Color.validateHexColor("#F00")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateHexColor_valid_lowercase()
local valid, err = Color.validateHexColor("#ff0000")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateHexColor_valid_mixed_case()
local valid, err = Color.validateHexColor("#Ff00Aa")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateHexColor_invalid_length()
local valid, err = Color.validateHexColor("#FF00")
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "Invalid hex length")
end
function TestColorValidation:test_validateHexColor_invalid_characters()
local valid, err = Color.validateHexColor("#GG0000")
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "Invalid hex characters")
end
function TestColorValidation:test_validateHexColor_not_string()
local valid, err = Color.validateHexColor(123)
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "must be a string")
end
-- === validateRGBColor Tests ===
function TestColorValidation:test_validateRGBColor_valid_0to1()
local valid, err = Color.validateRGBColor(0.5, 0.5, 0.5, 1.0, 1)
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateRGBColor_valid_0to255()
local valid, err = Color.validateRGBColor(128, 128, 128, 255, 255)
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateRGBColor_valid_no_alpha()
local valid, err = Color.validateRGBColor(0.5, 0.5, 0.5, nil, 1)
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateRGBColor_invalid_red()
local valid, err = Color.validateRGBColor("red", 0.5, 0.5, 1.0, 1)
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "Invalid red channel")
end
function TestColorValidation:test_validateRGBColor_invalid_green()
local valid, err = Color.validateRGBColor(0.5, nil, 0.5, 1.0, 1)
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "Invalid green channel")
end
function TestColorValidation:test_validateRGBColor_invalid_blue()
local valid, err = Color.validateRGBColor(0.5, 0.5, {}, 1.0, 1)
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "Invalid blue channel")
end
function TestColorValidation:test_validateRGBColor_invalid_alpha()
local valid, err = Color.validateRGBColor(0.5, 0.5, 0.5, 0/0, 1)
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "Invalid alpha channel")
end
-- === validateNamedColor Tests ===
function TestColorValidation:test_validateNamedColor_valid_lowercase()
local valid, err = Color.validateNamedColor("red")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateNamedColor_valid_uppercase()
local valid, err = Color.validateNamedColor("RED")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateNamedColor_valid_mixed_case()
local valid, err = Color.validateNamedColor("BlUe")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateNamedColor_invalid_name()
local valid, err = Color.validateNamedColor("notacolor")
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "Unknown color name")
end
function TestColorValidation:test_validateNamedColor_not_string()
local valid, err = Color.validateNamedColor(123)
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "must be a string")
end
-- === isValidColorFormat Tests ===
function TestColorValidation:test_isValidColorFormat_hex_6digit()
local format = Color.isValidColorFormat("#FF0000")
luaunit.assertEquals(format, "hex")
end
function TestColorValidation:test_isValidColorFormat_hex_8digit()
local format = Color.isValidColorFormat("#FF0000AA")
luaunit.assertEquals(format, "hex")
end
function TestColorValidation:test_isValidColorFormat_hex_3digit()
local format = Color.isValidColorFormat("#F00")
luaunit.assertEquals(format, "hex")
end
function TestColorValidation:test_isValidColorFormat_named()
local format = Color.isValidColorFormat("red")
luaunit.assertEquals(format, "named")
end
function TestColorValidation:test_isValidColorFormat_table_array()
local format = Color.isValidColorFormat({0.5, 0.5, 0.5, 1.0})
luaunit.assertEquals(format, "table")
end
function TestColorValidation:test_isValidColorFormat_table_named()
local format = Color.isValidColorFormat({r=0.5, g=0.5, b=0.5, a=1.0})
luaunit.assertEquals(format, "table")
end
function TestColorValidation:test_isValidColorFormat_table_color_instance()
local color = Color.new(0.5, 0.5, 0.5, 1.0)
local format = Color.isValidColorFormat(color)
luaunit.assertEquals(format, "table")
end
function TestColorValidation:test_isValidColorFormat_invalid_string()
local format = Color.isValidColorFormat("not-a-color")
luaunit.assertNil(format)
end
function TestColorValidation:test_isValidColorFormat_invalid_table()
local format = Color.isValidColorFormat({invalid=true})
luaunit.assertNil(format)
end
function TestColorValidation:test_isValidColorFormat_nil()
local format = Color.isValidColorFormat(nil)
luaunit.assertNil(format)
end
function TestColorValidation:test_isValidColorFormat_number()
local format = Color.isValidColorFormat(12345)
luaunit.assertNil(format)
end
-- === validateColor Tests ===
function TestColorValidation:test_validateColor_hex()
local valid, err = Color.validateColor("#FF0000")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateColor_named()
local valid, err = Color.validateColor("blue")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateColor_table_array()
local valid, err = Color.validateColor({0.5, 0.5, 0.5, 1.0})
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateColor_table_named()
local valid, err = Color.validateColor({r=0.5, g=0.5, b=0.5, a=1.0})
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateColor_named_disallowed()
local valid, err = Color.validateColor("red", {allowNamed=false})
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "Named colors not allowed")
end
function TestColorValidation:test_validateColor_require_alpha_8digit()
local valid, err = Color.validateColor("#FF0000AA", {requireAlpha=true})
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestColorValidation:test_validateColor_require_alpha_6digit()
local valid, err = Color.validateColor("#FF0000", {requireAlpha=true})
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "Alpha channel required")
end
function TestColorValidation:test_validateColor_nil()
local valid, err = Color.validateColor(nil)
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "nil")
end
function TestColorValidation:test_validateColor_invalid()
local valid, err = Color.validateColor("not-a-color")
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
luaunit.assertStrContains(err, "Invalid color format")
end
-- === sanitizeColor Tests ===
function TestColorValidation:test_sanitizeColor_hex_6digit()
local color = Color.sanitizeColor("#FF0000")
luaunit.assertAlmostEquals(color.r, 1.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
luaunit.assertAlmostEquals(color.a, 1.0, 0.01)
end
function TestColorValidation:test_sanitizeColor_hex_8digit()
local color = Color.sanitizeColor("#FF000080")
luaunit.assertAlmostEquals(color.r, 1.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
luaunit.assertAlmostEquals(color.a, 0.5, 0.01)
end
function TestColorValidation:test_sanitizeColor_hex_3digit()
local color = Color.sanitizeColor("#F00")
luaunit.assertAlmostEquals(color.r, 1.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
luaunit.assertAlmostEquals(color.a, 1.0, 0.01)
end
function TestColorValidation:test_sanitizeColor_named_red()
local color = Color.sanitizeColor("red")
luaunit.assertAlmostEquals(color.r, 1.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
luaunit.assertAlmostEquals(color.a, 1.0, 0.01)
end
function TestColorValidation:test_sanitizeColor_named_blue_uppercase()
local color = Color.sanitizeColor("BLUE")
luaunit.assertAlmostEquals(color.r, 0.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 1.0, 0.01)
luaunit.assertAlmostEquals(color.a, 1.0, 0.01)
end
function TestColorValidation:test_sanitizeColor_named_transparent()
local color = Color.sanitizeColor("transparent")
luaunit.assertAlmostEquals(color.r, 0.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
luaunit.assertAlmostEquals(color.a, 0.0, 0.01)
end
function TestColorValidation:test_sanitizeColor_table_array()
local color = Color.sanitizeColor({0.5, 0.6, 0.7, 0.8})
luaunit.assertAlmostEquals(color.r, 0.5, 0.01)
luaunit.assertAlmostEquals(color.g, 0.6, 0.01)
luaunit.assertAlmostEquals(color.b, 0.7, 0.01)
luaunit.assertAlmostEquals(color.a, 0.8, 0.01)
end
function TestColorValidation:test_sanitizeColor_table_named()
local color = Color.sanitizeColor({r=0.5, g=0.6, b=0.7, a=0.8})
luaunit.assertAlmostEquals(color.r, 0.5, 0.01)
luaunit.assertAlmostEquals(color.g, 0.6, 0.01)
luaunit.assertAlmostEquals(color.b, 0.7, 0.01)
luaunit.assertAlmostEquals(color.a, 0.8, 0.01)
end
function TestColorValidation:test_sanitizeColor_table_array_clamp_high()
local color = Color.sanitizeColor({1.5, 1.5, 1.5, 1.5})
luaunit.assertAlmostEquals(color.r, 1.0, 0.01)
luaunit.assertAlmostEquals(color.g, 1.0, 0.01)
luaunit.assertAlmostEquals(color.b, 1.0, 0.01)
luaunit.assertAlmostEquals(color.a, 1.0, 0.01)
end
function TestColorValidation:test_sanitizeColor_table_array_clamp_low()
local color = Color.sanitizeColor({-0.5, -0.5, -0.5, -0.5})
luaunit.assertAlmostEquals(color.r, 0.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
luaunit.assertAlmostEquals(color.a, 0.0, 0.01)
end
function TestColorValidation:test_sanitizeColor_table_no_alpha()
local color = Color.sanitizeColor({0.5, 0.6, 0.7})
luaunit.assertAlmostEquals(color.r, 0.5, 0.01)
luaunit.assertAlmostEquals(color.g, 0.6, 0.01)
luaunit.assertAlmostEquals(color.b, 0.7, 0.01)
luaunit.assertAlmostEquals(color.a, 1.0, 0.01)
end
function TestColorValidation:test_sanitizeColor_color_instance()
local original = Color.new(0.5, 0.6, 0.7, 0.8)
local color = Color.sanitizeColor(original)
luaunit.assertEquals(color, original)
end
function TestColorValidation:test_sanitizeColor_invalid_returns_default()
local color = Color.sanitizeColor("invalid-color")
luaunit.assertAlmostEquals(color.r, 0.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
luaunit.assertAlmostEquals(color.a, 1.0, 0.01)
end
function TestColorValidation:test_sanitizeColor_invalid_custom_default()
local defaultColor = Color.new(1.0, 1.0, 1.0, 1.0)
local color = Color.sanitizeColor("invalid-color", defaultColor)
luaunit.assertEquals(color, defaultColor)
end
function TestColorValidation:test_sanitizeColor_nil_returns_default()
local color = Color.sanitizeColor(nil)
luaunit.assertAlmostEquals(color.r, 0.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
luaunit.assertAlmostEquals(color.a, 1.0, 0.01)
end
-- === Color.parse Tests ===
function TestColorValidation:test_parse_hex()
local color = Color.parse("#00FF00")
luaunit.assertAlmostEquals(color.r, 0.0, 0.01)
luaunit.assertAlmostEquals(color.g, 1.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
end
function TestColorValidation:test_parse_named()
local color = Color.parse("green")
luaunit.assertAlmostEquals(color.r, 0.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.502, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
end
function TestColorValidation:test_parse_table()
local color = Color.parse({0.25, 0.50, 0.75, 1.0})
luaunit.assertAlmostEquals(color.r, 0.25, 0.01)
luaunit.assertAlmostEquals(color.g, 0.50, 0.01)
luaunit.assertAlmostEquals(color.b, 0.75, 0.01)
end
function TestColorValidation:test_parse_invalid()
local color = Color.parse("not-a-color")
luaunit.assertAlmostEquals(color.r, 0.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
end
-- === Edge Case Tests ===
function TestColorValidation:test_edge_empty_string()
local valid, err = Color.validateColor("")
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
end
function TestColorValidation:test_edge_whitespace()
local valid, err = Color.validateColor(" ")
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
end
function TestColorValidation:test_edge_empty_table()
local valid, err = Color.validateColor({})
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
end
function TestColorValidation:test_edge_hex_with_spaces()
local valid, err = Color.validateColor(" #FF0000 ")
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
end
function TestColorValidation:test_edge_negative_values_clamped()
local color = Color.sanitizeColor({-1, -2, -3, -4})
luaunit.assertAlmostEquals(color.r, 0.0, 0.01)
luaunit.assertAlmostEquals(color.g, 0.0, 0.01)
luaunit.assertAlmostEquals(color.b, 0.0, 0.01)
luaunit.assertAlmostEquals(color.a, 0.0, 0.01)
end
-- Run tests if this file is executed directly
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -0,0 +1,281 @@
-- Test suite for path validation functions
-- Tests sanitizePath, isPathSafe, validatePath, getFileExtension, hasAllowedExtension
package.path = package.path .. ";./?.lua;./modules/?.lua"
-- Load love stub before anything else
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local utils = require("modules.utils")
-- Test suite for sanitizePath
TestSanitizePath = {}
function TestSanitizePath:testSanitizePath_NilInput()
local result = utils.sanitizePath(nil)
luaunit.assertEquals(result, "")
end
function TestSanitizePath:testSanitizePath_EmptyString()
local result = utils.sanitizePath("")
luaunit.assertEquals(result, "")
end
function TestSanitizePath:testSanitizePath_Whitespace()
local result = utils.sanitizePath(" /path/to/file ")
luaunit.assertEquals(result, "/path/to/file")
end
function TestSanitizePath:testSanitizePath_Backslashes()
local result = utils.sanitizePath("C:\\path\\to\\file")
luaunit.assertEquals(result, "C:/path/to/file")
end
function TestSanitizePath:testSanitizePath_DuplicateSlashes()
local result = utils.sanitizePath("/path//to///file")
luaunit.assertEquals(result, "/path/to/file")
end
function TestSanitizePath:testSanitizePath_TrailingSlash()
local result = utils.sanitizePath("/path/to/dir/")
luaunit.assertEquals(result, "/path/to/dir")
-- Root should keep trailing slash
result = utils.sanitizePath("/")
luaunit.assertEquals(result, "/")
end
function TestSanitizePath:testSanitizePath_MixedIssues()
local result = utils.sanitizePath(" C:\\path\\\\to///file.txt ")
luaunit.assertEquals(result, "C:/path/to/file.txt")
end
-- Test suite for isPathSafe
TestIsPathSafe = {}
function TestIsPathSafe:testIsPathSafe_EmptyPath()
local safe, reason = utils.isPathSafe("")
luaunit.assertFalse(safe)
luaunit.assertStrContains(reason, "empty")
end
function TestIsPathSafe:testIsPathSafe_NilPath()
local safe, reason = utils.isPathSafe(nil)
luaunit.assertFalse(safe)
luaunit.assertNotNil(reason)
end
function TestIsPathSafe:testIsPathSafe_ParentDirectory()
local safe, reason = utils.isPathSafe("../etc/passwd")
luaunit.assertFalse(safe)
luaunit.assertStrContains(reason, "..")
end
function TestIsPathSafe:testIsPathSafe_MultipleParentDirectories()
local safe, reason = utils.isPathSafe("../../../../../../etc/passwd")
luaunit.assertFalse(safe)
luaunit.assertStrContains(reason, "..")
end
function TestIsPathSafe:testIsPathSafe_HiddenParentDirectory()
local safe, reason = utils.isPathSafe("/path/to/../../../etc/passwd")
luaunit.assertFalse(safe)
luaunit.assertStrContains(reason, "..")
end
function TestIsPathSafe:testIsPathSafe_NullBytes()
local safe, reason = utils.isPathSafe("/path/to/file\0.txt")
luaunit.assertFalse(safe)
luaunit.assertStrContains(reason, "null")
end
function TestIsPathSafe:testIsPathSafe_EncodedTraversal()
local safe, reason = utils.isPathSafe("/path/%2e%2e/file")
luaunit.assertFalse(safe)
luaunit.assertStrContains(reason, "encoded")
end
function TestIsPathSafe:testIsPathSafe_LegitimatePathNoBaseDir()
local safe, reason = utils.isPathSafe("/themes/default.lua")
luaunit.assertTrue(safe)
luaunit.assertNil(reason)
end
function TestIsPathSafe:testIsPathSafe_LegitimatePathWithBaseDir()
local safe, reason = utils.isPathSafe("/allowed/themes/default.lua", "/allowed")
luaunit.assertTrue(safe)
luaunit.assertNil(reason)
end
function TestIsPathSafe:testIsPathSafe_RelativePathWithBaseDir()
local safe, reason = utils.isPathSafe("themes/default.lua", "/allowed")
luaunit.assertTrue(safe)
luaunit.assertNil(reason)
end
function TestIsPathSafe:testIsPathSafe_OutsideBaseDir()
local safe, reason = utils.isPathSafe("/other/themes/default.lua", "/allowed")
luaunit.assertFalse(safe)
luaunit.assertStrContains(reason, "outside")
end
-- Test suite for validatePath
TestValidatePath = {}
function TestValidatePath:testValidatePath_EmptyPath()
local valid, err = utils.validatePath("")
luaunit.assertFalse(valid)
luaunit.assertStrContains(err, "empty")
end
function TestValidatePath:testValidatePath_NilPath()
local valid, err = utils.validatePath(nil)
luaunit.assertFalse(valid)
luaunit.assertStrContains(err, "empty")
end
function TestValidatePath:testValidatePath_TooLong()
local longPath = string.rep("a", 5000)
local valid, err = utils.validatePath(longPath, { maxLength = 100 })
luaunit.assertFalse(valid)
luaunit.assertStrContains(err, "maximum length")
end
function TestValidatePath:testValidatePath_TraversalAttack()
local valid, err = utils.validatePath("../../../etc/passwd")
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
end
function TestValidatePath:testValidatePath_AllowedExtension()
local valid, err = utils.validatePath("theme.lua", { allowedExtensions = { "lua", "txt" } })
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestValidatePath:testValidatePath_DisallowedExtension()
local valid, err = utils.validatePath("script.exe", { allowedExtensions = { "lua", "txt" } })
luaunit.assertFalse(valid)
luaunit.assertStrContains(err, "not allowed")
end
function TestValidatePath:testValidatePath_NoExtension()
local valid, err = utils.validatePath("README", { allowedExtensions = { "lua", "txt" } })
luaunit.assertFalse(valid)
luaunit.assertStrContains(err, "no file extension")
end
function TestValidatePath:testValidatePath_CaseInsensitiveExtension()
local valid, err = utils.validatePath("Theme.LUA", { allowedExtensions = { "lua" } })
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestValidatePath:testValidatePath_WithBaseDir()
local valid, err = utils.validatePath("themes/default.lua", { baseDir = "/allowed" })
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
function TestValidatePath:testValidatePath_OutsideBaseDir()
local valid, err = utils.validatePath("/other/theme.lua", { baseDir = "/allowed" })
luaunit.assertFalse(valid)
luaunit.assertStrContains(err, "outside")
end
-- Test suite for getFileExtension
TestGetFileExtension = {}
function TestGetFileExtension:testGetFileExtension_SimpleExtension()
local ext = utils.getFileExtension("file.txt")
luaunit.assertEquals(ext, "txt")
end
function TestGetFileExtension:testGetFileExtension_MultipleDotsInPath()
local ext = utils.getFileExtension("/path/to/file.name.txt")
luaunit.assertEquals(ext, "txt")
end
function TestGetFileExtension:testGetFileExtension_NoExtension()
local ext = utils.getFileExtension("README")
luaunit.assertNil(ext)
end
function TestGetFileExtension:testGetFileExtension_NilPath()
local ext = utils.getFileExtension(nil)
luaunit.assertNil(ext)
end
function TestGetFileExtension:testGetFileExtension_CaseSensitive()
local ext = utils.getFileExtension("File.TXT")
luaunit.assertEquals(ext, "txt") -- Should be lowercase
end
function TestGetFileExtension:testGetFileExtension_LongExtension()
local ext = utils.getFileExtension("archive.tar.gz")
luaunit.assertEquals(ext, "gz")
end
-- Test suite for hasAllowedExtension
TestHasAllowedExtension = {}
function TestHasAllowedExtension:testHasAllowedExtension_Allowed()
local allowed = utils.hasAllowedExtension("file.txt", { "txt", "lua" })
luaunit.assertTrue(allowed)
end
function TestHasAllowedExtension:testHasAllowedExtension_NotAllowed()
local allowed = utils.hasAllowedExtension("file.exe", { "txt", "lua" })
luaunit.assertFalse(allowed)
end
function TestHasAllowedExtension:testHasAllowedExtension_CaseInsensitive()
local allowed = utils.hasAllowedExtension("File.TXT", { "txt", "lua" })
luaunit.assertTrue(allowed)
end
function TestHasAllowedExtension:testHasAllowedExtension_NoExtension()
local allowed = utils.hasAllowedExtension("README", { "txt", "lua" })
luaunit.assertFalse(allowed)
end
function TestHasAllowedExtension:testHasAllowedExtension_EmptyArray()
local allowed = utils.hasAllowedExtension("file.txt", {})
luaunit.assertFalse(allowed)
end
-- Test suite for security scenarios
TestPathSecurity = {}
function TestPathSecurity:testPathSecurity_WindowsTraversal()
local safe = utils.isPathSafe("..\\..\\..\\windows\\system32")
luaunit.assertFalse(safe)
end
function TestPathSecurity:testPathSecurity_MixedSeparators()
local safe = utils.isPathSafe("../path\\to/../file")
luaunit.assertFalse(safe)
end
function TestPathSecurity:testPathSecurity_DoubleEncodedTraversal()
local safe = utils.isPathSafe("%252e%252e%252f")
luaunit.assertFalse(safe)
end
function TestPathSecurity:testPathSecurity_LegitimateFileWithDots()
-- Files with dots in name should be OK (not ..)
local safe = utils.isPathSafe("/path/to/file.backup.txt")
luaunit.assertTrue(safe)
end
function TestPathSecurity:testPathSecurity_HiddenFiles()
-- Hidden files (starting with .) should be OK
local safe = utils.isPathSafe("/path/to/.hidden")
luaunit.assertTrue(safe)
end
-- Run tests if this file is executed directly
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -0,0 +1,236 @@
-- Test suite for text sanitization functions
-- Tests sanitizeText, validateTextInput, escapeHtml, escapeLuaPattern, stripNonPrintable
package.path = package.path .. ";./?.lua;./modules/?.lua"
-- Load love stub before anything else
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local utils = require("modules.utils")
-- Test suite for sanitizeText
TestSanitizeText = {}
function TestSanitizeText:testSanitizeText_NilInput()
local result = utils.sanitizeText(nil)
luaunit.assertEquals(result, "")
end
function TestSanitizeText:testSanitizeText_NonStringInput()
local result = utils.sanitizeText(123)
luaunit.assertEquals(result, "123")
result = utils.sanitizeText(true)
luaunit.assertEquals(result, "true")
end
function TestSanitizeText:testSanitizeText_NullBytes()
local result = utils.sanitizeText("Hello\0World")
luaunit.assertEquals(result, "HelloWorld")
end
function TestSanitizeText:testSanitizeText_ControlCharacters()
-- Test removal of various control characters
local result = utils.sanitizeText("Hello\1\2\3World")
luaunit.assertEquals(result, "HelloWorld")
end
function TestSanitizeText:testSanitizeText_AllowNewlines()
local result = utils.sanitizeText("Hello\nWorld", { allowNewlines = true })
luaunit.assertEquals(result, "Hello\nWorld")
result = utils.sanitizeText("Hello\nWorld", { allowNewlines = false })
luaunit.assertEquals(result, "HelloWorld")
end
function TestSanitizeText:testSanitizeText_AllowTabs()
local result = utils.sanitizeText("Hello\tWorld", { allowTabs = true })
luaunit.assertEquals(result, "Hello\tWorld")
result = utils.sanitizeText("Hello\tWorld", { allowTabs = false })
luaunit.assertEquals(result, "HelloWorld")
end
function TestSanitizeText:testSanitizeText_TrimWhitespace()
local result = utils.sanitizeText(" Hello World ", { trimWhitespace = true })
luaunit.assertEquals(result, "Hello World")
result = utils.sanitizeText(" Hello World ", { trimWhitespace = false })
luaunit.assertEquals(result, " Hello World ")
end
function TestSanitizeText:testSanitizeText_MaxLength()
local longText = string.rep("a", 100)
local result = utils.sanitizeText(longText, { maxLength = 50 })
luaunit.assertEquals(#result, 50)
luaunit.assertEquals(result, string.rep("a", 50))
end
function TestSanitizeText:testSanitizeText_DefaultOptions()
-- Test with default options
local result = utils.sanitizeText(" Hello\nWorld\t ")
luaunit.assertEquals(result, "Hello\nWorld")
end
function TestSanitizeText:testSanitizeText_EmptyString()
local result = utils.sanitizeText("")
luaunit.assertEquals(result, "")
end
function TestSanitizeText:testSanitizeText_OnlyWhitespace()
local result = utils.sanitizeText(" \n \t ", { trimWhitespace = true })
luaunit.assertEquals(result, "")
end
-- Test suite for validateTextInput
TestValidateTextInput = {}
function TestValidateTextInput:testValidateTextInput_MinLength()
local valid, err = utils.validateTextInput("abc", { minLength = 3 })
luaunit.assertTrue(valid)
luaunit.assertNil(err)
valid, err = utils.validateTextInput("ab", { minLength = 3 })
luaunit.assertFalse(valid)
luaunit.assertStrContains(err, "at least")
end
function TestValidateTextInput:testValidateTextInput_MaxLength()
local valid, err = utils.validateTextInput("abc", { maxLength = 5 })
luaunit.assertTrue(valid)
luaunit.assertNil(err)
valid, err = utils.validateTextInput("abcdef", { maxLength = 5 })
luaunit.assertFalse(valid)
luaunit.assertStrContains(err, "at most")
end
function TestValidateTextInput:testValidateTextInput_Pattern()
local valid, err = utils.validateTextInput("123", { pattern = "^%d+$" })
luaunit.assertTrue(valid)
luaunit.assertNil(err)
valid, err = utils.validateTextInput("abc", { pattern = "^%d+$" })
luaunit.assertFalse(valid)
luaunit.assertNotNil(err)
end
function TestValidateTextInput:testValidateTextInput_AllowedChars()
local valid, err = utils.validateTextInput("abc123", { allowedChars = "a-z0-9" })
luaunit.assertTrue(valid)
valid, err = utils.validateTextInput("abc123!", { allowedChars = "a-z0-9" })
luaunit.assertFalse(valid)
luaunit.assertStrContains(err, "invalid characters")
end
function TestValidateTextInput:testValidateTextInput_ForbiddenChars()
local valid, err = utils.validateTextInput("hello world", { forbiddenChars = "@#$%%" })
luaunit.assertTrue(valid)
valid, err = utils.validateTextInput("hello@world", { forbiddenChars = "@#$%%" })
luaunit.assertFalse(valid)
luaunit.assertStrContains(err, "forbidden characters")
end
function TestValidateTextInput:testValidateTextInput_NoRules()
local valid, err = utils.validateTextInput("anything goes")
luaunit.assertTrue(valid)
luaunit.assertNil(err)
end
-- Test suite for escapeHtml
TestEscapeHtml = {}
function TestEscapeHtml:testEscapeHtml_BasicChars()
local result = utils.escapeHtml("<script>alert('xss')</script>")
luaunit.assertEquals(result, "&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;")
end
function TestEscapeHtml:testEscapeHtml_Ampersand()
local result = utils.escapeHtml("Tom & Jerry")
luaunit.assertEquals(result, "Tom &amp; Jerry")
end
function TestEscapeHtml:testEscapeHtml_Quotes()
local result = utils.escapeHtml('Hello "World"')
luaunit.assertEquals(result, "Hello &quot;World&quot;")
result = utils.escapeHtml("It's fine")
luaunit.assertEquals(result, "It&#39;s fine")
end
function TestEscapeHtml:testEscapeHtml_NilInput()
local result = utils.escapeHtml(nil)
luaunit.assertEquals(result, "")
end
function TestEscapeHtml:testEscapeHtml_EmptyString()
local result = utils.escapeHtml("")
luaunit.assertEquals(result, "")
end
-- Test suite for escapeLuaPattern
TestEscapeLuaPattern = {}
function TestEscapeLuaPattern:testEscapeLuaPattern_SpecialChars()
local result = utils.escapeLuaPattern("^$()%.[]*+-?")
luaunit.assertEquals(result, "%^%$%(%)%%%.%[%]%*%+%-%?")
end
function TestEscapeLuaPattern:testEscapeLuaPattern_NormalText()
local result = utils.escapeLuaPattern("Hello World")
luaunit.assertEquals(result, "Hello World")
end
function TestEscapeLuaPattern:testEscapeLuaPattern_NilInput()
local result = utils.escapeLuaPattern(nil)
luaunit.assertEquals(result, "")
end
function TestEscapeLuaPattern:testEscapeLuaPattern_UsageInMatch()
-- Test that escaped pattern can be used safely
local text = "The price is $10.50"
local escaped = utils.escapeLuaPattern("$10.50")
local found = text:match(escaped)
luaunit.assertEquals(found, "$10.50")
end
-- Test suite for stripNonPrintable
TestStripNonPrintable = {}
function TestStripNonPrintable:testStripNonPrintable_BasicText()
local result = utils.stripNonPrintable("Hello World")
luaunit.assertEquals(result, "Hello World")
end
function TestStripNonPrintable:testStripNonPrintable_KeepNewlines()
local result = utils.stripNonPrintable("Hello\nWorld")
luaunit.assertEquals(result, "Hello\nWorld")
end
function TestStripNonPrintable:testStripNonPrintable_KeepTabs()
local result = utils.stripNonPrintable("Hello\tWorld")
luaunit.assertEquals(result, "Hello\tWorld")
end
function TestStripNonPrintable:testStripNonPrintable_RemoveControlChars()
local result = utils.stripNonPrintable("Hello\1\2\3World")
luaunit.assertEquals(result, "HelloWorld")
end
function TestStripNonPrintable:testStripNonPrintable_NilInput()
local result = utils.stripNonPrintable(nil)
luaunit.assertEquals(result, "")
end
function TestStripNonPrintable:testStripNonPrintable_EmptyString()
local result = utils.stripNonPrintable("")
luaunit.assertEquals(result, "")
end
-- Run tests if this file is executed directly
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -0,0 +1,306 @@
-- Import test framework
package.path = package.path .. ";./?.lua;./game/?.lua"
local luaunit = require("testing.luaunit")
-- Set up LÖVE stub environment
require("testing.loveStub")
-- Import the TextEditor module and dependencies
local utils = require("modules.utils")
local Color = require("modules.Color")
-- Mock dependencies
local mockContext = {
_immediateMode = false,
_focusedElement = nil
}
local mockStateManager = {
getState = function() return nil end,
setState = function() end
}
-- Test Suite for TextEditor Sanitization
TestTextEditorSanitization = {}
---Helper to create a TextEditor instance
function TestTextEditorSanitization:_createEditor(config)
local TextEditor = require("modules.TextEditor")
config = config or {}
local deps = {
Context = mockContext,
StateManager = mockStateManager,
Color = Color,
utils = utils
}
return TextEditor.new(config, deps)
end
-- === Sanitization Enabled Tests ===
function TestTextEditorSanitization:test_sanitization_enabled_by_default()
local editor = self:_createEditor({editable = true})
luaunit.assertTrue(editor.sanitize)
end
function TestTextEditorSanitization:test_sanitization_can_be_disabled()
local editor = self:_createEditor({editable = true, sanitize = false})
luaunit.assertFalse(editor.sanitize)
end
function TestTextEditorSanitization:test_setText_removes_control_characters()
local editor = self:_createEditor({editable = true})
editor:setText("Hello\x00World\x01Test")
luaunit.assertEquals(editor:getText(), "HelloWorldTest")
end
function TestTextEditorSanitization:test_setText_preserves_valid_text()
local editor = self:_createEditor({editable = true})
editor:setText("Hello World! 123")
luaunit.assertEquals(editor:getText(), "Hello World! 123")
end
function TestTextEditorSanitization:test_setText_removes_multiple_control_chars()
local editor = self:_createEditor({editable = true})
editor:setText("Test\x00\x01\x02\x03\x04Data")
luaunit.assertEquals(editor:getText(), "TestData")
end
function TestTextEditorSanitization:test_setText_with_sanitization_disabled()
local editor = self:_createEditor({editable = true, sanitize = false})
editor:setText("Hello\x00World")
luaunit.assertEquals(editor:getText(), "Hello\x00World")
end
function TestTextEditorSanitization:test_setText_skip_sanitization_parameter()
local editor = self:_createEditor({editable = true})
editor:setText("Hello\x00World", true) -- skipSanitization = true
luaunit.assertEquals(editor:getText(), "Hello\x00World")
end
-- === Initial Text Sanitization ===
function TestTextEditorSanitization:test_initial_text_is_sanitized()
local editor = self:_createEditor({
editable = true,
text = "Initial\x00Text\x01"
})
luaunit.assertEquals(editor:getText(), "InitialText")
end
function TestTextEditorSanitization:test_initial_text_preserved_when_disabled()
local editor = self:_createEditor({
editable = true,
sanitize = false,
text = "Initial\x00Text"
})
luaunit.assertEquals(editor:getText(), "Initial\x00Text")
end
-- === insertText Sanitization ===
function TestTextEditorSanitization:test_insertText_sanitizes_input()
local editor = self:_createEditor({editable = true, text = "Hello"})
editor:insertText("\x00World", 5)
luaunit.assertEquals(editor:getText(), "HelloWorld")
end
function TestTextEditorSanitization:test_insertText_with_valid_text()
local editor = self:_createEditor({editable = true, text = "Hello"})
editor:insertText(" World", 5)
luaunit.assertEquals(editor:getText(), "Hello World")
end
function TestTextEditorSanitization:test_insertText_empty_after_sanitization()
local editor = self:_createEditor({editable = true, text = "Hello"})
editor:insertText("\x00\x01\x02", 5) -- Only control chars
luaunit.assertEquals(editor:getText(), "Hello") -- Should remain unchanged
end
-- === Length Limiting ===
function TestTextEditorSanitization:test_maxLength_enforced_on_setText()
local editor = self:_createEditor({editable = true, maxLength = 10})
editor:setText("This is a very long text")
luaunit.assertEquals(#editor:getText(), 10)
end
function TestTextEditorSanitization:test_maxLength_enforced_on_insertText()
local editor = self:_createEditor({editable = true, text = "12345", maxLength = 10})
editor:insertText("67890", 5) -- This would make it exactly 10
luaunit.assertEquals(editor:getText(), "1234567890")
end
function TestTextEditorSanitization:test_maxLength_truncates_excess()
local editor = self:_createEditor({editable = true, text = "12345", maxLength = 10})
editor:insertText("67890EXTRA", 5) -- Would exceed limit
luaunit.assertEquals(editor:getText(), "1234567890")
end
function TestTextEditorSanitization:test_maxLength_prevents_insert_when_full()
local editor = self:_createEditor({editable = true, text = "1234567890", maxLength = 10})
editor:insertText("X", 10)
luaunit.assertEquals(editor:getText(), "1234567890") -- Should not change
end
-- === Newline Handling ===
function TestTextEditorSanitization:test_newlines_allowed_in_multiline()
local editor = self:_createEditor({editable = true, multiline = true})
editor:setText("Line1\nLine2")
luaunit.assertEquals(editor:getText(), "Line1\nLine2")
end
function TestTextEditorSanitization:test_newlines_removed_in_singleline()
local editor = self:_createEditor({editable = true, multiline = false})
editor:setText("Line1\nLine2")
luaunit.assertEquals(editor:getText(), "Line1Line2")
end
function TestTextEditorSanitization:test_allowNewlines_explicit_false()
local editor = self:_createEditor({
editable = true,
multiline = true,
allowNewlines = false
})
editor:setText("Line1\nLine2")
luaunit.assertEquals(editor:getText(), "Line1Line2")
end
-- === Tab Handling ===
function TestTextEditorSanitization:test_tabs_allowed_by_default()
local editor = self:_createEditor({editable = true})
editor:setText("Hello\tWorld")
luaunit.assertEquals(editor:getText(), "Hello\tWorld")
end
function TestTextEditorSanitization:test_tabs_removed_when_disabled()
local editor = self:_createEditor({
editable = true,
allowTabs = false
})
editor:setText("Hello\tWorld")
luaunit.assertEquals(editor:getText(), "HelloWorld")
end
-- === Custom Sanitizer ===
function TestTextEditorSanitization:test_custom_sanitizer_used()
local customSanitizer = function(text)
return text:upper()
end
local editor = self:_createEditor({
editable = true,
customSanitizer = customSanitizer
})
editor:setText("hello world")
luaunit.assertEquals(editor:getText(), "HELLO WORLD")
end
function TestTextEditorSanitization:test_custom_sanitizer_with_control_chars()
local customSanitizer = function(text)
-- Custom sanitizer that replaces control chars with *
return text:gsub("[\x00-\x1F]", "*")
end
local editor = self:_createEditor({
editable = true,
customSanitizer = customSanitizer
})
editor:setText("Hello\x00World\x01")
luaunit.assertEquals(editor:getText(), "Hello*World*")
end
-- === Unicode and Special Characters ===
function TestTextEditorSanitization:test_unicode_preserved()
local editor = self:_createEditor({editable = true})
editor:setText("Hello 世界 🌍")
luaunit.assertEquals(editor:getText(), "Hello 世界 🌍")
end
function TestTextEditorSanitization:test_emoji_preserved()
local editor = self:_createEditor({editable = true})
editor:setText("😀😃😄😁")
luaunit.assertEquals(editor:getText(), "😀😃😄😁")
end
function TestTextEditorSanitization:test_special_chars_preserved()
local editor = self:_createEditor({editable = true})
editor:setText("!@#$%^&*()_+-=[]{}|;':\",./<>?")
luaunit.assertEquals(editor:getText(), "!@#$%^&*()_+-=[]{}|;':\",./<>?")
end
-- === Edge Cases ===
function TestTextEditorSanitization:test_empty_string()
local editor = self:_createEditor({editable = true})
editor:setText("")
luaunit.assertEquals(editor:getText(), "")
end
function TestTextEditorSanitization:test_only_control_characters()
local editor = self:_createEditor({editable = true})
editor:setText("\x00\x01\x02\x03")
luaunit.assertEquals(editor:getText(), "")
end
function TestTextEditorSanitization:test_nil_text()
local editor = self:_createEditor({editable = true})
editor:setText(nil)
luaunit.assertEquals(editor:getText(), "")
end
function TestTextEditorSanitization:test_very_long_text_with_control_chars()
local editor = self:_createEditor({editable = true})
local longText = string.rep("Hello\x00World", 100)
editor:setText(longText)
luaunit.assertStrContains(editor:getText(), "Hello")
luaunit.assertStrContains(editor:getText(), "World")
luaunit.assertNotStrContains(editor:getText(), "\x00")
end
function TestTextEditorSanitization:test_mixed_valid_and_invalid()
local editor = self:_createEditor({editable = true})
editor:setText("Valid\x00Text\x01With\x02Control\x03Chars")
luaunit.assertEquals(editor:getText(), "ValidTextWithControlChars")
end
-- === Whitespace Handling ===
function TestTextEditorSanitization:test_spaces_preserved()
local editor = self:_createEditor({editable = true})
editor:setText("Hello World")
luaunit.assertEquals(editor:getText(), "Hello World")
end
function TestTextEditorSanitization:test_leading_trailing_spaces_preserved()
local editor = self:_createEditor({editable = true})
editor:setText(" Hello World ")
luaunit.assertEquals(editor:getText(), " Hello World ")
end
-- === Integration Tests ===
function TestTextEditorSanitization:test_cursor_position_after_sanitization()
local editor = self:_createEditor({editable = true})
editor:setText("Hello")
editor:insertText("\x00World", 5)
-- Cursor should be at end of "HelloWorld" = position 10
luaunit.assertEquals(editor._cursorPosition, 10)
end
function TestTextEditorSanitization:test_multiple_operations()
local editor = self:_createEditor({editable = true})
editor:setText("Hello")
editor:insertText(" ", 5)
editor:insertText("World\x00", 6)
luaunit.assertEquals(editor:getText(), "Hello World")
end
-- Run tests if this file is executed directly
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -0,0 +1,598 @@
-- Import test framework
package.path = package.path .. ";./?.lua;./game/?.lua"
local luaunit = require("testing.luaunit")
-- Set up LÖVE stub environment
require("testing.loveStub")
-- Import the Theme module
local Theme = require("modules.Theme")
local Color = require("modules.Color")
-- Test Suite for Theme Validation
TestThemeValidation = {}
-- === Basic Theme Validation ===
function TestThemeValidation:test_validate_nil_theme()
local valid, errors = Theme.validateTheme(nil)
luaunit.assertFalse(valid)
luaunit.assertEquals(#errors, 1)
luaunit.assertStrContains(errors[1], "nil")
end
function TestThemeValidation:test_validate_non_table_theme()
local valid, errors = Theme.validateTheme("not a table")
luaunit.assertFalse(valid)
luaunit.assertEquals(#errors, 1)
luaunit.assertStrContains(errors[1], "must be a table")
end
function TestThemeValidation:test_validate_empty_theme()
local valid, errors = Theme.validateTheme({})
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
-- Should have error about missing name
end
function TestThemeValidation:test_validate_minimal_valid_theme()
local theme = {
name = "Test Theme"
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_theme_with_empty_name()
local theme = {
name = ""
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
function TestThemeValidation:test_validate_theme_with_non_string_name()
local theme = {
name = 123
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
-- === Color Validation ===
function TestThemeValidation:test_validate_valid_colors()
local theme = {
name = "Test Theme",
colors = {
primary = Color.new(1, 0, 0, 1),
secondary = Color.new(0, 1, 0, 1)
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_colors_with_hex()
local theme = {
name = "Test Theme",
colors = {
primary = "#FF0000"
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_colors_with_named()
local theme = {
name = "Test Theme",
colors = {
primary = "red",
secondary = "blue"
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_invalid_color()
local theme = {
name = "Test Theme",
colors = {
primary = "not-a-color"
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
luaunit.assertStrContains(errors[1], "primary")
end
function TestThemeValidation:test_validate_colors_non_table()
local theme = {
name = "Test Theme",
colors = "should be a table"
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
function TestThemeValidation:test_validate_color_with_non_string_name()
local theme = {
name = "Test Theme",
colors = {
[123] = Color.new(1, 0, 0, 1)
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
-- === Font Validation ===
function TestThemeValidation:test_validate_valid_fonts()
local theme = {
name = "Test Theme",
fonts = {
default = "path/to/font.ttf",
heading = "path/to/heading.ttf"
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_fonts_non_table()
local theme = {
name = "Test Theme",
fonts = "should be a table"
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
function TestThemeValidation:test_validate_font_with_non_string_path()
local theme = {
name = "Test Theme",
fonts = {
default = 123
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
function TestThemeValidation:test_validate_font_with_non_string_name()
local theme = {
name = "Test Theme",
fonts = {
[123] = "path/to/font.ttf"
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
-- === Component Validation ===
function TestThemeValidation:test_validate_valid_component()
local theme = {
name = "Test Theme",
components = {
button = {
atlas = "path/to/button.png"
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_component_with_insets()
local theme = {
name = "Test Theme",
components = {
button = {
atlas = "path/to/button.png",
insets = {left = 5, top = 5, right = 5, bottom = 5}
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_component_with_missing_inset()
local theme = {
name = "Test Theme",
components = {
button = {
atlas = "path/to/button.png",
insets = {left = 5, top = 5, right = 5} -- missing bottom
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
luaunit.assertStrContains(errors[1], "bottom")
end
function TestThemeValidation:test_validate_component_with_negative_inset()
local theme = {
name = "Test Theme",
components = {
button = {
atlas = "path/to/button.png",
insets = {left = -5, top = 5, right = 5, bottom = 5}
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
luaunit.assertStrContains(errors[1], "non-negative")
end
function TestThemeValidation:test_validate_component_with_states()
local theme = {
name = "Test Theme",
components = {
button = {
atlas = "path/to/button.png",
states = {
hover = {
atlas = "path/to/button_hover.png"
},
pressed = {
atlas = "path/to/button_pressed.png"
}
}
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_component_with_invalid_state()
local theme = {
name = "Test Theme",
components = {
button = {
atlas = "path/to/button.png",
states = {
hover = "should be a table"
}
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
function TestThemeValidation:test_validate_component_with_scaleCorners()
local theme = {
name = "Test Theme",
components = {
button = {
atlas = "path/to/button.png",
scaleCorners = 2.0
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_component_with_invalid_scaleCorners()
local theme = {
name = "Test Theme",
components = {
button = {
atlas = "path/to/button.png",
scaleCorners = -1
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
luaunit.assertStrContains(errors[1], "positive")
end
function TestThemeValidation:test_validate_component_with_valid_scalingAlgorithm()
local theme = {
name = "Test Theme",
components = {
button = {
atlas = "path/to/button.png",
scalingAlgorithm = "nearest"
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_component_with_invalid_scalingAlgorithm()
local theme = {
name = "Test Theme",
components = {
button = {
atlas = "path/to/button.png",
scalingAlgorithm = "invalid"
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
function TestThemeValidation:test_validate_components_non_table()
local theme = {
name = "Test Theme",
components = "should be a table"
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
-- === ContentAutoSizingMultiplier Validation ===
function TestThemeValidation:test_validate_valid_multiplier()
local theme = {
name = "Test Theme",
contentAutoSizingMultiplier = {width = 1.1, height = 1.2}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_multiplier_with_only_width()
local theme = {
name = "Test Theme",
contentAutoSizingMultiplier = {width = 1.1}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_multiplier_non_table()
local theme = {
name = "Test Theme",
contentAutoSizingMultiplier = 1.5
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
function TestThemeValidation:test_validate_multiplier_with_non_number()
local theme = {
name = "Test Theme",
contentAutoSizingMultiplier = {width = "not a number"}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
function TestThemeValidation:test_validate_multiplier_with_negative()
local theme = {
name = "Test Theme",
contentAutoSizingMultiplier = {width = -1}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
luaunit.assertStrContains(errors[1], "positive")
end
function TestThemeValidation:test_validate_multiplier_with_zero()
local theme = {
name = "Test Theme",
contentAutoSizingMultiplier = {width = 0}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
end
-- === Global Atlas Validation ===
function TestThemeValidation:test_validate_valid_global_atlas()
local theme = {
name = "Test Theme",
atlas = "path/to/atlas.png"
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
-- === Strict Mode Validation ===
function TestThemeValidation:test_validate_unknown_field_strict()
local theme = {
name = "Test Theme",
unknownField = "should trigger warning"
}
local valid, errors = Theme.validateTheme(theme, {strict = true})
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors > 0)
luaunit.assertStrContains(errors[1], "Unknown field")
end
function TestThemeValidation:test_validate_unknown_field_non_strict()
local theme = {
name = "Test Theme",
unknownField = "should be ignored"
}
local valid, errors = Theme.validateTheme(theme, {strict = false})
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
-- === Theme Sanitization ===
function TestThemeValidation:test_sanitize_nil_theme()
local sanitized = Theme.sanitizeTheme(nil)
luaunit.assertNotNil(sanitized)
luaunit.assertEquals(sanitized.name, "Invalid Theme")
end
function TestThemeValidation:test_sanitize_theme_without_name()
local theme = {
colors = {primary = "red"}
}
local sanitized = Theme.sanitizeTheme(theme)
luaunit.assertEquals(sanitized.name, "Unnamed Theme")
end
function TestThemeValidation:test_sanitize_theme_with_non_string_name()
local theme = {
name = 123
}
local sanitized = Theme.sanitizeTheme(theme)
luaunit.assertEquals(type(sanitized.name), "string")
end
function TestThemeValidation:test_sanitize_colors()
local theme = {
name = "Test",
colors = {
valid = "red",
invalid = "not-a-color"
}
}
local sanitized = Theme.sanitizeTheme(theme)
luaunit.assertNotNil(sanitized.colors.valid)
luaunit.assertNotNil(sanitized.colors.invalid) -- Should have fallback
end
function TestThemeValidation:test_sanitize_removes_non_string_color_names()
local theme = {
name = "Test",
colors = {
[123] = "red"
}
}
local sanitized = Theme.sanitizeTheme(theme)
luaunit.assertNil(sanitized.colors[123])
end
function TestThemeValidation:test_sanitize_fonts()
local theme = {
name = "Test",
fonts = {
default = "path/to/font.ttf",
invalid = 123
}
}
local sanitized = Theme.sanitizeTheme(theme)
luaunit.assertNotNil(sanitized.fonts.default)
luaunit.assertNil(sanitized.fonts.invalid)
end
function TestThemeValidation:test_sanitize_preserves_components()
local theme = {
name = "Test",
components = {
button = {atlas = "path/to/button.png"}
}
}
local sanitized = Theme.sanitizeTheme(theme)
luaunit.assertNotNil(sanitized.components.button)
luaunit.assertEquals(sanitized.components.button.atlas, "path/to/button.png")
end
-- === Complex Theme Validation ===
function TestThemeValidation:test_validate_complete_theme()
local theme = {
name = "Complete Theme",
atlas = "path/to/atlas.png",
contentAutoSizingMultiplier = {width = 1.05, height = 1.1},
colors = {
primary = Color.new(1, 0, 0, 1),
secondary = "#00FF00",
tertiary = "blue"
},
fonts = {
default = "path/to/font.ttf",
heading = "path/to/heading.ttf"
},
components = {
button = {
atlas = "path/to/button.png",
insets = {left = 5, top = 5, right = 5, bottom = 5},
scaleCorners = 2,
scalingAlgorithm = "nearest",
states = {
hover = {
atlas = "path/to/button_hover.png"
},
pressed = {
atlas = "path/to/button_pressed.png"
}
}
},
panel = {
atlas = "path/to/panel.png"
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertTrue(valid)
luaunit.assertEquals(#errors, 0)
end
function TestThemeValidation:test_validate_theme_with_multiple_errors()
local theme = {
name = "",
colors = {
invalid1 = "not-a-color",
invalid2 = 123
},
fonts = {
bad = 456
},
components = {
button = {
insets = {left = -5} -- missing fields and negative
}
}
}
local valid, errors = Theme.validateTheme(theme)
luaunit.assertFalse(valid)
luaunit.assertTrue(#errors >= 5) -- Should have multiple errors
end
-- Run tests if this file is executed directly
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -0,0 +1,399 @@
-- Test suite for Units.lua module
-- Tests unit parsing, resolution, and conversion functions
package.path = package.path .. ";./?.lua;./modules/?.lua"
-- Load love stub before anything else
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local Units = require("modules.Units")
-- Mock viewport dimensions for consistent tests
local MOCK_VIEWPORT_WIDTH = 1920
local MOCK_VIEWPORT_HEIGHT = 1080
-- Test suite for Units.parse()
TestUnitsParse = {}
function TestUnitsParse:testParseNumber()
local value, unit = Units.parse(100)
luaunit.assertEquals(value, 100)
luaunit.assertEquals(unit, "px")
end
function TestUnitsParse:testParsePixels()
local value, unit = Units.parse("100px")
luaunit.assertEquals(value, 100)
luaunit.assertEquals(unit, "px")
end
function TestUnitsParse:testParsePixelsNoUnit()
local value, unit = Units.parse("100")
luaunit.assertEquals(value, 100)
luaunit.assertEquals(unit, "px")
end
function TestUnitsParse:testParsePercentage()
local value, unit = Units.parse("50%")
luaunit.assertEquals(value, 50)
luaunit.assertEquals(unit, "%")
end
function TestUnitsParse:testParseViewportWidth()
local value, unit = Units.parse("10vw")
luaunit.assertEquals(value, 10)
luaunit.assertEquals(unit, "vw")
end
function TestUnitsParse:testParseViewportHeight()
local value, unit = Units.parse("20vh")
luaunit.assertEquals(value, 20)
luaunit.assertEquals(unit, "vh")
end
function TestUnitsParse:testParseElementWidth()
local value, unit = Units.parse("15ew")
luaunit.assertEquals(value, 15)
luaunit.assertEquals(unit, "ew")
end
function TestUnitsParse:testParseElementHeight()
local value, unit = Units.parse("25eh")
luaunit.assertEquals(value, 25)
luaunit.assertEquals(unit, "eh")
end
function TestUnitsParse:testParseDecimal()
local value, unit = Units.parse("10.5px")
luaunit.assertEquals(value, 10.5)
luaunit.assertEquals(unit, "px")
end
function TestUnitsParse:testParseNegative()
local value, unit = Units.parse("-50px")
luaunit.assertEquals(value, -50)
luaunit.assertEquals(unit, "px")
end
function TestUnitsParse:testParseNegativeDecimal()
local value, unit = Units.parse("-10.5%")
luaunit.assertEquals(value, -10.5)
luaunit.assertEquals(unit, "%")
end
function TestUnitsParse:testParseZero()
local value, unit = Units.parse("0")
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end
function TestUnitsParse:testParseInvalidType()
local value, unit = Units.parse(nil)
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end
function TestUnitsParse:testParseInvalidString()
local value, unit = Units.parse("abc")
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end
function TestUnitsParse:testParseInvalidUnit()
local value, unit = Units.parse("100xyz")
luaunit.assertEquals(value, 100)
luaunit.assertEquals(unit, "px") -- Falls back to px
end
function TestUnitsParse:testParseWithSpace()
-- Spaces between number and unit should be invalid
local value, unit = Units.parse("100 px")
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end
-- Test suite for Units.resolve()
TestUnitsResolve = {}
function TestUnitsResolve:testResolvePixels()
local result = Units.resolve(100, "px", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
luaunit.assertEquals(result, 100)
end
function TestUnitsResolve:testResolvePercentage()
local result = Units.resolve(50, "%", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT, 200)
luaunit.assertEquals(result, 100) -- 50% of 200
end
function TestUnitsResolve:testResolveViewportWidth()
local result = Units.resolve(10, "vw", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
luaunit.assertEquals(result, 192) -- 10% of 1920
end
function TestUnitsResolve:testResolveViewportHeight()
local result = Units.resolve(20, "vh", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
luaunit.assertEquals(result, 216) -- 20% of 1080
end
function TestUnitsResolve:testResolvePercentageZero()
local result = Units.resolve(0, "%", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT, 200)
luaunit.assertEquals(result, 0)
end
function TestUnitsResolve:testResolvePercentage100()
local result = Units.resolve(100, "%", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT, 200)
luaunit.assertEquals(result, 200)
end
function TestUnitsResolve:testResolveNegativePixels()
local result = Units.resolve(-50, "px", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
luaunit.assertEquals(result, -50)
end
function TestUnitsResolve:testResolveDecimalPercentage()
local result = Units.resolve(33.33, "%", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT, 300)
luaunit.assertAlmostEquals(result, 99.99, 0.01)
end
-- Test suite for Units.parseAndResolve()
TestUnitsParseAndResolve = {}
function TestUnitsParseAndResolve:testParseAndResolvePixels()
local result = Units.parseAndResolve("100px", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
luaunit.assertEquals(result, 100)
end
function TestUnitsParseAndResolve:testParseAndResolveNumber()
local result = Units.parseAndResolve(100, MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
luaunit.assertEquals(result, 100)
end
function TestUnitsParseAndResolve:testParseAndResolvePercentage()
local result = Units.parseAndResolve("50%", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT, 400)
luaunit.assertEquals(result, 200)
end
function TestUnitsParseAndResolve:testParseAndResolveViewportWidth()
local result = Units.parseAndResolve("10vw", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
luaunit.assertEquals(result, 192)
end
function TestUnitsParseAndResolve:testParseAndResolveViewportHeight()
local result = Units.parseAndResolve("50vh", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
luaunit.assertEquals(result, 540)
end
-- Test suite for Units.isValid()
TestUnitsIsValid = {}
function TestUnitsIsValid:testIsValidPixels()
luaunit.assertTrue(Units.isValid("100px"))
end
function TestUnitsIsValid:testIsValidPercentage()
luaunit.assertTrue(Units.isValid("50%"))
end
function TestUnitsIsValid:testIsValidViewportWidth()
luaunit.assertTrue(Units.isValid("10vw"))
end
function TestUnitsIsValid:testIsValidViewportHeight()
luaunit.assertTrue(Units.isValid("20vh"))
end
function TestUnitsIsValid:testIsValidElementWidth()
luaunit.assertTrue(Units.isValid("15ew"))
end
function TestUnitsIsValid:testIsValidElementHeight()
luaunit.assertTrue(Units.isValid("25eh"))
end
function TestUnitsIsValid:testIsValidNumber()
luaunit.assertTrue(Units.isValid("100"))
end
function TestUnitsIsValid:testIsValidNegative()
luaunit.assertTrue(Units.isValid("-50px"))
end
function TestUnitsIsValid:testIsValidDecimal()
luaunit.assertTrue(Units.isValid("10.5px"))
end
function TestUnitsIsValid:testIsInvalidString()
luaunit.assertFalse(Units.isValid("abc"))
end
function TestUnitsIsValid:testIsInvalidNil()
luaunit.assertFalse(Units.isValid(nil))
end
function TestUnitsIsValid:testIsInvalidNumber()
luaunit.assertFalse(Units.isValid(100))
end
-- Test suite for Units.resolveSpacing()
TestUnitsResolveSpacing = {}
function TestUnitsResolveSpacing:testResolveSpacingNil()
local result = Units.resolveSpacing(nil, 800, 600)
luaunit.assertEquals(result.top, 0)
luaunit.assertEquals(result.right, 0)
luaunit.assertEquals(result.bottom, 0)
luaunit.assertEquals(result.left, 0)
end
function TestUnitsResolveSpacing:testResolveSpacingAllSides()
local spacing = {
top = "10px",
right = "20px",
bottom = "30px",
left = "40px"
}
local result = Units.resolveSpacing(spacing, 800, 600)
luaunit.assertEquals(result.top, 10)
luaunit.assertEquals(result.right, 20)
luaunit.assertEquals(result.bottom, 30)
luaunit.assertEquals(result.left, 40)
end
function TestUnitsResolveSpacing:testResolveSpacingVerticalHorizontal()
local spacing = {
vertical = "10px",
horizontal = "20px"
}
local result = Units.resolveSpacing(spacing, 800, 600)
luaunit.assertEquals(result.top, 10)
luaunit.assertEquals(result.right, 20)
luaunit.assertEquals(result.bottom, 10)
luaunit.assertEquals(result.left, 20)
end
function TestUnitsResolveSpacing:testResolveSpacingVerticalHorizontalNumbers()
local spacing = {
vertical = 10,
horizontal = 20
}
local result = Units.resolveSpacing(spacing, 800, 600)
luaunit.assertEquals(result.top, 10)
luaunit.assertEquals(result.right, 20)
luaunit.assertEquals(result.bottom, 10)
luaunit.assertEquals(result.left, 20)
end
function TestUnitsResolveSpacing:testResolveSpacingMixedPercentage()
local spacing = {
top = "10%",
right = "5%",
bottom = "10%",
left = "5%"
}
local result = Units.resolveSpacing(spacing, 800, 600)
luaunit.assertEquals(result.top, 60) -- 10% of 600 (height)
luaunit.assertEquals(result.right, 40) -- 5% of 800 (width)
luaunit.assertEquals(result.bottom, 60) -- 10% of 600 (height)
luaunit.assertEquals(result.left, 40) -- 5% of 800 (width)
end
function TestUnitsResolveSpacing:testResolveSpacingOverride()
-- Individual sides should override vertical/horizontal
local spacing = {
vertical = "10px",
horizontal = "20px",
top = "50px"
}
local result = Units.resolveSpacing(spacing, 800, 600)
luaunit.assertEquals(result.top, 50) -- Overridden
luaunit.assertEquals(result.right, 20)
luaunit.assertEquals(result.bottom, 10)
luaunit.assertEquals(result.left, 20)
end
-- Test suite for Units.applyBaseScale()
TestUnitsApplyBaseScale = {}
function TestUnitsApplyBaseScale:testApplyBaseScaleX()
local scaleFactors = { x = 2, y = 3 }
local result = Units.applyBaseScale(100, "x", scaleFactors)
luaunit.assertEquals(result, 200)
end
function TestUnitsApplyBaseScale:testApplyBaseScaleY()
local scaleFactors = { x = 2, y = 3 }
local result = Units.applyBaseScale(100, "y", scaleFactors)
luaunit.assertEquals(result, 300)
end
function TestUnitsApplyBaseScale:testApplyBaseScaleIdentity()
local scaleFactors = { x = 1, y = 1 }
local result = Units.applyBaseScale(100, "x", scaleFactors)
luaunit.assertEquals(result, 100)
end
function TestUnitsApplyBaseScale:testApplyBaseScaleZero()
local scaleFactors = { x = 0, y = 0 }
local result = Units.applyBaseScale(100, "x", scaleFactors)
luaunit.assertEquals(result, 0)
end
function TestUnitsApplyBaseScale:testApplyBaseScaleDecimal()
local scaleFactors = { x = 0.5, y = 1.5 }
local result = Units.applyBaseScale(100, "x", scaleFactors)
luaunit.assertEquals(result, 50)
end
-- Test suite for Units.getViewport()
TestUnitsGetViewport = {}
function TestUnitsGetViewport:testGetViewportReturnsValues()
local width, height = Units.getViewport()
luaunit.assertIsNumber(width)
luaunit.assertIsNumber(height)
luaunit.assertTrue(width > 0)
luaunit.assertTrue(height > 0)
end
-- Test edge cases
TestUnitsEdgeCases = {}
function TestUnitsEdgeCases:testParseVeryLargeNumber()
local value, unit = Units.parse("999999px")
luaunit.assertEquals(value, 999999)
luaunit.assertEquals(unit, "px")
end
function TestUnitsEdgeCases:testParseVerySmallDecimal()
local value, unit = Units.parse("0.001px")
luaunit.assertEquals(value, 0.001)
luaunit.assertEquals(unit, "px")
end
function TestUnitsEdgeCases:testResolveZeroParentSize()
local result = Units.resolve(50, "%", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT, 0)
luaunit.assertEquals(result, 0)
end
function TestUnitsEdgeCases:testParseEmptyString()
local value, unit = Units.parse("")
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end
function TestUnitsEdgeCases:testParseOnlyUnit()
local value, unit = Units.parse("px")
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end
function TestUnitsEdgeCases:testResolveNegativePercentage()
local result = Units.resolve(-50, "%", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT, 200)
luaunit.assertEquals(result, -100)
end
-- Run tests if not running as part of a suite
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -0,0 +1,487 @@
-- Test suite for utils.lua module
-- Tests all 16+ utility functions with comprehensive edge cases
package.path = package.path .. ";./?.lua;./modules/?.lua"
-- Load love stub before anything else
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local utils = require("modules.utils")
-- Test suite for validation utilities
TestValidationUtils = {}
function TestValidationUtils:testValidateEnum_ValidValue()
local testEnum = { VALUE1 = "value1", VALUE2 = "value2", VALUE3 = "value3" }
luaunit.assertTrue(utils.validateEnum("value1", testEnum, "testProp"))
luaunit.assertTrue(utils.validateEnum("value2", testEnum, "testProp"))
luaunit.assertTrue(utils.validateEnum("value3", testEnum, "testProp"))
end
function TestValidationUtils:testValidateEnum_NilValue()
local testEnum = { VALUE1 = "value1", VALUE2 = "value2" }
luaunit.assertTrue(utils.validateEnum(nil, testEnum, "testProp"))
end
function TestValidationUtils:testValidateEnum_InvalidValue()
local testEnum = { VALUE1 = "value1", VALUE2 = "value2" }
luaunit.assertErrorMsgContains("must be one of", function()
utils.validateEnum("invalid", testEnum, "testProp")
end)
end
function TestValidationUtils:testValidateRange_InRange()
luaunit.assertTrue(utils.validateRange(5, 0, 10, "testProp"))
luaunit.assertTrue(utils.validateRange(0, 0, 10, "testProp"))
luaunit.assertTrue(utils.validateRange(10, 0, 10, "testProp"))
end
function TestValidationUtils:testValidateRange_OutOfRange()
luaunit.assertErrorMsgContains("must be between", function()
utils.validateRange(-1, 0, 10, "testProp")
end)
luaunit.assertErrorMsgContains("must be between", function()
utils.validateRange(11, 0, 10, "testProp")
end)
end
function TestValidationUtils:testValidateRange_NilValue()
luaunit.assertTrue(utils.validateRange(nil, 0, 10, "testProp"))
end
function TestValidationUtils:testValidateRange_WrongType()
luaunit.assertErrorMsgContains("must be a number", function()
utils.validateRange("not a number", 0, 10, "testProp")
end)
end
function TestValidationUtils:testValidateType_CorrectType()
luaunit.assertTrue(utils.validateType("hello", "string", "testProp"))
luaunit.assertTrue(utils.validateType(123, "number", "testProp"))
luaunit.assertTrue(utils.validateType(true, "boolean", "testProp"))
luaunit.assertTrue(utils.validateType({}, "table", "testProp"))
luaunit.assertTrue(utils.validateType(function() end, "function", "testProp"))
end
function TestValidationUtils:testValidateType_WrongType()
luaunit.assertErrorMsgContains("must be string", function()
utils.validateType(123, "string", "testProp")
end)
luaunit.assertErrorMsgContains("must be number", function()
utils.validateType("hello", "number", "testProp")
end)
end
function TestValidationUtils:testValidateType_NilValue()
luaunit.assertTrue(utils.validateType(nil, "string", "testProp"))
end
-- Test suite for math utilities
TestMathUtils = {}
function TestMathUtils:testClamp_WithinRange()
luaunit.assertEquals(utils.clamp(5, 0, 10), 5)
luaunit.assertEquals(utils.clamp(0, 0, 10), 0)
luaunit.assertEquals(utils.clamp(10, 0, 10), 10)
end
function TestMathUtils:testClamp_BelowMin()
luaunit.assertEquals(utils.clamp(-5, 0, 10), 0)
luaunit.assertEquals(utils.clamp(-100, 0, 10), 0)
end
function TestMathUtils:testClamp_AboveMax()
luaunit.assertEquals(utils.clamp(15, 0, 10), 10)
luaunit.assertEquals(utils.clamp(100, 0, 10), 10)
end
function TestMathUtils:testClamp_NegativeRange()
luaunit.assertEquals(utils.clamp(-5, -10, -1), -5)
luaunit.assertEquals(utils.clamp(-15, -10, -1), -10)
luaunit.assertEquals(utils.clamp(0, -10, -1), -1)
end
function TestMathUtils:testLerp_Boundaries()
luaunit.assertEquals(utils.lerp(0, 10, 0), 0)
luaunit.assertEquals(utils.lerp(0, 10, 1), 10)
end
function TestMathUtils:testLerp_Midpoint()
luaunit.assertEquals(utils.lerp(0, 10, 0.5), 5)
luaunit.assertEquals(utils.lerp(10, 20, 0.5), 15)
end
function TestMathUtils:testLerp_NegativeValues()
luaunit.assertEquals(utils.lerp(-10, 10, 0.5), 0)
luaunit.assertEquals(utils.lerp(-20, -10, 0.5), -15)
end
function TestMathUtils:testLerp_BeyondRange()
luaunit.assertEquals(utils.lerp(0, 10, 1.5), 15)
luaunit.assertEquals(utils.lerp(0, 10, -0.5), -5)
end
function TestMathUtils:testRound_UpAndDown()
luaunit.assertEquals(utils.round(0.4), 0)
luaunit.assertEquals(utils.round(0.5), 1)
luaunit.assertEquals(utils.round(0.6), 1)
end
function TestMathUtils:testRound_NegativeNumbers()
luaunit.assertEquals(utils.round(-0.4), 0)
luaunit.assertEquals(utils.round(-0.5), 0)
luaunit.assertEquals(utils.round(-0.6), -1)
end
function TestMathUtils:testRound_WholeNumbers()
luaunit.assertEquals(utils.round(5), 5)
luaunit.assertEquals(utils.round(-5), -5)
luaunit.assertEquals(utils.round(0), 0)
end
-- Test suite for path utilities
TestPathUtils = {}
function TestPathUtils:testNormalizePath_Whitespace()
luaunit.assertEquals(utils.normalizePath(" /path/to/file "), "/path/to/file")
luaunit.assertEquals(utils.normalizePath("\t/path/to/file\t"), "/path/to/file")
luaunit.assertEquals(utils.normalizePath(" /path/to/file "), "/path/to/file")
end
function TestPathUtils:testNormalizePath_Backslashes()
luaunit.assertEquals(utils.normalizePath("C:\\path\\to\\file"), "C:/path/to/file")
luaunit.assertEquals(utils.normalizePath("path\\to\\file"), "path/to/file")
end
function TestPathUtils:testNormalizePath_DuplicateSlashes()
luaunit.assertEquals(utils.normalizePath("/path//to///file"), "/path/to/file")
luaunit.assertEquals(utils.normalizePath("path//to//file"), "path/to/file")
end
function TestPathUtils:testNormalizePath_Combined()
luaunit.assertEquals(utils.normalizePath(" C:\\path\\\\to///file "), "C:/path/to/file")
end
function TestPathUtils:testResolveImagePath_AbsolutePath()
local result = utils.resolveImagePath("/absolute/path/to/image.png")
luaunit.assertEquals(result, "/absolute/path/to/image.png")
end
function TestPathUtils:testResolveImagePath_WindowsAbsolutePath()
local result = utils.resolveImagePath("C:/path/to/image.png")
luaunit.assertEquals(result, "C:/path/to/image.png")
end
function TestPathUtils:testResolveImagePath_RelativePath()
local result = utils.resolveImagePath("themes/images/icon.png")
-- Should prepend the FlexLove base path
luaunit.assertStrContains(result, "themes/images/icon.png")
end
function TestPathUtils:testSafeLoadImage_InvalidPath()
-- Test with an invalid path - should return nil and error message
local image, imageData, errorMsg = utils.safeLoadImage("nonexistent/path/to/image.png")
luaunit.assertNil(image)
luaunit.assertNil(imageData)
luaunit.assertNotNil(errorMsg)
luaunit.assertStrContains(errorMsg, "Failed to load")
end
-- Test suite for color utilities
TestColorUtils = {}
function TestColorUtils:testBrightenColor_NormalFactor()
local r, g, b, a = utils.brightenColor(0.5, 0.5, 0.5, 1.0, 1.5)
luaunit.assertAlmostEquals(r, 0.75, 0.001)
luaunit.assertAlmostEquals(g, 0.75, 0.001)
luaunit.assertAlmostEquals(b, 0.75, 0.001)
luaunit.assertAlmostEquals(a, 1.0, 0.001)
end
function TestColorUtils:testBrightenColor_ClampingAt1()
local r, g, b, a = utils.brightenColor(0.8, 0.8, 0.8, 1.0, 2.0)
luaunit.assertAlmostEquals(r, 1.0, 0.001)
luaunit.assertAlmostEquals(g, 1.0, 0.001)
luaunit.assertAlmostEquals(b, 1.0, 0.001)
luaunit.assertAlmostEquals(a, 1.0, 0.001)
end
function TestColorUtils:testBrightenColor_FactorOne()
local r, g, b, a = utils.brightenColor(0.5, 0.6, 0.7, 0.8, 1.0)
luaunit.assertAlmostEquals(r, 0.5, 0.001)
luaunit.assertAlmostEquals(g, 0.6, 0.001)
luaunit.assertAlmostEquals(b, 0.7, 0.001)
luaunit.assertAlmostEquals(a, 0.8, 0.001)
end
function TestColorUtils:testBrightenColor_AlphaUnchanged()
local _, _, _, a = utils.brightenColor(0.5, 0.5, 0.5, 0.5, 2.0)
luaunit.assertAlmostEquals(a, 0.5, 0.001) -- Alpha should remain unchanged
end
-- Test suite for property utilities
TestPropertyUtils = {}
function TestPropertyUtils:testNormalizeBooleanTable_Boolean()
local result = utils.normalizeBooleanTable(true)
luaunit.assertEquals(result.vertical, true)
luaunit.assertEquals(result.horizontal, true)
result = utils.normalizeBooleanTable(false)
luaunit.assertEquals(result.vertical, false)
luaunit.assertEquals(result.horizontal, false)
end
function TestPropertyUtils:testNormalizeBooleanTable_Nil()
local result = utils.normalizeBooleanTable(nil)
luaunit.assertEquals(result.vertical, false)
luaunit.assertEquals(result.horizontal, false)
end
function TestPropertyUtils:testNormalizeBooleanTable_NilWithDefault()
local result = utils.normalizeBooleanTable(nil, true)
luaunit.assertEquals(result.vertical, true)
luaunit.assertEquals(result.horizontal, true)
end
function TestPropertyUtils:testNormalizeBooleanTable_Table()
local result = utils.normalizeBooleanTable({ vertical = true, horizontal = false })
luaunit.assertEquals(result.vertical, true)
luaunit.assertEquals(result.horizontal, false)
end
function TestPropertyUtils:testNormalizeBooleanTable_PartialTable()
local result = utils.normalizeBooleanTable({ vertical = true })
luaunit.assertEquals(result.vertical, true)
luaunit.assertEquals(result.horizontal, false)
result = utils.normalizeBooleanTable({ horizontal = true })
luaunit.assertEquals(result.vertical, false)
luaunit.assertEquals(result.horizontal, true)
end
function TestPropertyUtils:testNormalizeBooleanTable_EmptyTable()
local result = utils.normalizeBooleanTable({})
luaunit.assertEquals(result.vertical, false)
luaunit.assertEquals(result.horizontal, false)
end
-- Test suite for font utilities
TestFontUtils = {}
function TestFontUtils:testResolveFontPath_DirectPath()
local result = utils.resolveFontPath("path/to/font.ttf", nil, nil)
luaunit.assertEquals(result, "path/to/font.ttf")
end
function TestFontUtils:testResolveFontPath_NilFontFamily()
local result = utils.resolveFontPath(nil, nil, nil)
luaunit.assertNil(result)
end
function TestFontUtils:testResolveFontPath_ThemeFont()
-- Mock theme manager with font
local mockThemeManager = {
getTheme = function()
return {
fonts = {
mainFont = "themes/fonts/main.ttf"
}
}
end
}
local result = utils.resolveFontPath("mainFont", "button", mockThemeManager)
luaunit.assertEquals(result, "themes/fonts/main.ttf")
end
function TestFontUtils:testResolveFontPath_ThemeFontNotFound()
-- Mock theme manager without the requested font
local mockThemeManager = {
getTheme = function()
return {
fonts = {}
}
end
}
-- Should fall back to treating it as a direct path
local result = utils.resolveFontPath("unknownFont", "button", mockThemeManager)
luaunit.assertEquals(result, "unknownFont")
end
function TestFontUtils:testGetFont_WithTextSize()
local font = utils.getFont(16, nil, nil, nil)
luaunit.assertNotNil(font)
-- Font height should match the requested size
if font.getHeight then
luaunit.assertEquals(font.getHeight(), 16)
end
end
function TestFontUtils:testGetFont_WithoutTextSize()
local font = utils.getFont(nil, nil, nil, nil)
luaunit.assertNotNil(font)
-- Should return the default font
end
function TestFontUtils:testApplyContentMultiplier_WithWidth()
local result = utils.applyContentMultiplier(100, { width = 2.0, height = 1.5 }, "width")
luaunit.assertEquals(result, 200)
end
function TestFontUtils:testApplyContentMultiplier_WithHeight()
local result = utils.applyContentMultiplier(100, { width = 2.0, height = 1.5 }, "height")
luaunit.assertEquals(result, 150)
end
function TestFontUtils:testApplyContentMultiplier_NilMultiplier()
local result = utils.applyContentMultiplier(100, nil, "width")
luaunit.assertEquals(result, 100)
end
function TestFontUtils:testApplyContentMultiplier_MissingAxis()
local result = utils.applyContentMultiplier(100, { width = 2.0 }, "height")
luaunit.assertEquals(result, 100)
end
-- Test suite for input utilities
TestInputUtils = {}
function TestInputUtils:testGetModifiers_NoModifiers()
-- Reset all modifier keys
love.keyboard.setDown("lshift", false)
love.keyboard.setDown("rshift", false)
love.keyboard.setDown("lctrl", false)
love.keyboard.setDown("rctrl", false)
love.keyboard.setDown("lalt", false)
love.keyboard.setDown("ralt", false)
love.keyboard.setDown("lgui", false)
love.keyboard.setDown("rgui", false)
local mods = utils.getModifiers()
luaunit.assertFalse(mods.shift)
luaunit.assertFalse(mods.ctrl)
luaunit.assertFalse(mods.alt)
luaunit.assertFalse(mods.super)
end
function TestInputUtils:testGetModifiers_ShiftKey()
love.keyboard.setDown("lshift", true)
local mods = utils.getModifiers()
luaunit.assertTrue(mods.shift)
love.keyboard.setDown("lshift", false)
love.keyboard.setDown("rshift", true)
mods = utils.getModifiers()
luaunit.assertTrue(mods.shift)
love.keyboard.setDown("rshift", false)
end
function TestInputUtils:testGetModifiers_CtrlKey()
love.keyboard.setDown("lctrl", true)
local mods = utils.getModifiers()
luaunit.assertTrue(mods.ctrl)
love.keyboard.setDown("lctrl", false)
end
function TestInputUtils:testGetModifiers_AltKey()
love.keyboard.setDown("lalt", true)
local mods = utils.getModifiers()
luaunit.assertTrue(mods.alt)
love.keyboard.setDown("lalt", false)
end
function TestInputUtils:testGetModifiers_SuperKey()
love.keyboard.setDown("lgui", true)
local mods = utils.getModifiers()
luaunit.assertTrue(mods.super)
love.keyboard.setDown("lgui", false)
end
function TestInputUtils:testGetModifiers_MultipleModifiers()
love.keyboard.setDown("lshift", true)
love.keyboard.setDown("lctrl", true)
local mods = utils.getModifiers()
luaunit.assertTrue(mods.shift)
luaunit.assertTrue(mods.ctrl)
luaunit.assertFalse(mods.alt)
luaunit.assertFalse(mods.super)
-- Clean up
love.keyboard.setDown("lshift", false)
love.keyboard.setDown("lctrl", false)
end
-- Test suite for text size presets
TestTextSizePresets = {}
function TestTextSizePresets:testResolveTextSizePreset_ValidPresets()
local value, unit = utils.resolveTextSizePreset("xs")
luaunit.assertEquals(value, 1.25)
luaunit.assertEquals(unit, "vh")
value, unit = utils.resolveTextSizePreset("md")
luaunit.assertEquals(value, 2.25)
luaunit.assertEquals(unit, "vh")
value, unit = utils.resolveTextSizePreset("xl")
luaunit.assertEquals(value, 3.5)
luaunit.assertEquals(unit, "vh")
end
function TestTextSizePresets:testResolveTextSizePreset_NumericValue()
local value, unit = utils.resolveTextSizePreset(20)
luaunit.assertNil(value)
luaunit.assertNil(unit)
end
function TestTextSizePresets:testResolveTextSizePreset_InvalidPreset()
local value, unit = utils.resolveTextSizePreset("invalid")
luaunit.assertNil(value)
luaunit.assertNil(unit)
end
function TestTextSizePresets:testResolveTextSizePreset_AllPresets()
-- Test all available presets
local presets = { "xxs", "2xs", "xs", "sm", "md", "lg", "xl", "xxl", "2xl", "3xl", "4xl" }
for _, preset in ipairs(presets) do
local value, unit = utils.resolveTextSizePreset(preset)
luaunit.assertNotNil(value, "Preset " .. preset .. " should return a value")
luaunit.assertEquals(unit, "vh", "Preset " .. preset .. " should return 'vh' unit")
end
end
-- Test suite for enums
TestEnums = {}
function TestEnums:testEnums_Exist()
luaunit.assertNotNil(utils.enums)
luaunit.assertNotNil(utils.enums.TextAlign)
luaunit.assertNotNil(utils.enums.Positioning)
luaunit.assertNotNil(utils.enums.FlexDirection)
luaunit.assertNotNil(utils.enums.JustifyContent)
luaunit.assertNotNil(utils.enums.AlignItems)
luaunit.assertNotNil(utils.enums.FlexWrap)
luaunit.assertNotNil(utils.enums.TextSize)
end
function TestEnums:testEnums_TextAlign()
luaunit.assertEquals(utils.enums.TextAlign.START, "start")
luaunit.assertEquals(utils.enums.TextAlign.CENTER, "center")
luaunit.assertEquals(utils.enums.TextAlign.END, "end")
luaunit.assertEquals(utils.enums.TextAlign.JUSTIFY, "justify")
end
function TestEnums:testEnums_Positioning()
luaunit.assertEquals(utils.enums.Positioning.ABSOLUTE, "absolute")
luaunit.assertEquals(utils.enums.Positioning.RELATIVE, "relative")
luaunit.assertEquals(utils.enums.Positioning.FLEX, "flex")
luaunit.assertEquals(utils.enums.Positioning.GRID, "grid")
end
-- Run tests if this file is executed directly
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end