import { render, screen } from '@testing-library/react' import type { DropResult } from '@hello-pangea/dnd' import { KanbanBoard, resolveKanbanMove } from '../KanbanBoard' interface Card { id: string title: string } const columns = [ { id: 'todo', label: 'To Do', color: '#6366f1' }, { id: 'in-progress', label: 'In Progress', color: '#f59e0b' }, { id: 'done', label: 'Done' }, ] const items: Record = { todo: [{ id: '1', title: 'Task One' }, { id: '2', title: 'Task Two' }], 'in-progress': [{ id: '3', title: 'Task Three' }], done: [], } describe('KanbanBoard', () => { const renderCard = (item: Card) => (
{item.title}
) it('renders all column labels', () => { render( ) expect(screen.getByText('To Do')).toBeInTheDocument() expect(screen.getByText('In Progress')).toBeInTheDocument() expect(screen.getByText('Done')).toBeInTheDocument() }) it('renders all card items', () => { render( ) expect(screen.getByText('Task One')).toBeInTheDocument() expect(screen.getByText('Task Two')).toBeInTheDocument() expect(screen.getByText('Task Three')).toBeInTheDocument() }) it('shows item counts per column in the default header', () => { render( ) // todo has 2, in-progress has 1, done has 0 const counts = screen.getAllByText(/^[0-9]+$/) const countValues = counts.map((el) => el.textContent) expect(countValues).toContain('2') expect(countValues).toContain('1') expect(countValues).toContain('0') }) it('shows empty column message when column has no items', () => { render( ) expect(screen.getByText('No items')).toBeInTheDocument() }) it('shows custom empty column message', () => { render( ) expect(screen.getByText('Nothing here yet')).toBeInTheDocument() }) it('renders color dot for columns that have a color', () => { const { container } = render( ) // Two columns have colors (todo, in-progress), one does not (done) const colorDots = container.querySelectorAll('[aria-hidden="true"]') expect(colorDots.length).toBe(2) }) it('uses custom column header when renderColumnHeader is provided', () => { const renderColumnHeader = (col: typeof columns[0]) => (
Custom: {col.label}
) render( ) expect(screen.getByTestId('custom-header-todo')).toBeInTheDocument() expect(screen.getByText('Custom: To Do')).toBeInTheDocument() }) it('renders column footer when renderColumnFooter is provided', () => { const renderColumnFooter = (col: typeof columns[0]) => (
Footer for {col.label}
) render( ) expect(screen.getByTestId('footer-todo')).toBeInTheDocument() expect(screen.getByText('Footer for To Do')).toBeInTheDocument() }) it('handles missing items for a column gracefully', () => { // Only provide items for one column const partialItems: Record = { todo: [{ id: '1', title: 'Task One' }], } render( ) // The other two columns should show the empty message const emptyMessages = screen.getAllByText('No items') expect(emptyMessages.length).toBe(2) }) it('applies custom className to container', () => { const { container } = render( ) expect(container.firstChild).toHaveClass('my-board') }) it('applies ring styles to column that isColumnOver returns true for', () => { const isColumnOver = (id: string) => id === 'todo' const { container } = render( ) // The first column div should have ring classes const firstCol = container.querySelector('[style*="320"]') expect(firstCol).toHaveClass('ring-2') }) it('renders draggable cards when getCardId + onCardMove are provided (DnD enabled)', () => { const onCardMove = jest.fn() render( c.id} onCardMove={onCardMove} /> ) // cards still render through the @hello-pangea/dnd path without crashing expect(screen.getByTestId('card-1')).toBeInTheDocument() expect(screen.getByTestId('card-3')).toBeInTheDocument() expect(screen.getByText('To Do')).toBeInTheDocument() }) }) describe('resolveKanbanMove', () => { const drop = ( draggableId: string, from: string, fromIndex: number, to?: string, toIndex?: number, ): DropResult => ({ draggableId, type: 'DEFAULT', source: { droppableId: from, index: fromIndex }, destination: to === undefined ? null : { droppableId: to, index: toIndex ?? 0 }, reason: 'DROP', mode: 'FLUID', combine: null, } as unknown as DropResult) it('resolves a cross-column move', () => { expect(resolveKanbanMove(drop('c1', 'new', 0, 'surfaced', 2))).toEqual({ cardId: 'c1', fromColumnId: 'new', toColumnId: 'surfaced', toIndex: 2, }) }) it('returns null when dropped outside any column', () => { expect(resolveKanbanMove(drop('c1', 'new', 0))).toBeNull() }) it('returns null when dropped back onto the same slot', () => { expect(resolveKanbanMove(drop('c1', 'new', 1, 'new', 1))).toBeNull() }) it('resolves a within-column reorder', () => { expect(resolveKanbanMove(drop('c1', 'new', 0, 'new', 3))).toEqual({ cardId: 'c1', fromColumnId: 'new', toColumnId: 'new', toIndex: 3, }) }) })