diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..592e36f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# FlexLöve Agent Guidelines + +## Testing +- **Run all tests**: `lua testing/runAll.lua` (coverage report in `luacov.report.out`) +- **Run single test**: `lua testing/__tests__/.lua` +- **Test immediate mode**: Call `FlexLove.setMode("immediate")` in `setUp()`, then `FlexLove.beginFrame()`/`FlexLove.endFrame()` to trigger layout + +## Code Style +- **Modules**: Use `local ModuleName = {}` pattern, return table at end +- **Constructors**: `ClassName.new(props)` → instance (always returns, never nil) +- **Instance methods**: `instance:methodName()` with colon syntax +- **Static methods**: `ClassName.methodName()` with dot syntax +- **Private fields**: Prefix with `_` (e.g., `self._internalState`) +- **LuaDoc annotations**: Use `---@param`, `---@return`, `---@class`, `---@field` for all public APIs +- **Error handling**: Use `ErrorHandler.error(module, message)` for critical errors, `ErrorHandler.warn(module, message)` for warnings +- **String format**: Use `string.format()` for complex strings, avoid concatenation +- **Auto-sizing**: Omit `width`/`height` properties (NOT `width = "auto"`) + +## Architecture +- **Immediate mode**: Elements recreated each frame, layout triggered by `endFrame()` → `layoutChildren()` called on top-level elements +- **Retained mode**: Elements persist, must manually update properties (default) +- **Dependencies**: Pass via `deps` table parameter in constructors (e.g., `{utils, ErrorHandler, Units}`) +- **Layout flow**: `Element.new()` → `layoutChildren()` on construction → `resize()` on viewport change → `layoutChildren()` again +- **CSS positioning**: `top/right/bottom/left` applied via `LayoutEngine:applyPositioningOffsets()` for absolute/relative containers + +## Common Patterns +- **Return values**: Single value OR `value, errorString` (nil on success for error) +- **Enums**: Access via `utils.enums.EnumName.VALUE` (e.g., `Positioning.FLEX`) +- **Units**: Parse with `Units.parse(value)` → `value, unit`, resolve with `Units.resolve(value, unit, viewportW, viewportH, parentSize)` +- **Colors**: Use `Color.new(r, g, b, a)` (0-1 range) or `Color.fromHex("#RRGGBB")` diff --git a/modules/Grid.lua b/modules/Grid.lua index 08238ab..f0f9e60 100644 --- a/modules/Grid.lua +++ b/modules/Grid.lua @@ -11,8 +11,9 @@ local Grid = {} --- Layout grid items within a grid container using simple row/column counts ---@param element Element -- Grid container element function Grid.layoutGridItems(element) - local rows = element.gridRows or 1 - local columns = element.gridColumns or 1 + -- Ensure valid row/column counts (must be at least 1 to avoid division by zero) + local rows = element.gridRows and element.gridRows > 0 and element.gridRows or 1 + local columns = element.gridColumns and element.gridColumns > 0 and element.gridColumns or 1 -- Calculate space reserved by absolutely positioned siblings local reservedLeft = 0 diff --git a/modules/LayoutEngine.lua b/modules/LayoutEngine.lua index 487ba5a..b00161c 100644 --- a/modules/LayoutEngine.lua +++ b/modules/LayoutEngine.lua @@ -154,6 +154,11 @@ function LayoutEngine:layoutChildren() self:applyPositioningOffsets(child) end end + + -- Detect overflow after children positioning + if self.element._detectOverflow then + self.element:_detectOverflow() + end return end diff --git a/modules/ScrollManager.lua b/modules/ScrollManager.lua index 7f034d3..09eb4a1 100644 --- a/modules/ScrollManager.lua +++ b/modules/ScrollManager.lua @@ -125,14 +125,16 @@ function ScrollManager:detectOverflow() for _, child in ipairs(element.children) do -- Skip absolutely positioned children (they don't contribute to overflow) if not child._explicitlyAbsolute then - -- Calculate child position relative to content area - local childLeft = child.x - contentX - local childTop = child.y - contentY - local childRight = childLeft + child:getBorderBoxWidth() + child.margin.right - local childBottom = childTop + child:getBorderBoxHeight() + child.margin.bottom + -- Calculate child's margin box bounds relative to content area + -- child.x/y is the border-box position, margins extend outside this + local childMarginLeft = child.x - contentX - child.margin.left + local childMarginTop = child.y - contentY - child.margin.top + local childMarginRight = child.x - contentX + child:getBorderBoxWidth() + child.margin.right + local childMarginBottom = child.y - contentY + child:getBorderBoxHeight() + child.margin.bottom - maxX = math.max(maxX, childRight) - maxY = math.max(maxY, childBottom) + -- Track the maximum extents (we ignore negative space from margins) + maxX = math.max(maxX, childMarginRight) + maxY = math.max(maxY, childMarginBottom) end end @@ -140,9 +142,10 @@ function ScrollManager:detectOverflow() self._contentWidth = maxX self._contentHeight = maxY - -- Detect overflow - local containerWidth = element.width - local containerHeight = element.height + -- Detect overflow (compare against content area, not total element size) + -- The content area excludes padding + local containerWidth = element.width - element.padding.left - element.padding.right + local containerHeight = element.height - element.padding.top - element.padding.bottom self._overflowX = self._contentWidth > containerWidth self._overflowY = self._contentHeight > containerHeight diff --git a/testing/__tests__/grid_test.lua b/testing/__tests__/grid_test.lua new file mode 100644 index 0000000..e58a6c5 --- /dev/null +++ b/testing/__tests__/grid_test.lua @@ -0,0 +1,490 @@ +-- Test suite for Grid layout functionality +-- Grid layout has 0% test coverage despite being integrated into the system +package.path = package.path .. ";./?.lua;./modules/?.lua" + +require("testing.loveStub") +local luaunit = require("testing.luaunit") +local FlexLove = require("FlexLove") + +TestGridLayout = {} + +function TestGridLayout:setUp() + FlexLove.beginFrame(1920, 1080) +end + +function TestGridLayout:tearDown() + FlexLove.endFrame() +end + +-- Test basic grid layout with default 1x1 grid +function TestGridLayout:test_default_grid_single_child() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 400, + height = 300, + positioning = "grid" + -- Default: gridRows=1, gridColumns=1 + }) + + local child = FlexLove.new({ + id = "child1", + parent = container, + width = 50, -- Will be stretched by grid + height = 50 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Child should be stretched to fill the entire grid cell + luaunit.assertEquals(child.x, 0, "Child should be at x=0") + luaunit.assertEquals(child.y, 0, "Child should be at y=0") + luaunit.assertEquals(child.width, 400, "Child should be stretched to container width") + luaunit.assertEquals(child.height, 300, "Child should be stretched to container height") +end + +-- Test 2x2 grid layout +function TestGridLayout:test_2x2_grid_four_children() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 400, + height = 400, + positioning = "grid", + gridRows = 2, + gridColumns = 2 + }) + + local children = {} + for i = 1, 4 do + children[i] = FlexLove.new({ + id = "child" .. i, + parent = container, + width = 50, + height = 50 + }) + end + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Each cell should be 200x200 + -- Child 1: top-left (0, 0) + luaunit.assertEquals(children[1].x, 0, "Child 1 should be at x=0") + luaunit.assertEquals(children[1].y, 0, "Child 1 should be at y=0") + luaunit.assertEquals(children[1].width, 200, "Cell width should be 200") + luaunit.assertEquals(children[1].height, 200, "Cell height should be 200") + + -- Child 2: top-right (200, 0) + luaunit.assertEquals(children[2].x, 200, "Child 2 should be at x=200") + luaunit.assertEquals(children[2].y, 0, "Child 2 should be at y=0") + + -- Child 3: bottom-left (0, 200) + luaunit.assertEquals(children[3].x, 0, "Child 3 should be at x=0") + luaunit.assertEquals(children[3].y, 200, "Child 3 should be at y=200") + + -- Child 4: bottom-right (200, 200) + luaunit.assertEquals(children[4].x, 200, "Child 4 should be at x=200") + luaunit.assertEquals(children[4].y, 200, "Child 4 should be at y=200") +end + +-- Test grid with column and row gaps +function TestGridLayout:test_grid_with_gaps() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 420, -- 2 cells * 200 + 1 gap * 20 + height = 320, -- 2 cells * 150 + 1 gap * 20 + positioning = "grid", + gridRows = 2, + gridColumns = 2, + columnGap = 20, + rowGap = 20 + }) + + local children = {} + for i = 1, 4 do + children[i] = FlexLove.new({ + id = "child" .. i, + parent = container, + width = 50, + height = 50 + }) + end + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Cell size: (420 - 20) / 2 = 200, (320 - 20) / 2 = 150 + luaunit.assertEquals(children[1].width, 200, "Cell width should be 200") + luaunit.assertEquals(children[1].height, 150, "Cell height should be 150") + + -- Child 2 should be offset by cell width + gap + luaunit.assertEquals(children[2].x, 220, "Child 2 x = 200 + 20 gap") + luaunit.assertEquals(children[2].y, 0, "Child 2 should be at y=0") + + -- Child 3 should be offset by cell height + gap + luaunit.assertEquals(children[3].x, 0, "Child 3 should be at x=0") + luaunit.assertEquals(children[3].y, 170, "Child 3 y = 150 + 20 gap") +end + +-- Test grid with more children than cells (overflow) +function TestGridLayout:test_grid_overflow_children() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 400, + height = 200, + positioning = "grid", + gridRows = 2, + gridColumns = 2 + -- Only 4 cells available + }) + + local children = {} + for i = 1, 6 do -- 6 children, but only 4 cells + children[i] = FlexLove.new({ + id = "child" .. i, + parent = container, + width = 50, + height = 50 + }) + end + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- First 4 children should be positioned + luaunit.assertNotNil(children[1].x, "Child 1 should be positioned") + luaunit.assertNotNil(children[4].x, "Child 4 should be positioned") + + -- Children 5 and 6 should NOT be positioned (or positioned at 0,0 by default) + -- This tests the overflow behavior: row >= rows breaks the loop +end + +-- Test grid with alignItems center +function TestGridLayout:test_grid_align_center() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 400, + height = 400, + positioning = "grid", + gridRows = 2, + gridColumns = 2, + alignItems = "center" + }) + + local child = FlexLove.new({ + id = "child1", + parent = container, + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Cell is 200x200, child is 100x100, should be centered + -- Center position: (200 - 100) / 2 = 50 + luaunit.assertEquals(child.x, 50, "Child should be centered horizontally in cell") + luaunit.assertEquals(child.y, 50, "Child should be centered vertically in cell") + luaunit.assertEquals(child.width, 100, "Child width should not be stretched") + luaunit.assertEquals(child.height, 100, "Child height should not be stretched") +end + +-- Test grid with alignItems flex-start +function TestGridLayout:test_grid_align_flex_start() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 400, + height = 400, + positioning = "grid", + gridRows = 2, + gridColumns = 2, + alignItems = "flex-start" + }) + + local child = FlexLove.new({ + id = "child1", + parent = container, + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Child should be at top-left of cell + luaunit.assertEquals(child.x, 0, "Child should be at left of cell") + luaunit.assertEquals(child.y, 0, "Child should be at top of cell") + luaunit.assertEquals(child.width, 100, "Child width should not be stretched") + luaunit.assertEquals(child.height, 100, "Child height should not be stretched") +end + +-- Test grid with alignItems flex-end +function TestGridLayout:test_grid_align_flex_end() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 400, + height = 400, + positioning = "grid", + gridRows = 2, + gridColumns = 2, + alignItems = "flex-end" + }) + + local child = FlexLove.new({ + id = "child1", + parent = container, + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Cell is 200x200, child is 100x100, should be at bottom-right + luaunit.assertEquals(child.x, 100, "Child should be at right of cell (200 - 100)") + luaunit.assertEquals(child.y, 100, "Child should be at bottom of cell (200 - 100)") + luaunit.assertEquals(child.width, 100, "Child width should not be stretched") + luaunit.assertEquals(child.height, 100, "Child height should not be stretched") +end + +-- Test grid with padding +function TestGridLayout:test_grid_with_padding() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 500, -- Total width + height = 500, + padding = { top = 50, right = 50, bottom = 50, left = 50 }, + positioning = "grid", + gridRows = 2, + gridColumns = 2 + }) + + local child = FlexLove.new({ + id = "child1", + parent = container, + width = 50, + height = 50 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Available space: 500 - 50 - 50 = 400 + -- Cell size: 400 / 2 = 200 + -- Child should be positioned at padding.left, padding.top + luaunit.assertEquals(child.x, 50, "Child x should account for left padding") + luaunit.assertEquals(child.y, 50, "Child y should account for top padding") + luaunit.assertEquals(child.width, 200, "Cell width should be 200") + luaunit.assertEquals(child.height, 200, "Cell height should be 200") +end + +-- Test grid with absolutely positioned child (should be skipped in grid layout) +function TestGridLayout:test_grid_with_absolute_child() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 400, + height = 400, + positioning = "grid", + gridRows = 2, + gridColumns = 2 + }) + + -- Regular child + local child1 = FlexLove.new({ + id = "child1", + parent = container, + width = 50, + height = 50 + }) + + -- Absolutely positioned child (should be ignored by grid layout) + local child2 = FlexLove.new({ + id = "child2", + parent = container, + positioning = "absolute", + x = 10, + y = 10, + width = 30, + height = 30 + }) + + -- Another regular child + local child3 = FlexLove.new({ + id = "child3", + parent = container, + width = 50, + height = 50 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- child1 should be in first grid cell (0, 0) + luaunit.assertEquals(child1.x, 0, "Child 1 should be at x=0") + luaunit.assertEquals(child1.y, 0, "Child 1 should be at y=0") + + -- child2 should keep its absolute position + luaunit.assertEquals(child2.x, 10, "Absolute child should keep x=10") + luaunit.assertEquals(child2.y, 10, "Absolute child should keep y=10") + + -- child3 should be in second grid cell (200, 0), not third + luaunit.assertEquals(child3.x, 200, "Child 3 should be in second cell at x=200") + luaunit.assertEquals(child3.y, 0, "Child 3 should be in second cell at y=0") +end + +-- Test edge case: empty grid +function TestGridLayout:test_empty_grid() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 400, + height = 400, + positioning = "grid", + gridRows = 2, + gridColumns = 2 + -- No children + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Should not crash + luaunit.assertEquals(#container.children, 0, "Grid should have no children") +end + +-- Test edge case: grid with 0 columns or rows +function TestGridLayout:test_grid_zero_dimensions() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 400, + height = 400, + positioning = "grid", + gridRows = 0, -- Invalid: 0 rows + gridColumns = 0 -- Invalid: 0 columns + }) + + local child = FlexLove.new({ + id = "child1", + parent = container, + width = 50, + height = 50 + }) + + -- This might cause division by zero or other errors + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Test passes if it doesn't crash + luaunit.assertTrue(true, "Grid with 0 dimensions should not crash") +end + +-- Test nested grids +function TestGridLayout:test_nested_grids() + local outerGrid = FlexLove.new({ + id = "outer", + x = 0, + y = 0, + width = 400, + height = 400, + positioning = "grid", + gridRows = 2, + gridColumns = 2 + }) + + -- First cell contains another grid + local innerGrid = FlexLove.new({ + id = "inner", + parent = outerGrid, + width = 200, + height = 200, + positioning = "grid", + gridRows = 2, + gridColumns = 2 + }) + + -- Add children to inner grid + for i = 1, 4 do + FlexLove.new({ + id = "inner_child" .. i, + parent = innerGrid, + width = 25, + height = 25 + }) + end + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Inner grid should be positioned in first cell of outer grid + luaunit.assertEquals(innerGrid.x, 0, "Inner grid should be at x=0") + luaunit.assertEquals(innerGrid.y, 0, "Inner grid should be at y=0") + luaunit.assertEquals(#innerGrid.children, 4, "Inner grid should have 4 children") +end + +-- Test grid with reserved space from absolute children +function TestGridLayout:test_grid_with_reserved_space() + local container = FlexLove.new({ + id = "grid", + x = 0, + y = 0, + width = 400, + height = 400, + positioning = "grid", + gridRows = 2, + gridColumns = 2 + }) + + -- Absolute child with left positioning (reserves left space) + FlexLove.new({ + id = "absolute_left", + parent = container, + positioning = "absolute", + left = 0, + top = 0, + width = 50, + height = 50 + }) + + -- Regular grid child + local child1 = FlexLove.new({ + id = "child1", + parent = container, + width = 50, + height = 50 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Grid should account for reserved space + -- Available width: 400 - 50 (reserved left) = 350 + -- Cell width: 350 / 2 = 175 + -- Child should start at x = reserved left = 50 + luaunit.assertEquals(child1.x, 50, "Child should be offset by reserved left space") + luaunit.assertEquals(child1.width, 175, "Cell width should account for reserved space") +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/layout_edge_cases_test.lua b/testing/__tests__/layout_edge_cases_test.lua new file mode 100644 index 0000000..a9a504a --- /dev/null +++ b/testing/__tests__/layout_edge_cases_test.lua @@ -0,0 +1,404 @@ +-- Test suite for layout edge cases and warnings +-- Tests untested code paths in LayoutEngine +package.path = package.path .. ";./?.lua;./modules/?.lua" + +require("testing.loveStub") +local luaunit = require("testing.luaunit") +local FlexLove = require("FlexLove") +local ErrorHandler = require("modules.ErrorHandler") + +TestLayoutEdgeCases = {} + +function TestLayoutEdgeCases:setUp() + FlexLove.setMode("immediate") + FlexLove.beginFrame() + -- Capture warnings + self.warnings = {} + self.originalWarn = ErrorHandler.warn + ErrorHandler.warn = function(module, message) + table.insert(self.warnings, {module = module, message = message}) + end +end + +function TestLayoutEdgeCases:tearDown() + -- Restore original warn function + ErrorHandler.warn = self.originalWarn + FlexLove.endFrame() +end + +-- Test: Child with percentage width in auto-sizing parent should trigger warning +function TestLayoutEdgeCases:test_percentage_width_with_auto_parent_warns() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + -- width not specified - auto-sizing width + height = 200, + positioning = "flex", + flexDirection = "horizontal" + }) + + FlexLove.new({ + id = "child_with_percentage", + parent = container, + width = "50%", -- Percentage width with auto-sizing parent - should warn + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Check that a warning was issued + luaunit.assertTrue(#self.warnings > 0, "Should issue warning for percentage width with auto-sizing parent") + + local found = false + for _, warning in ipairs(self.warnings) do + if warning.message:match("percentage width") and warning.message:match("auto%-sizing") then + found = true + break + end + end + + luaunit.assertTrue(found, "Warning should mention percentage width and auto-sizing") +end + +-- Test: Child with percentage height in auto-sizing parent should trigger warning +function TestLayoutEdgeCases:test_percentage_height_with_auto_parent_warns() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 200, + -- height not specified - auto-sizing height + positioning = "flex", + flexDirection = "vertical" + }) + + FlexLove.new({ + id = "child_with_percentage", + parent = container, + width = 100, + height = "50%" -- Percentage height with auto-sizing parent - should warn + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Check that a warning was issued + luaunit.assertTrue(#self.warnings > 0, "Should issue warning for percentage height with auto-sizing parent") + + local found = false + for _, warning in ipairs(self.warnings) do + if warning.message:match("percentage height") and warning.message:match("auto%-sizing") then + found = true + break + end + end + + luaunit.assertTrue(found, "Warning should mention percentage height and auto-sizing") +end + +-- Test: Pixel-sized children in auto-sizing parent should NOT warn +function TestLayoutEdgeCases:test_pixel_width_with_auto_parent_no_warn() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + -- width not specified - auto-sizing + height = 200, + positioning = "flex", + flexDirection = "horizontal" + }) + + FlexLove.new({ + id = "child_with_pixels", + parent = container, + width = 100, -- Pixel width - should NOT warn + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Check that NO warning was issued about percentage sizing + for _, warning in ipairs(self.warnings) do + local hasPercentageWarning = warning.message:match("percentage") and warning.message:match("auto%-sizing") + luaunit.assertFalse(hasPercentageWarning, "Should not warn for pixel-sized children") + end +end + +-- Test: CSS positioning - top offset in absolute container +function TestLayoutEdgeCases:test_css_positioning_top_offset() + local container = FlexLove.new({ + id = "container", + x = 100, + y = 100, + width = 400, + height = 400, + positioning = "absolute" + }) + + local child = FlexLove.new({ + id = "child", + parent = container, + positioning = "absolute", + top = 50, -- 50px from top + left = 0, + width = 100, + height = 100 + }) + + -- Trigger layout by ending and restarting frame + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Child should be positioned 50px from container's top edge (accounting for padding) + local expectedY = container.y + container.padding.top + 50 + luaunit.assertEquals(child.y, expectedY, "Child should be positioned with top offset") +end + +-- Test: CSS positioning - bottom offset in absolute container +function TestLayoutEdgeCases:test_css_positioning_bottom_offset() + local container = FlexLove.new({ + id = "container", + x = 100, + y = 100, + width = 400, + height = 400, + positioning = "absolute" + }) + + local child = FlexLove.new({ + id = "child", + parent = container, + positioning = "absolute", + bottom = 50, -- 50px from bottom + left = 0, + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Child should be positioned 50px from container's bottom edge + local expectedY = container.y + container.padding.top + container.height - 50 - child:getBorderBoxHeight() + luaunit.assertEquals(child.y, expectedY, "Child should be positioned with bottom offset") +end + +-- Test: CSS positioning - left offset in absolute container +function TestLayoutEdgeCases:test_css_positioning_left_offset() + local container = FlexLove.new({ + id = "container", + x = 100, + y = 100, + width = 400, + height = 400, + positioning = "absolute" + }) + + local child = FlexLove.new({ + id = "child", + parent = container, + positioning = "absolute", + top = 0, + left = 50, -- 50px from left + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Child should be positioned 50px from container's left edge + local expectedX = container.x + container.padding.left + 50 + luaunit.assertEquals(child.x, expectedX, "Child should be positioned with left offset") +end + +-- Test: CSS positioning - right offset in absolute container +function TestLayoutEdgeCases:test_css_positioning_right_offset() + local container = FlexLove.new({ + id = "container", + x = 100, + y = 100, + width = 400, + height = 400, + positioning = "absolute" + }) + + local child = FlexLove.new({ + id = "child", + parent = container, + positioning = "absolute", + top = 0, + right = 50, -- 50px from right + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Child should be positioned 50px from container's right edge + local expectedX = container.x + container.padding.left + container.width - 50 - child:getBorderBoxWidth() + luaunit.assertEquals(child.x, expectedX, "Child should be positioned with right offset") +end + +-- Test: CSS positioning - combined top and bottom (bottom should take precedence or be ignored) +function TestLayoutEdgeCases:test_css_positioning_top_and_bottom() + local container = FlexLove.new({ + id = "container", + x = 100, + y = 100, + width = 400, + height = 400, + positioning = "absolute" + }) + + local child = FlexLove.new({ + id = "child", + parent = container, + positioning = "absolute", + top = 10, + bottom = 20, -- Both specified - last one wins in current implementation + left = 0, + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Bottom should override top + local expectedY = container.y + container.padding.top + container.height - 20 - child:getBorderBoxHeight() + luaunit.assertEquals(child.y, expectedY, "Bottom offset should override top when both specified") +end + +-- Test: CSS positioning - combined left and right (right should take precedence or be ignored) +function TestLayoutEdgeCases:test_css_positioning_left_and_right() + local container = FlexLove.new({ + id = "container", + x = 100, + y = 100, + width = 400, + height = 400, + positioning = "absolute" + }) + + local child = FlexLove.new({ + id = "child", + parent = container, + positioning = "absolute", + top = 0, + left = 10, + right = 20, -- Both specified - last one wins in current implementation + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Right should override left + local expectedX = container.x + container.padding.left + container.width - 20 - child:getBorderBoxWidth() + luaunit.assertEquals(child.x, expectedX, "Right offset should override left when both specified") +end + +-- Test: CSS positioning with padding in container +function TestLayoutEdgeCases:test_css_positioning_with_padding() + local container = FlexLove.new({ + id = "container", + x = 100, + y = 100, + width = 400, + height = 400, + padding = { top = 20, right = 20, bottom = 20, left = 20 }, + positioning = "absolute" + }) + + local child = FlexLove.new({ + id = "child", + parent = container, + positioning = "absolute", + top = 10, + left = 10, + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Offsets should be relative to content area (after padding) + local expectedX = container.x + container.padding.left + 10 + local expectedY = container.y + container.padding.top + 10 + + luaunit.assertEquals(child.x, expectedX, "Left offset should account for container padding") + luaunit.assertEquals(child.y, expectedY, "Top offset should account for container padding") +end + +-- Test: CSS positioning should NOT affect flex children +function TestLayoutEdgeCases:test_css_positioning_ignored_in_flex() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 400, + height = 400, + positioning = "flex", + flexDirection = "horizontal" + }) + + local child = FlexLove.new({ + id = "child", + parent = container, + top = 100, -- This should be IGNORED in flex layout + left = 100, -- This should be IGNORED in flex layout + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- In flex layout, child should be positioned by flex rules, not CSS offsets + -- Child should be at (0, 0) relative to container content area + luaunit.assertEquals(child.x, 0, "CSS offsets should be ignored in flex layout") + luaunit.assertEquals(child.y, 0, "CSS offsets should be ignored in flex layout") +end + +-- Test: CSS positioning in relative container +function TestLayoutEdgeCases:test_css_positioning_in_relative_container() + local container = FlexLove.new({ + id = "container", + x = 100, + y = 100, + width = 400, + height = 400, + positioning = "relative" + }) + + local child = FlexLove.new({ + id = "child", + parent = container, + positioning = "absolute", + top = 30, + left = 30, + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame() + + -- Should work the same as absolute container + local expectedX = container.x + container.padding.left + 30 + local expectedY = container.y + container.padding.top + 30 + + luaunit.assertEquals(child.x, expectedX, "CSS positioning should work in relative containers") + luaunit.assertEquals(child.y, expectedY, "CSS positioning should work in relative containers") +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/overflow_test.lua b/testing/__tests__/overflow_test.lua new file mode 100644 index 0000000..6c50e21 --- /dev/null +++ b/testing/__tests__/overflow_test.lua @@ -0,0 +1,345 @@ +-- Test suite for overflow detection and scroll behavior +-- This tests the critical ScrollManager.detectOverflow() path which is currently 0% covered +package.path = package.path .. ";./?.lua;./modules/?.lua" + +require("testing.loveStub") +local luaunit = require("testing.luaunit") +local FlexLove = require("FlexLove") + +TestOverflowDetection = {} + +function TestOverflowDetection:setUp() + FlexLove.beginFrame(1920, 1080) +end + +function TestOverflowDetection:tearDown() + FlexLove.endFrame() +end + +-- Test basic overflow detection when content exceeds container +function TestOverflowDetection:test_vertical_overflow_detected() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 200, + height = 100, + overflow = "scroll" + }) + + -- Add child that exceeds container height + FlexLove.new({ + id = "tall_child", + parent = container, + x = 0, + y = 0, + width = 100, + height = 200 -- Taller than container (100) + }) + + -- Force layout to trigger detectOverflow + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Check if overflow was detected + local maxScrollX, maxScrollY = container:getMaxScroll() + luaunit.assertTrue(maxScrollY > 0, "Should detect vertical overflow") + luaunit.assertEquals(maxScrollX, 0, "Should not have horizontal overflow") +end + +function TestOverflowDetection:test_horizontal_overflow_detected() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 100, + height = 200, + overflow = "scroll" + }) + + -- Add child that exceeds container width + FlexLove.new({ + id = "wide_child", + parent = container, + x = 0, + y = 0, + width = 300, -- Wider than container (100) + height = 50 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + local maxScrollX, maxScrollY = container:getMaxScroll() + luaunit.assertTrue(maxScrollX > 0, "Should detect horizontal overflow") + luaunit.assertEquals(maxScrollY, 0, "Should not have vertical overflow") +end + +function TestOverflowDetection:test_both_axes_overflow() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 100, + height = 100, + overflow = "scroll" + }) + + -- Add child that exceeds both dimensions + FlexLove.new({ + id = "large_child", + parent = container, + x = 0, + y = 0, + width = 200, + height = 200 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + local maxScrollX, maxScrollY = container:getMaxScroll() + luaunit.assertTrue(maxScrollX > 0, "Should detect horizontal overflow") + luaunit.assertTrue(maxScrollY > 0, "Should detect vertical overflow") +end + +function TestOverflowDetection:test_no_overflow_when_content_fits() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 200, + height = 200, + overflow = "scroll" + }) + + -- Add child that fits within container + FlexLove.new({ + id = "small_child", + parent = container, + x = 0, + y = 0, + width = 100, + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + local maxScrollX, maxScrollY = container:getMaxScroll() + luaunit.assertEquals(maxScrollX, 0, "Should not have horizontal overflow") + luaunit.assertEquals(maxScrollY, 0, "Should not have vertical overflow") +end + +function TestOverflowDetection:test_overflow_with_multiple_children() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 200, + height = 200, + overflow = "scroll", + positioning = "flex", + flexDirection = "vertical" + }) + + -- Add multiple children that together exceed container + for i = 1, 5 do + FlexLove.new({ + id = "child_" .. i, + parent = container, + width = 150, + height = 60 -- 5 * 60 = 300, exceeds container height of 200 + }) + end + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + local maxScrollX, maxScrollY = container:getMaxScroll() + luaunit.assertTrue(maxScrollY > 0, "Should detect overflow from multiple children") +end + +function TestOverflowDetection:test_overflow_with_padding() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 200, + height = 200, + padding = { top = 10, right = 10, bottom = 10, left = 10 }, + overflow = "scroll" + }) + + -- Child that fits in container but exceeds available content area (200 - 20 = 180) + FlexLove.new({ + id = "child", + parent = container, + x = 0, + y = 0, + width = 190, -- Exceeds content width (180) + height = 100 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + local maxScrollX, maxScrollY = container:getMaxScroll() + luaunit.assertTrue(maxScrollX > 0, "Should detect overflow accounting for padding") +end + +function TestOverflowDetection:test_overflow_with_margins() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 200, + height = 200, + positioning = "flex", + flexDirection = "horizontal", + overflow = "scroll" + }) + + -- Child with margins that contribute to overflow + -- In flex layout, margins are properly accounted for in positioning + FlexLove.new({ + id = "child", + parent = container, + width = 180, + height = 180, + margin = { top = 5, right = 20, bottom = 5, left = 5 } -- Total width: 5+180+20=205, overflows 200px container + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + local maxScrollX, maxScrollY = container:getMaxScroll() + luaunit.assertTrue(maxScrollX > 0, "Should include child margins in overflow calculation") +end + +-- Test edge case: overflow = "visible" should skip detection +function TestOverflowDetection:test_visible_overflow_skips_detection() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 100, + height = 100, + overflow = "visible" -- Should not clip or calculate overflow + }) + + -- Add oversized child + FlexLove.new({ + id = "large_child", + parent = container, + x = 0, + y = 0, + width = 300, + height = 300 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- With overflow="visible", maxScroll should be 0 (no scrolling) + local maxScrollX, maxScrollY = container:getMaxScroll() + luaunit.assertEquals(maxScrollX, 0, "visible overflow should not enable scrolling") + luaunit.assertEquals(maxScrollY, 0, "visible overflow should not enable scrolling") +end + +-- Test edge case: empty container +function TestOverflowDetection:test_empty_container_no_overflow() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 200, + height = 200, + overflow = "scroll" + -- No children + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + local maxScrollX, maxScrollY = container:getMaxScroll() + luaunit.assertEquals(maxScrollX, 0, "Empty container should have no overflow") + luaunit.assertEquals(maxScrollY, 0, "Empty container should have no overflow") +end + +-- Test overflow with absolutely positioned children (should be ignored) +function TestOverflowDetection:test_absolute_children_ignored_in_overflow() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 200, + height = 200, + overflow = "scroll" + }) + + -- Regular child that fits + FlexLove.new({ + id = "normal_child", + parent = container, + x = 0, + y = 0, + width = 150, + height = 150 + }) + + -- Absolutely positioned child that extends beyond (should NOT cause overflow) + FlexLove.new({ + id = "absolute_child", + parent = container, + positioning = "absolute", + top = 0, + left = 0, + width = 400, + height = 400 + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + local maxScrollX, maxScrollY = container:getMaxScroll() + -- Should not have overflow because absolute children are ignored + luaunit.assertEquals(maxScrollX, 0, "Absolute children should not cause overflow") + luaunit.assertEquals(maxScrollY, 0, "Absolute children should not cause overflow") +end + +-- Test scroll clamping with overflow +function TestOverflowDetection:test_scroll_clamped_to_max() + local container = FlexLove.new({ + id = "container", + x = 0, + y = 0, + width = 100, + height = 100, + overflow = "scroll" + }) + + FlexLove.new({ + id = "child", + parent = container, + x = 0, + y = 0, + width = 100, + height = 300 -- Creates 200px of vertical overflow + }) + + FlexLove.endFrame() + FlexLove.beginFrame(1920, 1080) + + -- Try to scroll beyond max + container:setScrollPosition(0, 999999) + local scrollX, scrollY = container:getScrollPosition() + local maxScrollX, maxScrollY = container:getMaxScroll() + + luaunit.assertEquals(scrollY, maxScrollY, "Scroll should be clamped to maximum") + luaunit.assertTrue(scrollY < 999999, "Should not scroll beyond content") +end + +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/runAll.lua b/testing/runAll.lua index 77732ac..71ab85e 100644 --- a/testing/runAll.lua +++ b/testing/runAll.lua @@ -25,6 +25,9 @@ local testFiles = { "testing/__tests__/theme_test.lua", "testing/__tests__/layout_engine_test.lua", "testing/__tests__/element_test.lua", + "testing/__tests__/overflow_test.lua", + "testing/__tests__/grid_test.lua", + "testing/__tests__/layout_edge_cases_test.lua", } local success = true