Files
FlexLove/testing/__tests__/flex_grow_shrink_test.lua
2026-01-05 12:22:06 -05:00

1008 lines
23 KiB
Lua

package.path = package.path .. ";./?.lua;./modules/?.lua"
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function()
return require("modules." .. moduleName)
end
end
end)
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local FlexLove = require("FlexLove")
FlexLove.init()
--[[
================================================================================
KNOWN BUGS IN COMMIT 157b932 - Tests will fail until these are fixed
================================================================================
BUG #1: FLEX BASIS AUTO USES MODIFIED WIDTH
Location: LayoutEngine.lua:247-253
Problem: When flexBasis="auto", the implementation uses getBorderBoxWidth() which
returns the CURRENT width (already modified by previous layouts), not the
ORIGINAL width specified in props.
Impact: Incorrect flex calculations when layout runs multiple times
Example: child with width=100, flexGrow=1 added alone grows to 600px.
When second child added, first child's flexBasis uses 600px not 100px.
Fix: Use element.units.width to recalculate original size from stored value/unit
instead of getBorderBoxWidth()
Tests affected: Most TestFlexGrow tests will fail due to this bug
BUG #2: SHRINKING NOT TRIGGERED WITHOUT FLEXGROW
Location: LayoutEngine.lua:524-530
Problem: needsFlexSizing only checks if (flexGrow > 0 OR flexBasis != "auto")
This means items with default flexShrink=1 won't shrink in overflow
situations unless they also have flexGrow > 0 or explicit flexBasis.
Impact: Items don't shrink by default like CSS flexbox behavior
CSS default: flex-shrink: 1 (items shrink to prevent overflow)
Fix: Also check if shrinking might be needed (items overflow container)
Or always run flex sizing algorithm for flex containers
Tests affected: All TestFlexShrink tests will fail due to this bug
================================================================================
]]
-- ============================================================================
-- Helper Functions
-- ============================================================================
local function roundToDecimal(num, decimals)
local mult = 10 ^ (decimals or 2)
return math.floor(num * mult + 0.5) / mult
end
-- ============================================================================
-- Test Suite 1: Flex Shorthand Parsing
-- ============================================================================
TestFlexShorthand = {}
function TestFlexShorthand:setUp()
FlexLove.beginFrame()
end
function TestFlexShorthand:tearDown()
FlexLove.endFrame()
end
function TestFlexShorthand:test_flex_number_shorthand()
-- flex: 1 should set flexGrow=1, flexShrink=1, flexBasis=0
local element = FlexLove.new({
width = 100,
height = 100,
flex = 1,
})
luaunit.assertEquals(element.flexGrow, 1)
luaunit.assertEquals(element.flexShrink, 1)
luaunit.assertEquals(element.flexBasis, 0)
end
function TestFlexShorthand:test_flex_auto_shorthand()
-- flex: "auto" should set flexGrow=1, flexShrink=1, flexBasis="auto"
local element = FlexLove.new({
width = 100,
height = 100,
flex = "auto",
})
luaunit.assertEquals(element.flexGrow, 1)
luaunit.assertEquals(element.flexShrink, 1)
luaunit.assertEquals(element.flexBasis, "auto")
end
function TestFlexShorthand:test_flex_none_shorthand()
-- flex: "none" should set flexGrow=0, flexShrink=0, flexBasis="auto"
local element = FlexLove.new({
width = 100,
height = 100,
flex = "none",
})
luaunit.assertEquals(element.flexGrow, 0)
luaunit.assertEquals(element.flexShrink, 0)
luaunit.assertEquals(element.flexBasis, "auto")
end
function TestFlexShorthand:test_flex_two_values_numbers()
-- flex: "2 0" should set flexGrow=2, flexShrink=0, flexBasis=0
local element = FlexLove.new({
width = 100,
height = 100,
flex = "2 0",
})
luaunit.assertEquals(element.flexGrow, 2)
luaunit.assertEquals(element.flexShrink, 0)
luaunit.assertEquals(element.flexBasis, 0)
end
function TestFlexShorthand:test_flex_grow_with_basis()
-- flex: "1 200px" should set flexGrow=1, flexShrink=1, flexBasis="200px"
local element = FlexLove.new({
width = 100,
height = 100,
flex = "1 200px",
})
luaunit.assertEquals(element.flexGrow, 1)
luaunit.assertEquals(element.flexShrink, 1)
luaunit.assertEquals(element.flexBasis, "200px")
end
function TestFlexShorthand:test_flex_three_values()
-- flex: "2 1 150px" should set all three values
local element = FlexLove.new({
width = 100,
height = 100,
flex = "2 1 150px",
})
luaunit.assertEquals(element.flexGrow, 2)
luaunit.assertEquals(element.flexShrink, 1)
luaunit.assertEquals(element.flexBasis, "150px")
end
function TestFlexShorthand:test_explicit_props_override_shorthand()
-- Explicit properties should override flex shorthand
local element = FlexLove.new({
width = 100,
height = 100,
flex = 1,
flexGrow = 3,
flexShrink = 0,
})
luaunit.assertEquals(element.flexGrow, 3)
luaunit.assertEquals(element.flexShrink, 0)
luaunit.assertEquals(element.flexBasis, 0)
end
-- ============================================================================
-- Test Suite 2: Flex Property Validation
-- ============================================================================
TestFlexPropertyValidation = {}
function TestFlexPropertyValidation:setUp()
FlexLove.beginFrame()
end
function TestFlexPropertyValidation:tearDown()
FlexLove.endFrame()
end
function TestFlexPropertyValidation:test_default_flex_values()
local element = FlexLove.new({
width = 100,
height = 100,
})
luaunit.assertEquals(element.flexGrow, 0)
luaunit.assertEquals(element.flexShrink, 1)
luaunit.assertEquals(element.flexBasis, "auto")
end
function TestFlexPropertyValidation:test_valid_flexGrow()
local element = FlexLove.new({
width = 100,
height = 100,
flexGrow = 2,
})
luaunit.assertEquals(element.flexGrow, 2)
end
function TestFlexPropertyValidation:test_invalid_flexGrow_negative()
-- Negative flexGrow should default to 0 with warning
local element = FlexLove.new({
width = 100,
height = 100,
flexGrow = -1,
})
luaunit.assertEquals(element.flexGrow, 0)
end
function TestFlexPropertyValidation:test_invalid_flexGrow_string()
-- String flexGrow should default to 0 with warning
local element = FlexLove.new({
width = 100,
height = 100,
flexGrow = "invalid",
})
luaunit.assertEquals(element.flexGrow, 0)
end
function TestFlexPropertyValidation:test_valid_flexShrink()
local element = FlexLove.new({
width = 100,
height = 100,
flexShrink = 0,
})
luaunit.assertEquals(element.flexShrink, 0)
end
function TestFlexPropertyValidation:test_invalid_flexShrink_negative()
-- Negative flexShrink should default to 1 with warning
local element = FlexLove.new({
width = 100,
height = 100,
flexShrink = -1,
})
luaunit.assertEquals(element.flexShrink, 1)
end
function TestFlexPropertyValidation:test_flexBasis_auto()
local element = FlexLove.new({
width = 100,
height = 100,
flexBasis = "auto",
})
luaunit.assertEquals(element.flexBasis, "auto")
end
function TestFlexPropertyValidation:test_flexBasis_number()
local element = FlexLove.new({
width = 100,
height = 100,
flexBasis = 200,
})
luaunit.assertEquals(element.flexBasis, 200)
end
function TestFlexPropertyValidation:test_flexBasis_with_units()
local element = FlexLove.new({
width = 100,
height = 100,
flexBasis = "50%",
})
luaunit.assertEquals(element.flexBasis, "50%")
luaunit.assertNotNil(element.units.flexBasis)
end
-- ============================================================================
-- Test Suite 3: Flex Grow - Distributing Extra Space
-- ============================================================================
TestFlexGrow = {}
function TestFlexGrow:setUp()
FlexLove.beginFrame()
end
function TestFlexGrow:tearDown()
FlexLove.endFrame()
end
function TestFlexGrow:test_single_item_with_flex_grow()
local container = FlexLove.new({
width = 500,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
parent = container,
})
container:layoutChildren()
-- Child should grow to fill container: 500px
luaunit.assertEquals(child.width, 500)
end
function TestFlexGrow:test_two_items_equal_flex_grow()
local container = FlexLove.new({
width = 600,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
parent = container,
})
container:layoutChildren()
-- Both children should grow equally: (600 - 200) / 2 = 200 extra each
luaunit.assertEquals(child1.width, 300)
luaunit.assertEquals(child2.width, 300)
end
function TestFlexGrow:test_proportional_flex_grow()
local container = FlexLove.new({
width = 700,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 2,
parent = container,
})
container:layoutChildren()
-- Free space: 700 - 200 = 500
-- child1 gets: 100 + (1/3 * 500) = 266.67
-- child2 gets: 100 + (2/3 * 500) = 433.33
luaunit.assertEquals(roundToDecimal(child1.width, 1), 266.7)
luaunit.assertEquals(roundToDecimal(child2.width, 1), 433.3)
end
function TestFlexGrow:test_flex_grow_with_gap()
local container = FlexLove.new({
width = 620,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 20,
})
local child1 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
parent = container,
})
container:layoutChildren()
-- Available space: 620 - 20(gap) = 600
-- Free space: 600 - 200 = 400, divided equally = 200 each
luaunit.assertEquals(child1.width, 300)
luaunit.assertEquals(child2.width, 300)
end
function TestFlexGrow:test_flex_grow_vertical()
local container = FlexLove.new({
width = 100,
height = 500,
positioning = "flex",
flexDirection = "vertical",
gap = 0,
})
local child1 = FlexLove.new({
width = 50,
height = 100,
flexGrow = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 50,
height = 100,
flexGrow = 1,
parent = container,
})
container:layoutChildren()
-- Both children should grow equally in vertical direction
luaunit.assertEquals(child1.height, 250)
luaunit.assertEquals(child2.height, 250)
end
function TestFlexGrow:test_flex_grow_with_margins()
local container = FlexLove.new({
width = 640,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
margin = { left = 10, right = 10, top = 0, bottom = 0 },
parent = container,
})
local child2 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
margin = { left = 10, right = 10, top = 0, bottom = 0 },
parent = container,
})
container:layoutChildren()
-- Total margins: 40px (10+10 for each child)
-- Basis sizes with margins: 120 + 120 = 240
-- Free space: 640 - 240 = 400
-- Each child grows by 200, so final width = 300
luaunit.assertEquals(child1.width, 300)
luaunit.assertEquals(child2.width, 300)
end
-- ============================================================================
-- Test Suite 4: Flex Shrink - Handling Overflow
-- ============================================================================
TestFlexShrink = {}
function TestFlexShrink:setUp()
FlexLove.beginFrame()
end
function TestFlexShrink:tearDown()
FlexLove.endFrame()
end
function TestFlexShrink:test_items_shrink_equally()
-- CSS behavior: Items with flex-shrink: 1 (default) should shrink when overflow
local container = FlexLove.new({
width = 300,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 200,
height = 50,
flexShrink = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 200,
height = 50,
flexShrink = 1,
parent = container,
})
container:layoutChildren()
-- Overflow: 400 - 300 = 100
-- Both shrink equally (same flex-shrink and flex-basis)
-- Expected CSS behavior: 150px each
luaunit.assertEquals(child1.width, 150)
luaunit.assertEquals(child2.width, 150)
end
function TestFlexShrink:test_proportional_shrink()
local container = FlexLove.new({
width = 300,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 200,
height = 50,
flexShrink = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 200,
height = 50,
flexShrink = 2,
parent = container,
})
container:layoutChildren()
-- Overflow: 400 - 300 = 100
-- Scaled shrink factors: 1*200=200, 2*200=400, total=600
-- child1 shrinks: (200/600) * 100 = 33.33, final = 166.67
-- child2 shrinks: (400/600) * 100 = 66.67, final = 133.33
luaunit.assertEquals(roundToDecimal(child1.width, 1), 166.7)
luaunit.assertEquals(roundToDecimal(child2.width, 1), 133.3)
end
function TestFlexShrink:test_flex_shrink_zero_prevents_shrinking()
local container = FlexLove.new({
width = 300,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 150,
height = 50,
flexShrink = 0,
parent = container,
})
local child2 = FlexLove.new({
width = 200,
height = 50,
flexShrink = 1,
parent = container,
})
container:layoutChildren()
-- child1 should not shrink (flexShrink = 0)
-- child2 absorbs all overflow: 200 - 50 = 150
luaunit.assertEquals(child1.width, 150)
luaunit.assertEquals(child2.width, 150)
end
function TestFlexShrink:test_different_basis_affects_shrink()
local container = FlexLove.new({
width = 300,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 100,
height = 50,
flexShrink = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 300,
height = 50,
flexShrink = 1,
parent = container,
})
container:layoutChildren()
-- Overflow: 400 - 300 = 100
-- Scaled shrink factors: 1*100=100, 1*300=300, total=400
-- child1 shrinks: (100/400) * 100 = 25, final = 75
-- child2 shrinks: (300/400) * 100 = 75, final = 225
luaunit.assertEquals(child1.width, 75)
luaunit.assertEquals(child2.width, 225)
end
function TestFlexShrink:test_shrink_with_margins()
local container = FlexLove.new({
width = 300,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 200,
height = 50,
flexShrink = 1,
margin = { left = 10, right = 10, top = 0, bottom = 0 },
parent = container,
})
local child2 = FlexLove.new({
width = 200,
height = 50,
flexShrink = 1,
margin = { left = 10, right = 10, top = 0, bottom = 0 },
parent = container,
})
container:layoutChildren()
-- Basis sizes with margins: 220 + 220 = 440
-- Overflow: 440 - 300 = 140
-- Both shrink equally: 70 each
luaunit.assertEquals(child1.width, 130)
luaunit.assertEquals(child2.width, 130)
end
-- ============================================================================
-- Test Suite 5: Flex Basis
-- ============================================================================
TestFlexBasis = {}
function TestFlexBasis:setUp()
FlexLove.beginFrame()
end
function TestFlexBasis:tearDown()
FlexLove.endFrame()
end
function TestFlexBasis:test_flexBasis_auto_uses_element_width()
local container = FlexLove.new({
width = 500,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child = FlexLove.new({
width = 150,
height = 50,
flexBasis = "auto",
flexGrow = 0,
parent = container,
})
container:layoutChildren()
-- flexBasis "auto" should use element's width
luaunit.assertEquals(child.width, 150)
end
function TestFlexBasis:test_flexBasis_numeric_overrides_width()
local container = FlexLove.new({
width = 500,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child = FlexLove.new({
width = 150,
height = 50,
flexBasis = 200,
flexGrow = 0,
parent = container,
})
container:layoutChildren()
-- flexBasis should override width as the starting point
luaunit.assertEquals(child.width, 200)
end
function TestFlexBasis:test_flexBasis_percentage()
local container = FlexLove.new({
width = 600,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child = FlexLove.new({
width = 100,
height = 50,
flexBasis = "50%",
flexGrow = 0,
parent = container,
})
container:layoutChildren()
-- flexBasis 50% of container = 300px
luaunit.assertEquals(child.width, 300)
end
function TestFlexBasis:test_flexBasis_with_grow()
local container = FlexLove.new({
width = 600,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 100,
height = 50,
flexBasis = 100,
flexGrow = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 200,
height = 50,
flexBasis = 200,
flexGrow = 1,
parent = container,
})
container:layoutChildren()
-- Total basis: 300, free space: 300
-- Each grows by 150
luaunit.assertEquals(child1.width, 250)
luaunit.assertEquals(child2.width, 350)
end
function TestFlexBasis:test_flexBasis_zero_with_grow()
local container = FlexLove.new({
width = 600,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 100,
height = 50,
flexBasis = 0,
flexGrow = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 200,
height = 50,
flexBasis = 0,
flexGrow = 1,
parent = container,
})
container:layoutChildren()
-- Total basis: 0, all space is free space
-- Distributed equally: 300 each
luaunit.assertEquals(child1.width, 300)
luaunit.assertEquals(child2.width, 300)
end
-- ============================================================================
-- Test Suite 6: Complex Scenarios
-- ============================================================================
TestFlexComplexScenarios = {}
function TestFlexComplexScenarios:setUp()
FlexLove.beginFrame()
end
function TestFlexComplexScenarios:tearDown()
FlexLove.endFrame()
end
function TestFlexComplexScenarios:test_mixed_grow_no_grow()
local container = FlexLove.new({
width = 700,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 150,
height = 50,
flexGrow = 0,
parent = container,
})
local child2 = FlexLove.new({
width = 150,
height = 50,
flexGrow = 1,
parent = container,
})
local child3 = FlexLove.new({
width = 150,
height = 50,
flexGrow = 2,
parent = container,
})
container:layoutChildren()
-- child1 doesn't grow: 150
-- Free space: 700 - 450 = 250
-- child2 grows: 150 + (1/3 * 250) = 233.33
-- child3 grows: 150 + (2/3 * 250) = 316.67
luaunit.assertEquals(child1.width, 150)
luaunit.assertEquals(roundToDecimal(child2.width, 1), 233.3)
luaunit.assertEquals(roundToDecimal(child3.width, 1), 316.7)
end
function TestFlexComplexScenarios:test_wrapping_with_flex_grow()
local container = FlexLove.new({
width = 400,
height = 200,
positioning = "flex",
flexDirection = "horizontal",
flexWrap = "wrap",
gap = 0,
})
local child1 = FlexLove.new({
width = 150,
height = 50,
flexGrow = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 150,
height = 50,
flexGrow = 1,
parent = container,
})
local child3 = FlexLove.new({
width = 150,
height = 50,
flexGrow = 1,
parent = container,
})
container:layoutChildren()
-- First line: child1, child2 (300px basis, 100px free space)
-- Each grows by 50px -> 200px each
-- Second line: child3 (150px basis, 250px free space)
-- child3 grows to fill line -> 400px
luaunit.assertEquals(child1.width, 200)
luaunit.assertEquals(child2.width, 200)
luaunit.assertEquals(child3.width, 400)
end
function TestFlexComplexScenarios:test_exact_fit_no_grow_no_shrink()
local container = FlexLove.new({
width = 400,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local child1 = FlexLove.new({
width = 200,
height = 50,
flexGrow = 1,
flexShrink = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 200,
height = 50,
flexGrow = 1,
flexShrink = 1,
parent = container,
})
container:layoutChildren()
-- Perfect fit: no growing or shrinking needed
luaunit.assertEquals(child1.width, 200)
luaunit.assertEquals(child2.width, 200)
end
function TestFlexComplexScenarios:test_nested_flex_containers()
local outer = FlexLove.new({
width = 800,
height = 200,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
})
local inner = FlexLove.new({
width = 400,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
flexGrow = 1,
gap = 0,
parent = outer,
})
local child1 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
parent = inner,
})
local child2 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
parent = inner,
})
outer:layoutChildren()
-- Inner container grows from 400 to 800
luaunit.assertEquals(inner.width, 800)
-- Children of inner should each get 400
luaunit.assertEquals(child1.width, 400)
luaunit.assertEquals(child2.width, 400)
end
function TestFlexComplexScenarios:test_flex_with_padding()
local container = FlexLove.new({
width = 600,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
gap = 0,
padding = { left = 20, right = 20, top = 10, bottom = 10 },
})
local child1 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
parent = container,
})
local child2 = FlexLove.new({
width = 100,
height = 50,
flexGrow = 1,
parent = container,
})
container:layoutChildren()
-- Available space: 600 - 40 (padding) = 560
-- Free space: 560 - 200 = 360, divided equally = 180 each
luaunit.assertEquals(child1.width, 280)
luaunit.assertEquals(child2.width, 280)
end
-- Run tests only if not part of runAll.lua
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end