hermes-agent/ui-tui/src/__tests__/widgetGrid.test.ts

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 })
})
})