import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Kanban } from './kanban'; import { KANBAN_MOVE_EVENT, type KanbanMoveEvent } from './kanban.types'; const MARKUP = `
A
B
C
D
`; function setup(): HTMLElement { document.body.innerHTML = MARKUP; return document.body.firstElementChild as HTMLElement; } function item(root: HTMLElement, value: string): HTMLElement { return root.querySelector(`[data-c42-kanban-item][data-value="${value}"]`)!; } function columnOrder(root: HTMLElement, column: string): string[] { const col = root.querySelector(`[data-c42-kanban-column][data-value="${column}"]`)!; return Array.from(col.querySelectorAll('[data-c42-kanban-item]')).map( (el) => el.dataset.value!, ); } function press(el: HTMLElement, key: string): void { el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })); } describe('Kanban', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('wires roles and makes items focusable', () => { const root = setup(); new Kanban(root); expect(root.querySelector('[data-c42-kanban-column]')!.getAttribute('role')).toBe('group'); expect(root.querySelector('[data-c42-kanban-list]')!.getAttribute('role')).toBe('list'); expect(item(root, 'a').getAttribute('tabindex')).toBe('0'); expect(item(root, 'a').getAttribute('aria-roledescription')).toBe('Draggable item'); }); it('throws when there are no columns', () => { document.body.innerHTML = '
'; const root = document.body.firstElementChild as HTMLElement; expect(() => new Kanban(root)).toThrow(/column/); }); it('reports the current order per column', () => { const root = setup(); const board = new Kanban(root); expect(board.getOrder()).toEqual({ todo: ['a', 'b', 'c'], doing: ['d'], done: [] }); }); it('reorders within a column with ArrowDown/ArrowUp', () => { const root = setup(); new Kanban(root); press(item(root, 'a'), 'ArrowDown'); expect(columnOrder(root, 'todo')).toEqual(['b', 'a', 'c']); press(item(root, 'a'), 'ArrowUp'); expect(columnOrder(root, 'todo')).toEqual(['a', 'b', 'c']); }); it('jumps to start/end of a column with Home/End', () => { const root = setup(); new Kanban(root); press(item(root, 'a'), 'End'); expect(columnOrder(root, 'todo')).toEqual(['b', 'c', 'a']); press(item(root, 'a'), 'Home'); expect(columnOrder(root, 'todo')).toEqual(['a', 'b', 'c']); }); it('moves to an adjacent column with ArrowRight/ArrowLeft', () => { const root = setup(); new Kanban(root); press(item(root, 'b'), 'ArrowRight'); // todo index 1 -> doing index 1 expect(columnOrder(root, 'todo')).toEqual(['a', 'c']); expect(columnOrder(root, 'doing')).toEqual(['d', 'b']); press(item(root, 'b'), 'ArrowLeft'); // back toward todo expect(columnOrder(root, 'doing')).toEqual(['d']); expect(columnOrder(root, 'todo')).toContain('b'); }); it('does not move past the first or last column', () => { const root = setup(); const board = new Kanban(root); press(item(root, 'a'), 'ArrowLeft'); // already leftmost column expect(columnOrder(root, 'todo')).toEqual(['a', 'b', 'c']); board.moveItem('d', 'done'); press(item(root, 'd'), 'ArrowRight'); // already rightmost column expect(columnOrder(root, 'done')).toEqual(['d']); }); it('emits a move event with full detail', () => { const root = setup(); const board = new Kanban(root); const spy = vi.fn(); board.on(KANBAN_MOVE_EVENT, spy); press(item(root, 'a'), 'ArrowRight'); // todo[0] -> doing[0] expect(spy).toHaveBeenCalledOnce(); const detail = spy.mock.calls[0][0].detail; expect(detail.item).toBe('a'); expect(detail.from).toBe('todo'); expect(detail.to).toBe('doing'); expect(detail.fromIndex).toBe(0); expect(detail.toIndex).toBe(0); expect(detail.order.doing).toEqual(['a', 'd']); }); it('does not emit when nothing changes', () => { const root = setup(); const board = new Kanban(root); const spy = vi.fn(); board.on(KANBAN_MOVE_EVENT, spy); press(item(root, 'a'), 'ArrowUp'); // already at top expect(spy).not.toHaveBeenCalled(); }); it('moves programmatically via moveItem at a given index', () => { const root = setup(); const board = new Kanban(root); expect(board.moveItem('c', 'doing', 0)).toBe(true); expect(columnOrder(root, 'doing')).toEqual(['c', 'd']); expect(columnOrder(root, 'todo')).toEqual(['a', 'b']); }); it('moveItem returns false for unknown item or column', () => { const root = setup(); const board = new Kanban(root); expect(board.moveItem('zzz', 'doing')).toBe(false); expect(board.moveItem('a', 'nope')).toBe(false); }); it('registers items as draggable when drag is enabled (pragmatic-dnd)', () => { const root = setup(); new Kanban(root, { drag: true }); expect(item(root, 'a').getAttribute('draggable')).toBe('true'); expect(item(root, 'd').getAttribute('draggable')).toBe('true'); }); it('does not register draggables when drag is disabled', () => { const root = setup(); new Kanban(root, { drag: false }); expect(item(root, 'a').hasAttribute('draggable')).toBe(false); }); it('removes listeners and drag wiring on destroy', () => { const root = setup(); const board = new Kanban(root); expect(item(root, 'a').getAttribute('draggable')).toBe('true'); board.destroy(); expect(item(root, 'a').hasAttribute('draggable')).toBe(false); press(item(root, 'a'), 'ArrowDown'); expect(columnOrder(root, 'todo')).toEqual(['a', 'b', 'c']); }); });