import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SortableTable } from './sortable-table'; import { SORTABLE_TABLE_REORDER_EVENT, SORTABLE_TABLE_RESIZE_EVENT, type SortableTableReorderEvent, type SortableTableResizeEvent, } from './sortable-table.types'; const MARKUP = `
Name Age
Ada 36
Linus 54
Grace 85
`; function setup(): HTMLElement { document.body.innerHTML = MARKUP; return document.body.firstElementChild as HTMLElement; } function rowHandle(root: HTMLElement, value: string): HTMLButtonElement { return root .querySelector(`[data-c42-sortable-row][data-value="${value}"]`)! .querySelector('[data-c42-sortable-row-handle]')!; } function colHandle(root: HTMLElement, value: string): HTMLButtonElement { return root .querySelector(`[data-c42-sortable-col][data-value="${value}"]`)! .querySelector('[data-c42-sortable-col-handle]')!; } function press(el: HTMLElement, key: string): void { el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })); } const FULL = { reorderRows: true, reorderColumns: true, resizeColumns: true }; describe('SortableTable', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('reports the initial row and column order', () => { const root = setup(); const table = new SortableTable(root, FULL); expect(table.getRowOrder()).toEqual(['r1', 'r2', 'r3']); expect(table.getColumnOrder()).toEqual(['name', 'age']); }); it('moveRow reorders rows and emits', () => { const root = setup(); const table = new SortableTable(root, FULL); const spy = vi.fn(); table.on(SORTABLE_TABLE_REORDER_EVENT, spy); table.moveRow('r1', 'r3'); expect(table.getRowOrder()).toEqual(['r2', 'r3', 'r1']); expect(spy.mock.calls[0][0].detail).toMatchObject({ axis: 'row', order: ['r2', 'r3', 'r1'] }); }); it('moves a row up with ArrowUp on its handle', () => { const root = setup(); const table = new SortableTable(root, FULL); press(rowHandle(root, 'r2'), 'ArrowUp'); expect(table.getRowOrder()).toEqual(['r2', 'r1', 'r3']); }); it('moves a row down with ArrowDown on its handle', () => { const root = setup(); const table = new SortableTable(root, FULL); press(rowHandle(root, 'r2'), 'ArrowDown'); expect(table.getRowOrder()).toEqual(['r1', 'r3', 'r2']); }); it('moves a row to the start/end with Home/End', () => { const root = setup(); const table = new SortableTable(root, FULL); press(rowHandle(root, 'r3'), 'Home'); expect(table.getRowOrder()).toEqual(['r3', 'r1', 'r2']); press(rowHandle(root, 'r3'), 'End'); expect(table.getRowOrder()).toEqual(['r1', 'r2', 'r3']); }); it('moveColumn reorders the header and every body cell', () => { const root = setup(); const table = new SortableTable(root, FULL); table.moveColumn('name', 'age'); expect(table.getColumnOrder()).toEqual(['age', 'name']); const firstBodyRow = root.querySelector('[data-c42-sortable-row]')!; const cells = Array.from(firstBodyRow.children).map((c) => c.textContent?.trim()); expect(cells[0]).toBe('36'); }); it('moves a column with arrow keys on the column handle', () => { const root = setup(); const table = new SortableTable(root, FULL); press(colHandle(root, 'age'), 'ArrowLeft'); expect(table.getColumnOrder()).toEqual(['age', 'name']); }); it('setColumnWidth sets width and emits resize', () => { const root = setup(); const table = new SortableTable(root, FULL); const spy = vi.fn(); table.on(SORTABLE_TABLE_RESIZE_EVENT, spy); table.setColumnWidth('name', 220); const th = root.querySelector('[data-c42-sortable-col][data-value="name"]')!; expect(th.style.width).toBe('220px'); expect(spy.mock.calls[0][0].detail).toEqual({ value: 'name', width: 220 }); }); it('clamps width to the minimum', () => { const root = setup(); const table = new SortableTable(root, { ...FULL, minColumnWidth: 60 }); table.setColumnWidth('name', 10); const th = root.querySelector('[data-c42-sortable-col][data-value="name"]')!; expect(th.style.width).toBe('60px'); }); it('resizes a column with arrow keys on the resize handle', () => { const root = setup(); new SortableTable(root, { ...FULL, resizeStep: 20, minColumnWidth: 50 }); const th = root.querySelector('[data-c42-sortable-col][data-value="name"]')!; const handle = th.querySelector('[data-c42-sortable-resize]')!; press(handle, 'ArrowRight'); // starts from minColumnWidth (50, no measurable rect in jsdom) + 20 expect(th.style.width).toBe('70px'); }); it('wires resize handle ARIA', () => { const root = setup(); new SortableTable(root, FULL); const handle = root.querySelector('[data-c42-sortable-resize]')!; expect(handle.getAttribute('role')).toBe('separator'); expect(handle.getAttribute('aria-orientation')).toBe('vertical'); expect(handle.getAttribute('tabindex')).toBe('0'); }); it('registers rows and columns as draggable (pragmatic-dnd)', () => { const root = setup(); new SortableTable(root, FULL); const row = root.querySelector('[data-c42-sortable-row][data-value="r1"]')!; const th = root.querySelector('[data-c42-sortable-col][data-value="name"]')!; expect(row.getAttribute('draggable')).toBe('true'); expect(th.getAttribute('draggable')).toBe('true'); }); it('does not register row draggables when reorderRows is false', () => { const root = setup(); new SortableTable(root, { reorderRows: false }); const row = root.querySelector('[data-c42-sortable-row][data-value="r1"]')!; expect(row.hasAttribute('draggable')).toBe(false); }); it('does not wire row handles when reorderRows is false', () => { const root = setup(); const table = new SortableTable(root, { reorderRows: false }); press(rowHandle(root, 'r2'), 'ArrowUp'); expect(table.getRowOrder()).toEqual(['r1', 'r2', 'r3']); }); it('removes listeners and drag wiring on destroy', () => { const root = setup(); const table = new SortableTable(root, FULL); const row = root.querySelector('[data-c42-sortable-row][data-value="r1"]')!; expect(row.getAttribute('draggable')).toBe('true'); const spy = vi.fn(); table.on(SORTABLE_TABLE_REORDER_EVENT, spy); table.destroy(); expect(row.hasAttribute('draggable')).toBe(false); press(rowHandle(root, 'r2'), 'ArrowUp'); expect(spy).not.toHaveBeenCalled(); expect(table.getRowOrder()).toEqual(['r1', 'r2', 'r3']); }); });