From bf9e180be1118be9dc7210ef8d2b7dac31b63d1d Mon Sep 17 00:00:00 2001 From: atul-upadhyay-7 Date: Sat, 20 Jun 2026 01:26:33 +0530 Subject: [PATCH] fix(core): add iteration bound to grid auto-placement to prevent infinite loop The grid auto-placement algorithm used a while(true) loop with no termination bound. When a child's rowSpan exceeded available space or all slots were occupied, the loop ran forever, hanging the application. Add a maximum row limit (numCols * 100, minimum 1000) so the loop terminates. Children that cannot fit are silently skipped rather than causing an infinite loop. Closes #1661 --- packages/core/src/layout/LayoutEngine.test.ts | 66 +++++++++++++++++++ packages/core/src/layout/LayoutEngine.ts | 5 +- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/core/src/layout/LayoutEngine.test.ts b/packages/core/src/layout/LayoutEngine.test.ts index 8a15511f..dcc19b15 100644 --- a/packages/core/src/layout/LayoutEngine.test.ts +++ b/packages/core/src/layout/LayoutEngine.test.ts @@ -225,3 +225,69 @@ describe('border offset in LayoutEngine', () => { expect(root.children[0].computed.height).toBe(6); }); }); + +describe('CSS Grid auto-placement', () => { + it('places children in a 2-column grid', () => { + const root = makeNode('root', { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + }, [ + makeNode('a', { height: 3 }), + makeNode('b', { height: 3 }), + makeNode('c', { height: 3 }), + ]); + computeLayout(root, 20, 10); + + expect(root.children[0].computed.x).toBe(0); + expect(root.children[0].computed.y).toBe(0); + expect(root.children[1].computed.x).toBe(10); + expect(root.children[1].computed.y).toBe(0); + expect(root.children[2].computed.x).toBe(0); + // Row heights are resolved from gridTemplateRows (1fr default), + // so the third child lands at the start of row 1. + expect(root.children[2].computed.y).toBeGreaterThan(0); + }); + + it('terminates when rowSpan exceeds available space', () => { + const children = []; + for (let i = 0; i < 5; i++) { + children.push(makeNode(`c${i}`, { height: 10 })); + } + const root = makeNode('root', { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + }, children); + + expect(() => computeLayout(root, 20, 10)).not.toThrow(); + }); + + it('does not loop forever when grid is completely full', () => { + const root = makeNode('root', { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + }, [ + makeNode('a', { gridRowStart: 1, gridRowEnd: 3, gridColumnStart: 1, gridColumnEnd: 2, height: 4 }), + makeNode('b', { gridRowStart: 1, gridRowEnd: 3, gridColumnStart: 2, gridColumnEnd: 3, height: 4 }), + makeNode('c', { height: 2 }), + makeNode('d', { height: 2 }), + ]); + + expect(() => computeLayout(root, 20, 10)).not.toThrow(); + }); + + it('handles explicit grid positions alongside auto-placement', () => { + const root = makeNode('root', { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + }, [ + makeNode('a', { gridRowStart: 1, gridColumnStart: 1, height: 3 }), + makeNode('b', { height: 3 }), + ]); + computeLayout(root, 20, 10); + + expect(root.children[0].computed.x).toBe(0); + expect(root.children[0].computed.y).toBe(0); + expect(root.children[1].computed.x).toBe(10); + expect(root.children[1].computed.y).toBe(0); + }); +}); diff --git a/packages/core/src/layout/LayoutEngine.ts b/packages/core/src/layout/LayoutEngine.ts index f6b9f919..a6905a12 100644 --- a/packages/core/src/layout/LayoutEngine.ts +++ b/packages/core/src/layout/LayoutEngine.ts @@ -210,6 +210,7 @@ function layoutNode(node: LayoutNode, availWidth: number, availHeight: number, p let currentAutoRow = 0; let currentAutoCol = 0; + const maxAutoRow = Math.max(numCols * 100, 1000); for (const child of autoChildren) { const s = child.style; @@ -218,7 +219,8 @@ function layoutNode(node: LayoutNode, availWidth: number, availHeight: number, p const clampedColSpan = Math.max(1, Math.min(colInfo.span, numCols)); - while (true) { + let placed = false; + while (currentAutoRow < maxAutoRow) { let available = true; for (let r = currentAutoRow; r < currentAutoRow + rowInfo.span; r++) { for (let c = currentAutoCol; c < currentAutoCol + clampedColSpan; c++) { @@ -254,6 +256,7 @@ function layoutNode(node: LayoutNode, availWidth: number, availHeight: number, p currentAutoCol = 0; currentAutoRow++; } + placed = true; break; } else { currentAutoCol++;