243 lines
7.5 KiB
TypeScript
243 lines
7.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { type GridTrackSize, layoutGridAreas, layoutWidgetGrid, resolveGridTracks } from '../lib/widgetGrid.js'
|
|
|
|
describe('layoutWidgetGrid', () => {
|
|
it('falls back to a single column on narrow widths', () => {
|
|
const layout = layoutWidgetGrid({
|
|
items: [{ id: 'a' }, { id: 'b' }],
|
|
maxColumns: 3,
|
|
minColumnWidth: 40,
|
|
width: 35
|
|
})
|
|
|
|
expect(layout.columnCount).toBe(1)
|
|
expect(layout.columns).toEqual([35])
|
|
expect(layout.rows).toEqual([[{ col: 0, id: 'a', span: 1, width: 35 }], [{ col: 0, id: 'b', span: 1, width: 35 }]])
|
|
})
|
|
|
|
it('packs spans left-to-right and wraps to the next row', () => {
|
|
const layout = layoutWidgetGrid({
|
|
gap: 2,
|
|
items: [
|
|
{ id: 'a', span: 1 },
|
|
{ id: 'b', span: 2 },
|
|
{ id: 'c', span: 1 }
|
|
],
|
|
maxColumns: 3,
|
|
minColumnWidth: 30,
|
|
width: 100
|
|
})
|
|
|
|
expect(layout.columnCount).toBe(3)
|
|
expect(layout.columns).toEqual([32, 32, 32])
|
|
expect(layout.rows).toEqual([
|
|
[
|
|
{ col: 0, id: 'a', span: 1, width: 32 },
|
|
{ col: 1, id: 'b', span: 2, width: 66 }
|
|
],
|
|
[{ col: 0, id: 'c', span: 1, width: 32 }]
|
|
])
|
|
})
|
|
|
|
it('clamps spans to available columns', () => {
|
|
const layout = layoutWidgetGrid({
|
|
gap: 1,
|
|
items: [{ id: 'huge', span: 9 }],
|
|
maxColumns: 2,
|
|
minColumnWidth: 20,
|
|
width: 50
|
|
})
|
|
|
|
expect(layout.columnCount).toBe(2)
|
|
expect(layout.rows[0]?.[0]).toEqual({
|
|
col: 0,
|
|
id: 'huge',
|
|
span: 2,
|
|
width: 50
|
|
})
|
|
})
|
|
|
|
it('honors an exact column count when the grid has room', () => {
|
|
const layout = layoutWidgetGrid({
|
|
columns: 4,
|
|
gap: 1,
|
|
items: Array.from({ length: 8 }, (_, idx) => ({ id: `cell-${idx}` })),
|
|
width: 43
|
|
})
|
|
|
|
expect(layout.columnCount).toBe(4)
|
|
expect(layout.columns).toEqual([10, 10, 10, 10])
|
|
expect(layout.rows).toHaveLength(2)
|
|
})
|
|
|
|
it('renders sparse explicit starts without collapsing holes', () => {
|
|
const layout = layoutWidgetGrid({
|
|
columns: 4,
|
|
gap: 1,
|
|
items: [
|
|
{ colStart: 0, id: 'a' },
|
|
{ colStart: 2, id: 'b' },
|
|
{ colStart: 3, id: 'c' }
|
|
],
|
|
width: 43
|
|
})
|
|
|
|
expect(layout.rows).toEqual([
|
|
[
|
|
{ col: 0, id: 'a', span: 1, width: 10 },
|
|
{ col: 2, id: 'b', span: 1, width: 10 },
|
|
{ col: 3, id: 'c', span: 1, width: 10 }
|
|
]
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('resolveGridTracks', () => {
|
|
it('splits equal fr tracks with the remainder spread left-to-right', () => {
|
|
expect(resolveGridTracks(10, 0, [{ fr: 1 }, { fr: 1 }, { fr: 1 }])).toEqual([4, 3, 3])
|
|
})
|
|
|
|
it('gives fixed tracks their size and shares the leftover by weight', () => {
|
|
// usable = 40 - 2*1 = 38; fixed 10; leftover 28 over 1fr+2fr.
|
|
expect(resolveGridTracks(40, 1, [10, { fr: 1 }, { fr: 2 }])).toEqual([10, 10, 18])
|
|
})
|
|
|
|
it('pins tracks that fall below their min and re-solves the rest', () => {
|
|
expect(resolveGridTracks(20, 0, [{ fr: 1 }, { fr: 1, min: 15 }])).toEqual([5, 15])
|
|
})
|
|
|
|
it('never lets the sum exceed the usable axis when tracks fit at their minimums', () => {
|
|
const cases: Array<{ gap: number; total: number; tracks: GridTrackSize[] }> = [
|
|
{ gap: 1, total: 80, tracks: [12, { fr: 1 }, { fr: 3, min: 20 }, 6] },
|
|
{ gap: 2, total: 33, tracks: [{ fr: 1 }, { fr: 1 }, { fr: 1 }, { fr: 1 }] },
|
|
{ gap: 0, total: 9, tracks: [4, 4, { fr: 1 }] }
|
|
]
|
|
|
|
for (const { gap, total, tracks } of cases) {
|
|
const sizes = resolveGridTracks(total, gap, tracks)
|
|
const usable = total - gap * (tracks.length - 1)
|
|
|
|
expect(sizes.reduce((acc, s) => acc + s, 0)).toBeLessThanOrEqual(Math.max(tracks.length, usable))
|
|
expect(sizes.every(s => s >= 1)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('shaves overflow off trailing tracks when fixed sizes exceed the axis', () => {
|
|
const sizes = resolveGridTracks(20, 0, [15, 15, { fr: 1 }])
|
|
|
|
expect(sizes.reduce((acc, s) => acc + s, 0)).toBeLessThanOrEqual(20)
|
|
expect(sizes[0]).toBe(15)
|
|
expect(sizes.every(s => s >= 1)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('layoutGridAreas', () => {
|
|
it('solves a rowSpan cell so it occupies both rows and blocks placement beneath it', () => {
|
|
const layout = layoutGridAreas({
|
|
columns: 2,
|
|
gap: 0,
|
|
height: 10,
|
|
items: [{ id: 'tall', rowSpan: 2 }, { id: 'b' }, { id: 'c' }],
|
|
rowGap: 0,
|
|
width: 20
|
|
})
|
|
|
|
const [tall, b, c] = layout.cells
|
|
|
|
expect(tall).toMatchObject({ col: 0, height: 10, row: 0, rowSpan: 2, width: 10, x: 0, y: 0 })
|
|
expect(b).toMatchObject({ col: 1, row: 0, x: 10, y: 0 })
|
|
// c cannot go under `tall` — it lands in the second row's free column.
|
|
expect(c).toMatchObject({ col: 1, row: 1, x: 10, y: 5 })
|
|
})
|
|
|
|
it('fills holes densely (auto-flow dense) after a colSpan pushes a wrap', () => {
|
|
const layout = layoutGridAreas({
|
|
columns: 3,
|
|
gap: 0,
|
|
height: 6,
|
|
items: [{ id: 'a' }, { colSpan: 2, id: 'wide' }, { id: 'filler' }],
|
|
width: 30
|
|
})
|
|
|
|
const byId = Object.fromEntries(layout.cells.map(cell => [cell.id, cell]))
|
|
|
|
expect(byId['a']).toMatchObject({ col: 0, row: 0 })
|
|
expect(byId['wide']).toMatchObject({ col: 1, colSpan: 2, row: 0 })
|
|
expect(byId['filler']).toMatchObject({ col: 0, row: 1 })
|
|
})
|
|
|
|
it('spans include the gaps they bridge', () => {
|
|
const layout = layoutGridAreas({
|
|
columns: [{ fr: 1 }, { fr: 1 }, { fr: 1 }],
|
|
gap: 2,
|
|
height: 11,
|
|
items: [{ colSpan: 3, id: 'full' }, { id: 'a' }, { id: 'b' }, { rowSpan: 2, id: 'tall' }],
|
|
rowGap: 1,
|
|
width: 32
|
|
})
|
|
|
|
const byId = Object.fromEntries(layout.cells.map(cell => [cell.id, cell]))
|
|
|
|
// 3 tracks over usable 28 → [10, 9, 9]; full spans all three + two gaps.
|
|
expect(layout.columnSizes).toEqual([10, 9, 9])
|
|
expect(byId['full']!.width).toBe(32)
|
|
// 3 rows over usable 9 → [3, 3, 3]; tall spans rows 2-3 + one rowGap.
|
|
expect(layout.rowSizes).toEqual([3, 3, 3])
|
|
expect(byId['tall']!.height).toBe(7)
|
|
expect(byId['tall']!.y).toBe(4)
|
|
})
|
|
|
|
it('honors explicit col/row pins and grows implicit rows beneath explicit tracks', () => {
|
|
const layout = layoutGridAreas({
|
|
columns: 2,
|
|
gap: 0,
|
|
height: 12,
|
|
items: [{ col: 1, id: 'pinned', row: 2 }, { id: 'auto-a' }, { id: 'auto-b' }],
|
|
rowGap: 0,
|
|
rows: 2,
|
|
width: 10
|
|
})
|
|
|
|
const byId = Object.fromEntries(layout.cells.map(cell => [cell.id, cell]))
|
|
|
|
// The pin forces a third row beyond the two explicit tracks.
|
|
expect(layout.rowCount).toBe(3)
|
|
expect(byId['pinned']).toMatchObject({ col: 1, row: 2 })
|
|
expect(byId['auto-a']).toMatchObject({ col: 0, row: 0 })
|
|
expect(byId['auto-b']).toMatchObject({ col: 1, row: 0 })
|
|
})
|
|
|
|
it('supports weighted and fixed row tracks', () => {
|
|
const layout = layoutGridAreas({
|
|
columns: 1,
|
|
gap: 0,
|
|
height: 20,
|
|
items: [{ id: 'header' }, { id: 'body' }, { id: 'footer' }],
|
|
rowGap: 0,
|
|
rows: [3, { fr: 1 }, 3],
|
|
width: 40
|
|
})
|
|
|
|
expect(layout.rowSizes).toEqual([3, 14, 3])
|
|
|
|
const byId = Object.fromEntries(layout.cells.map(cell => [cell.id, cell]))
|
|
|
|
expect(byId['header']!.height).toBe(3)
|
|
expect(byId['body']).toMatchObject({ height: 14, y: 3 })
|
|
expect(byId['footer']).toMatchObject({ height: 3, y: 17 })
|
|
})
|
|
|
|
it('clamps colSpan to the column count', () => {
|
|
const layout = layoutGridAreas({
|
|
columns: 2,
|
|
gap: 0,
|
|
height: 4,
|
|
items: [{ colSpan: 9, id: 'huge' }],
|
|
width: 10
|
|
})
|
|
|
|
expect(layout.cells[0]).toMatchObject({ col: 0, colSpan: 2, width: 10 })
|
|
})
|
|
})
|