- Move render.test.ts into focused JSX builder, tree render, grid layout, and text prop files - Preserve all 56 render tests and keep scoped non-null assertion enforcement on the new folder - Validate the split with the render/jsx test folder and the full check command
355 lines
11 KiB
TypeScript
355 lines
11 KiB
TypeScript
import { describe, expect, it } from 'bun:test'
|
|
|
|
import {
|
|
renderTree,
|
|
renderJSX,
|
|
computeLayout,
|
|
computeAllLayouts,
|
|
Frame,
|
|
Rectangle
|
|
} from '@open-pencil/core'
|
|
|
|
import { expectDefined, getNodeOrThrow } from '../../../helpers/assert'
|
|
import { makeSceneGraph } from '../../../helpers/scene'
|
|
|
|
describe('grid layout rendering', () => {
|
|
it('creates grid frame with template columns', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame(
|
|
{ name: 'Grid', w: 300, h: 200, grid: true, columns: '1fr 1fr 1fr' },
|
|
Rectangle({ name: 'A', w: 50, h: 50 }),
|
|
Rectangle({ name: 'B', w: 50, h: 50 }),
|
|
Rectangle({ name: 'C', w: 50, h: 50 })
|
|
)
|
|
const result = await renderTree(g, tree)
|
|
const frame = getNodeOrThrow(g, result.id)
|
|
|
|
expect(frame.layoutMode).toBe('GRID')
|
|
expect(frame.gridTemplateColumns).toEqual([
|
|
{ sizing: 'FR', value: 1 },
|
|
{ sizing: 'FR', value: 1 },
|
|
{ sizing: 'FR', value: 1 }
|
|
])
|
|
})
|
|
|
|
it('creates grid frame with mixed track sizes', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame({ name: 'Grid', w: 400, h: 200, grid: true, columns: '100 1fr 2fr' })
|
|
const result = await renderTree(g, tree)
|
|
const frame = getNodeOrThrow(g, result.id)
|
|
|
|
expect(frame.gridTemplateColumns).toEqual([
|
|
{ sizing: 'FIXED', value: 100 },
|
|
{ sizing: 'FR', value: 1 },
|
|
{ sizing: 'FR', value: 2 }
|
|
])
|
|
})
|
|
|
|
it('creates grid with numeric columns shorthand', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame({ name: 'Grid', w: 300, h: 200, grid: true, columns: 3 })
|
|
const result = await renderTree(g, tree)
|
|
const frame = getNodeOrThrow(g, result.id)
|
|
|
|
expect(frame.gridTemplateColumns).toEqual([
|
|
{ sizing: 'FR', value: 1 },
|
|
{ sizing: 'FR', value: 1 },
|
|
{ sizing: 'FR', value: 1 }
|
|
])
|
|
})
|
|
|
|
it('sets grid gaps', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame({
|
|
name: 'Grid',
|
|
w: 300,
|
|
h: 200,
|
|
grid: true,
|
|
columns: '1fr 1fr',
|
|
columnGap: 10,
|
|
rowGap: 20
|
|
})
|
|
const result = await renderTree(g, tree)
|
|
const frame = getNodeOrThrow(g, result.id)
|
|
|
|
expect(frame.gridColumnGap).toBe(10)
|
|
expect(frame.gridRowGap).toBe(20)
|
|
})
|
|
|
|
it('sets grid gap shorthand', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame({ name: 'Grid', w: 300, h: 200, grid: true, columns: '1fr 1fr', gap: 16 })
|
|
const result = await renderTree(g, tree)
|
|
const frame = getNodeOrThrow(g, result.id)
|
|
|
|
expect(frame.gridColumnGap).toBe(16)
|
|
expect(frame.gridRowGap).toBe(16)
|
|
})
|
|
|
|
it('applies grid padding', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame({ name: 'Grid', w: 300, h: 200, grid: true, columns: '1fr 1fr', p: 10 })
|
|
const result = await renderTree(g, tree)
|
|
const frame = getNodeOrThrow(g, result.id)
|
|
|
|
expect(frame.paddingTop).toBe(10)
|
|
expect(frame.paddingRight).toBe(10)
|
|
expect(frame.paddingBottom).toBe(10)
|
|
expect(frame.paddingLeft).toBe(10)
|
|
})
|
|
|
|
it('sets grid rows', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame({
|
|
name: 'Grid',
|
|
w: 300,
|
|
h: 200,
|
|
grid: true,
|
|
columns: '1fr',
|
|
rows: '100 1fr'
|
|
})
|
|
const result = await renderTree(g, tree)
|
|
const frame = getNodeOrThrow(g, result.id)
|
|
|
|
expect(frame.gridTemplateRows).toEqual([
|
|
{ sizing: 'FIXED', value: 100 },
|
|
{ sizing: 'FR', value: 1 }
|
|
])
|
|
})
|
|
|
|
it('sets grid child position', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame(
|
|
{ name: 'Grid', w: 300, h: 200, grid: true, columns: '1fr 1fr 1fr', rows: '1fr 1fr' },
|
|
Rectangle({ name: 'A', w: 50, h: 50, colStart: 2, rowStart: 1 })
|
|
)
|
|
const result = await renderTree(g, tree)
|
|
const children = g.getChildren(result.id)
|
|
|
|
expect(children[0].gridPosition).toEqual({ column: 2, row: 1, columnSpan: 1, rowSpan: 1 })
|
|
})
|
|
|
|
it('sets grid child span', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame(
|
|
{ name: 'Grid', w: 300, h: 200, grid: true, columns: '1fr 1fr 1fr', rows: '1fr 1fr' },
|
|
Rectangle({ name: 'A', w: 50, h: 50, colStart: 1, colSpan: 2, rowStart: 1, rowSpan: 2 })
|
|
)
|
|
const result = await renderTree(g, tree)
|
|
const children = g.getChildren(result.id)
|
|
|
|
expect(children[0].gridPosition).toEqual({ column: 1, row: 1, columnSpan: 2, rowSpan: 2 })
|
|
})
|
|
|
|
it('grid + computeLayout positions children in cells', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame(
|
|
{ name: 'Grid', w: 300, h: 200, grid: true, columns: '1fr 1fr 1fr', rows: '1fr' },
|
|
Rectangle({ name: 'A', w: 50, h: 50 }),
|
|
Rectangle({ name: 'B', w: 50, h: 50 }),
|
|
Rectangle({ name: 'C', w: 50, h: 50 })
|
|
)
|
|
const result = await renderTree(g, tree)
|
|
computeLayout(g, result.id)
|
|
|
|
const children = g.getChildren(result.id)
|
|
expect(children[0].x).toBe(0)
|
|
expect(children[1].x).toBe(100)
|
|
expect(children[2].x).toBe(200)
|
|
})
|
|
|
|
it('grid 2x2 with gap positions correctly', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame(
|
|
{ name: 'Grid', w: 210, h: 210, grid: true, columns: '1fr 1fr', rows: '1fr 1fr', gap: 10 },
|
|
Rectangle({ name: 'A', w: 30, h: 30 }),
|
|
Rectangle({ name: 'B', w: 30, h: 30 }),
|
|
Rectangle({ name: 'C', w: 30, h: 30 }),
|
|
Rectangle({ name: 'D', w: 30, h: 30 })
|
|
)
|
|
const result = await renderTree(g, tree)
|
|
computeLayout(g, result.id)
|
|
|
|
const children = g.getChildren(result.id)
|
|
expect(children[0].x).toBe(0)
|
|
expect(children[0].y).toBe(0)
|
|
expect(children[1].x).toBe(110)
|
|
expect(children[1].y).toBe(0)
|
|
expect(children[2].x).toBe(0)
|
|
expect(children[2].y).toBe(110)
|
|
expect(children[3].x).toBe(110)
|
|
expect(children[3].y).toBe(110)
|
|
})
|
|
|
|
it('renders grid from JSX string', async () => {
|
|
const g = makeSceneGraph()
|
|
const jsx = `
|
|
<Frame name="Grid" w={300} h={100} grid columns="1fr 1fr" gap={10}>
|
|
<Rectangle name="A" w={50} h={50} />
|
|
<Rectangle name="B" w={50} h={50} />
|
|
</Frame>
|
|
`
|
|
const [result] = await renderJSX(g, jsx)
|
|
const frame = getNodeOrThrow(g, result.id)
|
|
|
|
expect(frame.layoutMode).toBe('GRID')
|
|
expect(frame.gridTemplateColumns).toEqual([
|
|
{ sizing: 'FR', value: 1 },
|
|
{ sizing: 'FR', value: 1 }
|
|
])
|
|
expect(frame.gridColumnGap).toBe(10)
|
|
expect(frame.gridRowGap).toBe(10)
|
|
|
|
computeLayout(g, result.id)
|
|
const children = g.getChildren(result.id)
|
|
expect(children[0].x).toBe(0)
|
|
expect(children[1].x).toBe(155)
|
|
})
|
|
|
|
it('grid auto-height (no rows) grows to fit content', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame(
|
|
{ name: 'Grid', w: 300, grid: true, columns: '1fr 1fr', gap: 10 },
|
|
Rectangle({ name: 'A', w: 50, h: 80 }),
|
|
Rectangle({ name: 'B', w: 50, h: 80 }),
|
|
Rectangle({ name: 'C', w: 50, h: 60 }),
|
|
Rectangle({ name: 'D', w: 50, h: 60 })
|
|
)
|
|
const result = await renderTree(g, tree)
|
|
const frame = getNodeOrThrow(g, result.id)
|
|
expect(frame.gridTemplateRows).toEqual([])
|
|
|
|
computeLayout(g, result.id)
|
|
const updated = getNodeOrThrow(g, result.id)
|
|
expect(updated.height).toBe(150)
|
|
})
|
|
|
|
it('grid children with flex stretch to cell width', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame(
|
|
{ name: 'Grid', w: 200, grid: true, columns: '1fr 1fr', gap: 0 },
|
|
Frame({
|
|
name: 'A',
|
|
flex: 'col',
|
|
gap: 4,
|
|
p: 8,
|
|
children: [Rectangle({ name: 'R1', w: 10, h: 20 })]
|
|
}),
|
|
Frame({
|
|
name: 'B',
|
|
flex: 'col',
|
|
gap: 4,
|
|
p: 8,
|
|
children: [Rectangle({ name: 'R2', w: 10, h: 20 })]
|
|
})
|
|
)
|
|
const result = await renderTree(g, tree)
|
|
computeAllLayouts(g)
|
|
|
|
const children = g.getChildren(result.id)
|
|
expect(children[0].width).toBe(100)
|
|
expect(children[1].width).toBe(100)
|
|
expect(children[0].x).toBe(0)
|
|
expect(children[1].x).toBe(100)
|
|
})
|
|
|
|
it('grid prop takes precedence over padding-triggered auto-layout', async () => {
|
|
const g = makeSceneGraph()
|
|
const tree = Frame(
|
|
{ name: 'Grid', w: 300, grid: true, columns: '1fr 1fr', p: 20 },
|
|
Rectangle({ name: 'A', w: 50, h: 50 }),
|
|
Rectangle({ name: 'B', w: 50, h: 50 })
|
|
)
|
|
const result = await renderTree(g, tree)
|
|
const frame = getNodeOrThrow(g, result.id)
|
|
expect(frame.layoutMode).toBe('GRID')
|
|
})
|
|
|
|
it('grid w=fill stretches in flex-col parent', async () => {
|
|
const g = makeSceneGraph()
|
|
const jsx = `
|
|
<Frame name="P" flex="col" w={400} p={20}>
|
|
<Frame name="G" grid columns="1fr 1fr" gap={10} w="fill">
|
|
<Rectangle w={10} h={30} />
|
|
<Rectangle w={10} h={30} />
|
|
</Frame>
|
|
</Frame>
|
|
`
|
|
const [result] = await renderJSX(g, jsx)
|
|
computeAllLayouts(g)
|
|
const grid = expectDefined(
|
|
g.getChildren(result.id).find((c) => c.name === 'G'),
|
|
'grid node'
|
|
)
|
|
expect(grid.width).toBe(360)
|
|
expect(grid.layoutMode).toBe('GRID')
|
|
})
|
|
|
|
it('grid grow=1 fills remaining space in flex-row', async () => {
|
|
const g = makeSceneGraph()
|
|
const jsx = `
|
|
<Frame name="P" flex="row" w={500} p={20} gap={10}>
|
|
<Rectangle w={100} h={50} />
|
|
<Frame name="G" grid columns="1fr 1fr" gap={10} grow={1}>
|
|
<Rectangle w={10} h={30} />
|
|
<Rectangle w={10} h={30} />
|
|
</Frame>
|
|
</Frame>
|
|
`
|
|
const [result] = await renderJSX(g, jsx)
|
|
computeAllLayouts(g)
|
|
const grid = expectDefined(
|
|
g.getChildren(result.id).find((c) => c.name === 'G'),
|
|
'grid node'
|
|
)
|
|
expect(grid.width).toBe(350)
|
|
})
|
|
|
|
it('nested flex > flex > grid: grid stretches to correct width', async () => {
|
|
const g = makeSceneGraph()
|
|
const jsx = `
|
|
<Frame name="Card" flex="col" w={780}>
|
|
<Frame name="Content" flex="col" w="fill" p={28}>
|
|
<Frame name="G" grid columns="1fr 1fr 1fr" w="fill" columnGap={16}>
|
|
<Rectangle w={10} h={30} />
|
|
<Rectangle w={10} h={30} />
|
|
<Rectangle w={10} h={30} />
|
|
</Frame>
|
|
</Frame>
|
|
</Frame>
|
|
`
|
|
const [result] = await renderJSX(g, jsx)
|
|
computeAllLayouts(g)
|
|
const content = expectDefined(
|
|
g.getChildren(result.id).find((c) => c.name === 'Content'),
|
|
'content node'
|
|
)
|
|
const grid = expectDefined(
|
|
g.getChildren(content.id).find((c) => c.name === 'G'),
|
|
'grid node'
|
|
)
|
|
expect(content.width).toBe(780)
|
|
expect(grid.width).toBe(724)
|
|
})
|
|
|
|
it('grid h=fill grows in flex-col parent', async () => {
|
|
const g = makeSceneGraph()
|
|
const jsx = `
|
|
<Frame name="P" flex="col" w={400} h={600} p={20}>
|
|
<Rectangle w={360} h={100} />
|
|
<Frame name="G" grid columns="1fr 1fr" gap={10} h="fill">
|
|
<Rectangle w={10} h={30} />
|
|
<Rectangle w={10} h={30} />
|
|
</Frame>
|
|
</Frame>
|
|
`
|
|
const [result] = await renderJSX(g, jsx)
|
|
computeAllLayouts(g)
|
|
const grid = expectDefined(
|
|
g.getChildren(result.id).find((c) => c.name === 'G'),
|
|
'grid node'
|
|
)
|
|
expect(grid.height).toBe(460)
|
|
})
|
|
})
|