/**
* Broad behavioral test suite for the render component.
*
* The intent is wide coverage on a single file - SvGrid.svelte is ~3000
* lines and most of its branches are reachable only when the component is
* mounted with a particular prop combination. Each test below exercises a
* different feature set so the whole file's effect graph is traversed.
*
* Pattern (re-used across files):
* 1. mount SvGrid in jsdom with the props that trigger the feature
* 2. wait one microtask for $effects to flush
* 3. assert on the api object delivered via onApiReady, or on visible DOM
* 4. unmount
*/
import { describe, expect, it } from 'vitest'
import { mount, unmount } from 'svelte'
import SvGrid from './SvGrid.svelte'
import {
columnFilteringFeature,
columnGroupingFeature,
createCoreRowModel,
createExpandedRowModel,
createFilteredRowModel,
createGroupedRowModel,
createPaginatedRowModel,
createSortedRowModel,
rowExpandingFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
sortFns,
tableFeatures,
} from './index'
import type { ColumnDef, SvGridApi } from './index'
type Person = {
id: number
name: string
team: string
age: number
active: boolean
joined: string
salary: number
}
const fullFeatures = tableFeatures({
columnFilteringFeature,
columnGroupingFeature,
rowExpandingFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
})
const people: Person[] = [
{ id: 1, name: 'Ada Lovelace', team: 'Research', age: 38, active: true, joined: '2020-01-10', salary: 142_000 },
{ id: 2, name: 'Grace Hopper', team: 'Compilers', age: 42, active: true, joined: '2018-05-22', salary: 158_000 },
{ id: 3, name: 'Alan Turing', team: 'Research', age: 41, active: false, joined: '2021-09-01', salary: 138_000 },
{ id: 4, name: 'Margaret Hamilton', team: 'Apollo', age: 35, active: true, joined: '2019-03-15', salary: 165_000 },
{ id: 5, name: 'Linus Torvalds', team: 'Kernel', age: 54, active: true, joined: '2017-11-30', salary: 175_000 },
{ id: 6, name: 'Donald Knuth', team: 'Research', age: 86, active: false, joined: '2010-06-06', salary: 120_000 },
{ id: 7, name: 'Brian Kernighan', team: 'Compilers', age: 80, active: true, joined: '2012-02-02', salary: 160_000 },
{ id: 8, name: 'Dennis Ritchie', team: 'Kernel', age: 70, active: false, joined: '2009-07-14', salary: 110_000 },
]
const personColumns: ColumnDef[] = [
{ field: 'name', header: 'Name', width: 200 },
{ field: 'team', header: 'Team', width: 160 },
{
field: 'age',
header: 'Age',
width: 100,
editorType: 'number',
format: { type: 'number' },
},
{
field: 'active',
header: 'Active',
width: 100,
editorType: 'checkbox',
},
{
field: 'joined',
header: 'Joined',
width: 140,
editorType: 'date',
format: { type: 'number' },
},
{
field: 'salary',
header: 'Salary',
width: 140,
editorType: 'number',
format: { type: 'currency', currency: 'USD' },
},
]
type MountResult = {
api: SvGridApi
target: HTMLElement
destroy: () => void
}
function mountGrid(overrides: Record = {}): Promise {
return new Promise((resolveApi, rejectApi) => {
const target = document.createElement('div')
target.style.width = '1200px'
target.style.height = '600px'
document.body.appendChild(target)
let capturedApi: SvGridApi | null = null
const app = mount(SvGrid, {
target,
props: {
data: people,
columns: personColumns,
features: fullFeatures,
_rowModels: {
coreRowModel: createCoreRowModel(),
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(sortFns),
groupedRowModel: createGroupedRowModel(),
expandedRowModel: createExpandedRowModel(),
paginatedRowModel: createPaginatedRowModel(),
},
rowHeight: 36,
containerHeight: 480,
virtualization: false,
onApiReady(api: SvGridApi) {
capturedApi = api
resolveApi({
api,
target,
destroy: () => {
unmount(app)
target.remove()
},
})
},
...overrides,
} as any,
})
queueMicrotask(() => {
if (!capturedApi) rejectApi(new Error('onApiReady never fired'))
})
})
}
const tick = () => Promise.resolve()
describe('SvGrid - core render', () => {
it('mounts and renders the column headers', async () => {
const { target, destroy } = await mountGrid()
try {
const headers = target.querySelectorAll('[data-svgrid-header-col]')
// jsdom reports 0 for offsetWidth so column virtualization may render
// a subset. The point of this assertion is just that the header row
// exists and is populated.
expect(headers.length).toBeGreaterThan(0)
} finally {
destroy()
}
})
it('renders the rows in the body', async () => {
const { target, destroy } = await mountGrid()
try {
await tick()
const rows = target.querySelectorAll('[data-svgrid-row]')
// Pagination defaults to off in our mount - all rows render.
expect(rows.length).toBeGreaterThanOrEqual(people.length)
} finally {
destroy()
}
})
it('renders the empty message when data is empty', async () => {
const { target, destroy } = await mountGrid({ data: [], emptyMessage: 'No rows yet' })
try {
await tick()
expect(target.textContent).toContain('No rows yet')
} finally {
destroy()
}
})
it('renders a loading state when `loading` is true', async () => {
const { target, destroy } = await mountGrid({ loading: true })
try {
await tick()
const statusEls = target.querySelectorAll('[role="status"]')
expect(statusEls.length).toBeGreaterThan(0)
} finally {
destroy()
}
})
it('renders an error state when `error` is set', async () => {
const { target, destroy } = await mountGrid({ error: 'Something broke' })
try {
await tick()
expect(target.textContent).toContain('Something broke')
} finally {
destroy()
}
})
})
describe('SvGrid - sort', () => {
it('setSort then clearSort updates filters via api', async () => {
const { api, destroy } = await mountGrid()
try {
api.setSort('name', 'asc')
await tick()
api.setSort('name', 'desc')
await tick()
api.setSort('name', null)
await tick()
api.clearSort()
await tick()
// No throw + state consistent = pass.
expect(api.getData().length).toBe(people.length)
} finally {
destroy()
}
})
})
describe('SvGrid - pagination', () => {
it('shows fewer displayed rows than total when paginated', async () => {
const { api, destroy } = await mountGrid({ showPagination: true, pageSize: 3 })
try {
await tick()
const displayed = api.getDisplayedRows()
expect(displayed.length).toBeLessThanOrEqual(3)
} finally {
destroy()
}
})
})
describe('SvGrid - global filter', () => {
it('showGlobalFilter renders the input', async () => {
const { target, destroy } = await mountGrid({ showGlobalFilter: true })
try {
await tick()
const input = target.querySelector(
'.sv-grid-global-filter input, input[type="text"]',
)
expect(input).not.toBeNull()
} finally {
destroy()
}
})
})
describe('SvGrid - column filter row', () => {
it('showColumnFilters renders the filter row inputs', async () => {
const { target, destroy } = await mountGrid({ showColumnFilters: true })
try {
await tick()
const inputs = target.querySelectorAll('.sv-grid-column-filter')
// One filter input per filterable column.
expect(inputs.length).toBeGreaterThan(0)
} finally {
destroy()
}
})
})
describe('SvGrid - inline editing', () => {
it('enables enableInlineEditing without throwing', async () => {
const { api, destroy } = await mountGrid({ enableInlineEditing: true })
try {
// setCellValue should mutate internal data.
api.setCellValue(0, 'name', 'Renamed Person')
await tick()
const data = api.getData()
expect((data[0] as Person).name).toBe('Renamed Person')
} finally {
destroy()
}
})
})
describe('SvGrid - row selection', () => {
it('showRowSelection renders the checkbox column', async () => {
const { target, destroy } = await mountGrid({ showRowSelection: true })
try {
await tick()
const checkboxes = target.querySelectorAll('.sv-grid-checkbox')
// At least the header + one per row.
expect(checkboxes.length).toBeGreaterThan(0)
} finally {
destroy()
}
})
})
describe('SvGrid - capability shortcuts', () => {
it('sortable + filterable enable sorting/filtering UI with no features prop', async () => {
const { target, destroy } = await mountGrid({
features: undefined,
sortable: true,
filterable: true,
})
try {
await tick()
// rowSortingFeature injected -> headers expose aria-sort.
expect(
target.querySelectorAll('[role="columnheader"][aria-sort]').length,
).toBeGreaterThan(0)
// columnFilteringFeature injected -> per-column filter button renders.
expect(target.querySelector('.sv-grid-col-filter-btn')).toBeTruthy()
} finally {
destroy()
}
})
it('sortable={false} overrides a rowSortingFeature passed via features', async () => {
// The harness default `fullFeatures` includes rowSortingFeature.
const { target, destroy } = await mountGrid({ sortable: false })
try {
await tick()
expect(
target.querySelector('[role="columnheader"][aria-sort]'),
).toBeNull()
} finally {
destroy()
}
})
it('pageable toggles the pagination footer', async () => {
const off = await mountGrid({ pageable: false })
try {
await tick()
expect(off.target.querySelector('.sv-grid-pagination')).toBeNull()
} finally {
off.destroy()
}
const on = await mountGrid({ features: undefined, pageable: true })
try {
await tick()
expect(on.target.querySelector('.sv-grid-pagination')).toBeTruthy()
} finally {
on.destroy()
}
})
it('paginationPosition mounts + gates the bottom pager', async () => {
// The top pager is gated on measurement (jsdom does not measure), so it's not
// asserted here; the visible top pager is covered by the browser/e2e check.
// This guards the mount path (a top footer rendered pre-measure used to crash).
for (const paginationPosition of ['top', 'both', 'bottom'] as const) {
const g = await mountGrid({ showPagination: true, paginationPosition })
try {
await tick()
expect(g.api).toBeTruthy()
const bottoms = [...g.target.querySelectorAll('.sv-grid-pagination')].filter((el) => !el.classList.contains('sv-grid-pagination-top'))
expect(bottoms.length).toBe(paginationPosition === 'top' ? 0 : 1) // no bottom pager when position is 'top'
} finally {
g.destroy()
}
}
})
it('pageSizeOptions is accepted by the pager', async () => {
const g = await mountGrid({ showPagination: true, pageSizeOptions: [5, 15, 45] })
try {
await tick()
expect(g.target.querySelector('.sv-grid-pagination')).toBeTruthy()
} finally {
g.destroy()
}
})
it('editable={false} blocks inline editing entry points', async () => {
const { api, target, destroy } = await mountGrid({ editable: false })
try {
await tick()
// Double-clicking a body cell must not open an editor when editing
// is disabled via the shortcut.
const cell = target.querySelector('.sv-grid-cell')
cell?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
await tick()
expect(target.querySelector('.sv-grid-cell-editor')).toBeNull()
expect(api.getData().length).toBe(people.length)
} finally {
destroy()
}
})
})
describe('SvGrid - cell selection', () => {
it('enableCellSelection mounts without error', async () => {
const { api, destroy } = await mountGrid({ enableCellSelection: true })
try {
await tick()
expect(api.getData().length).toBe(people.length)
} finally {
destroy()
}
})
})
describe('SvGrid - grouping + aggregation', () => {
it('setGroupBy mounts + the codepath runs without throwing', async () => {
const { api, target, destroy } = await mountGrid({
enableRowSummaries: true,
showGroupingControls: true,
})
try {
api.setGroupBy(['team'])
await tick()
await tick()
// Sanity: the grid is still mounted and the API responsive.
const data = api.getData()
expect(data.length).toBe(people.length)
// Group rendering depends on row model + container dimensions in jsdom;
// just verify the codepath does not throw.
expect(target).toBeTruthy()
} finally {
destroy()
}
})
})
describe('SvGrid - column add / remove / visibility', () => {
it('addColumn and removeColumn round-trip', async () => {
const { api, destroy } = await mountGrid()
try {
api.addColumn({ id: 'id-col', field: 'id', header: 'ID', width: 60 } as any, 'left')
await tick()
api.addColumn(
{ id: 'extra-tag', field: 'team', header: 'Extra', width: 60 } as any,
'right',
)
await tick()
api.addColumn(
{ id: 'mid-col', field: 'name', header: 'Mid', width: 60 } as any,
1,
)
await tick()
api.removeColumn('id-col')
await tick()
// No throw means the codepath was exercised.
expect(typeof api.getData).toBe('function')
} finally {
destroy()
}
})
it('setColumnVisible toggles visibility flag', async () => {
const { api, destroy } = await mountGrid()
try {
expect(api.isColumnVisible('name')).toBe(true)
api.setColumnVisible('name', false)
await tick()
expect(api.isColumnVisible('name')).toBe(false)
api.setColumnVisible('name', true)
await tick()
expect(api.isColumnVisible('name')).toBe(true)
} finally {
destroy()
}
})
it('columns marked `visible: false` start hidden but stay listed', async () => {
const columns: ColumnDef[] = personColumns.map(
(c) => (c.field === 'team' ? { ...c, visible: false } : c),
)
const { api, destroy } = await mountGrid({ columns })
try {
// Starts hidden...
expect(api.isColumnVisible('team')).toBe(false)
// ...but is still listed in getColumns() (so a Choose Columns UI
// can offer it), just with visible=false.
const team = api.getColumns().find((c) => c.id === 'team')
expect(team).toBeDefined()
expect(team?.visible).toBe(false)
// A later user toggle wins over the initial flag.
api.setColumnVisible('team', true)
await tick()
expect(api.isColumnVisible('team')).toBe(true)
} finally {
destroy()
}
})
})
describe('SvGrid - row add / remove', () => {
it('addRow at top / bottom / specific index', async () => {
const { api, destroy } = await mountGrid()
try {
const before = api.getData().length
api.addRow({ id: 99, name: 'Top', team: 'X', age: 20, active: true, joined: '2025', salary: 1 }, 'top')
api.addRow({ id: 100, name: 'Bottom', team: 'X', age: 20, active: true, joined: '2025', salary: 1 }, 'bottom')
api.addRow({ id: 101, name: 'Mid', team: 'X', age: 20, active: true, joined: '2025', salary: 1 }, 2)
await tick()
expect(api.getData().length).toBe(before + 3)
} finally {
destroy()
}
})
it('addRows / removeRow / removeRows', async () => {
const { api, destroy } = await mountGrid()
try {
const before = api.getData().length
api.addRows(
[
{ id: 90, name: 'A', team: 'X', age: 1, active: true, joined: '2025', salary: 1 },
{ id: 91, name: 'B', team: 'X', age: 1, active: true, joined: '2025', salary: 1 },
],
'bottom',
)
await tick()
api.removeRow(0)
await tick()
api.removeRows([0, 1])
await tick()
expect(api.getData().length).toBe(before + 2 - 3)
} finally {
destroy()
}
})
})
describe('SvGrid - getCellValue', () => {
it('returns the cell value for a known row+column', async () => {
const { api, destroy } = await mountGrid()
try {
const value = api.getCellValue(0, 'name')
expect(value).toBe(people[0]!.name)
} finally {
destroy()
}
})
it('returns undefined for unknown row or column', async () => {
const { api, destroy } = await mountGrid()
try {
expect(api.getCellValue(999, 'name')).toBeUndefined()
expect(api.getCellValue(0, 'no-such-col')).toBeUndefined()
} finally {
destroy()
}
})
})
describe('SvGrid - row number column', () => {
it('showRowNumbers renders the leading row-number column', async () => {
const { target, destroy } = await mountGrid({ showRowNumbers: true })
try {
await tick()
const rnCells = target.querySelectorAll('.sv-grid-row-number-cell')
expect(rnCells.length).toBeGreaterThan(0)
} finally {
destroy()
}
})
})
describe('SvGrid - external sort / filter', () => {
it('externalSort + onSortingChange fires without re-ordering rows', async () => {
let captured: Array<{ id: string; desc: boolean }> | null = null
const { api, destroy } = await mountGrid({
externalSort: true,
onSortingChange: (s: any) => (captured = s),
})
try {
api.setSort('age', 'desc')
await tick()
expect(captured).not.toBeNull()
} finally {
destroy()
}
})
it('externalFilter + onFiltersChange fires when a filter is set', async () => {
let calls = 0
const { api, destroy } = await mountGrid({
externalFilter: true,
onFiltersChange: () => {
calls += 1
},
})
try {
api.setFilter('team', { operator: 'contains', value: 'Research' })
await tick()
expect(calls).toBeGreaterThan(0)
} finally {
destroy()
}
})
})
describe('SvGrid - custom snippet cell', () => {
it('accepts a custom cell renderer without throwing', async () => {
// Build a snippet via raw object - the SvGrid component reads
// `column.columnDef.cell` and pipes it through FlexRender. Returning a
// plain string is the simplest renderer; tests only need to verify the
// render path doesn't crash.
const customCols: ColumnDef[] = personColumns.map((c) =>
c.field === 'name'
? {
...c,
cell: (ctx: any) => `★ ${(ctx.row.original as Person).name}`,
}
: c,
)
const { target, destroy } = await mountGrid({ columns: customCols })
try {
await tick()
expect(target.textContent).toContain('★')
} finally {
destroy()
}
})
})
describe('SvGrid - filterMode shortcuts', () => {
it('filterMode="row" + global filter input wiring', async () => {
const { destroy } = await mountGrid({ filterMode: 'row' })
try {
await tick()
// No assertion needed - exercising the codepath is enough.
expect(true).toBe(true)
} finally {
destroy()
}
})
it('filterMode="global"', async () => {
const { destroy } = await mountGrid({ filterMode: 'global' })
try {
await tick()
expect(true).toBe(true)
} finally {
destroy()
}
})
it('filterMode="none"', async () => {
const { destroy } = await mountGrid({ filterMode: 'none' })
try {
await tick()
expect(true).toBe(true)
} finally {
destroy()
}
})
})
describe('SvGrid - selectionMode shortcuts', () => {
it('selectionMode="row"', async () => {
const { destroy } = await mountGrid({ selectionMode: 'row' })
try {
await tick()
expect(true).toBe(true)
} finally {
destroy()
}
})
it('selectionMode="cell"', async () => {
const { destroy } = await mountGrid({ selectionMode: 'cell' })
try {
await tick()
expect(true).toBe(true)
} finally {
destroy()
}
})
it('selectionMode="both"', async () => {
const { destroy } = await mountGrid({ selectionMode: 'both' })
try {
await tick()
expect(true).toBe(true)
} finally {
destroy()
}
})
it('selectionMode="none"', async () => {
const { destroy } = await mountGrid({ selectionMode: 'none' })
try {
await tick()
expect(true).toBe(true)
} finally {
destroy()
}
})
})
describe('SvGrid - fitColumns', () => {
it('mounts with fitColumns=true without throwing', async () => {
const { destroy } = await mountGrid({ fitColumns: true })
try {
await tick()
expect(true).toBe(true)
} finally {
destroy()
}
})
})