import { render, screen, fireEvent } from '@testing-library/react' import { GanttBoardView } from '../GanttBoardView' import type { GanttBoardViewProps } from '../GanttBoardView' import type { TimelineItem } from '../types' // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeItem(overrides: Partial & { id: string; title: string; status: string }): TimelineItem { return { startDate: '2025-01-01', endDate: '2025-03-31', ...overrides } } const CATEGORY_COLORS: Record = { product: '#3b82f6', team: '#a855f7', other: '#6b7280', } function defaultProps(overrides: Partial = {}): GanttBoardViewProps { return { boardColumns: [], boardDragItem: null, setBoardDragItem: jest.fn(), onStatusChange: undefined, categoryColors: CATEGORY_COLORS, handleItemClick: jest.fn(), ...overrides, } } // --------------------------------------------------------------------------- // Column rendering // --------------------------------------------------------------------------- describe('GanttBoardView — column rendering', () => { it('renders nothing when boardColumns is empty', () => { const { container } = render() // The board wrapper div is rendered but contains no column children expect(container.querySelector('.gantt-board-column')).not.toBeInTheDocument() }) it('renders one column per status', () => { const columns = [ { status: 'not_started', items: [] }, { status: 'in_progress', items: [] }, { status: 'completed', items: [] }, ] const { container } = render() expect(container.querySelectorAll('.gantt-board-column')).toHaveLength(3) }) it('renders a status badge in each column header', () => { const columns = [ { status: 'not_started', items: [] }, { status: 'in_progress', items: [] }, ] render() // Underscores are replaced with spaces in the badge expect(screen.getByText('not started')).toBeInTheDocument() expect(screen.getByText('in progress')).toBeInTheDocument() }) it('displays the item count in each column header', () => { const columns = [ { status: 'not_started', items: [makeItem({ id: 'a', title: 'Task A', status: 'not_started' })] }, { status: 'in_progress', items: [] }, ] render() // Count badges: '1' for the first column, '0' for the second const counts = screen.getAllByText('1') expect(counts.length).toBeGreaterThanOrEqual(1) expect(screen.getByText('0')).toBeInTheDocument() }) }) // --------------------------------------------------------------------------- // Card rendering // --------------------------------------------------------------------------- describe('GanttBoardView — card rendering', () => { it('renders a card for each item in a column', () => { const columns = [ { status: 'in_progress', items: [ makeItem({ id: 'a', title: 'Alpha Task', status: 'in_progress' }), makeItem({ id: 'b', title: 'Beta Task', status: 'in_progress' }), ], }, ] render() expect(screen.getByText('Alpha Task')).toBeInTheDocument() expect(screen.getByText('Beta Task')).toBeInTheDocument() }) it('renders a category badge on cards that have a category', () => { const columns = [ { status: 'in_progress', items: [makeItem({ id: 'a', title: 'Task A', status: 'in_progress', category: 'product' })], }, ] render() expect(screen.getByText('product')).toBeInTheDocument() }) it('does not render a category badge when the item has no category', () => { const columns = [ { status: 'in_progress', items: [makeItem({ id: 'a', title: 'Task A', status: 'in_progress' })], }, ] const { container } = render() expect(container.querySelector('.gantt-category-badge')).not.toBeInTheDocument() }) it('renders the end date on a card when the item has an endDate', () => { // Use local-time ISO string to avoid UTC→local timezone shift const columns = [ { status: 'in_progress', items: [makeItem({ id: 'a', title: 'Task A', status: 'in_progress', endDate: '2025-03-15T00:00:00' })], }, ] render() // date-fns format(new Date('2025-03-15T00:00:00'), 'MMM d') === 'Mar 15' expect(screen.getByText('Mar 15')).toBeInTheDocument() }) it('does not render an end date when the item has no endDate', () => { const columns = [ { status: 'in_progress', items: [{ id: 'a', title: 'No Date Task', status: 'in_progress' }], }, ] const { container } = render() expect(container.querySelector('.gantt-board-card-date')).not.toBeInTheDocument() }) it('uses the fallback "other" color when the item category is not in categoryColors', () => { const columns = [ { status: 'in_progress', items: [makeItem({ id: 'a', title: 'Task A', status: 'in_progress', category: 'unknown' })], }, ] const { container } = render() const badge = container.querySelector('.gantt-category-badge') as HTMLElement | null expect(badge).toBeInTheDocument() expect(badge?.style.backgroundColor).toBe('rgb(107, 114, 128)') // #6b7280 in rgb }) }) // --------------------------------------------------------------------------- // Item click // --------------------------------------------------------------------------- describe('GanttBoardView — item click', () => { it('calls handleItemClick when a card is clicked', () => { const handleItemClick = jest.fn() const item = makeItem({ id: 'a', title: 'Clickable', status: 'in_progress' }) const columns = [{ status: 'in_progress', items: [item] }] render() fireEvent.click(screen.getByText('Clickable')) expect(handleItemClick).toHaveBeenCalledWith(item) }) it('calls handleItemClick with the correct item when multiple cards are present', () => { const handleItemClick = jest.fn() const itemA = makeItem({ id: 'a', title: 'Task A', status: 'in_progress' }) const itemB = makeItem({ id: 'b', title: 'Task B', status: 'in_progress' }) const columns = [{ status: 'in_progress', items: [itemA, itemB] }] render() fireEvent.click(screen.getByText('Task B')) expect(handleItemClick).toHaveBeenCalledWith(itemB) }) }) // --------------------------------------------------------------------------- // Drag and drop // --------------------------------------------------------------------------- describe('GanttBoardView — drag and drop', () => { it('sets boardDragItem when drag starts on a card', () => { const setBoardDragItem = jest.fn() const onStatusChange = jest.fn() const item = makeItem({ id: 'a', title: 'Draggable', status: 'not_started' }) const columns = [{ status: 'not_started', items: [item] }] const { container } = render( ) const card = container.querySelector('.gantt-board-card') as HTMLElement fireEvent.dragStart(card) expect(setBoardDragItem).toHaveBeenCalledWith('a') }) it('clears boardDragItem when drag ends', () => { const setBoardDragItem = jest.fn() const onStatusChange = jest.fn() const item = makeItem({ id: 'a', title: 'Draggable', status: 'not_started' }) const columns = [{ status: 'not_started', items: [item] }] const { container } = render( ) const card = container.querySelector('.gantt-board-card') as HTMLElement fireEvent.dragEnd(card) expect(setBoardDragItem).toHaveBeenCalledWith(null) }) it('calls onStatusChange with the new status on drop', () => { const setBoardDragItem = jest.fn() const onStatusChange = jest.fn() const item = makeItem({ id: 'a', title: 'Draggable', status: 'not_started' }) const columns = [ { status: 'not_started', items: [item] }, { status: 'completed', items: [] }, ] const { container } = render( ) const targetColumn = container.querySelectorAll('.gantt-board-column')[1] as HTMLElement fireEvent.drop(targetColumn) expect(onStatusChange).toHaveBeenCalledWith('a', 'completed') }) it('does not call onStatusChange when onStatusChange is not provided', () => { const setBoardDragItem = jest.fn() const item = makeItem({ id: 'a', title: 'Draggable', status: 'not_started' }) const columns = [ { status: 'not_started', items: [item] }, { status: 'completed', items: [] }, ] // Should not throw even without onStatusChange const { container } = render( ) const targetColumn = container.querySelectorAll('.gantt-board-column')[1] as HTMLElement expect(() => fireEvent.drop(targetColumn)).not.toThrow() expect(setBoardDragItem).toHaveBeenCalledWith(null) }) it('cards are not draggable when onStatusChange is undefined', () => { const item = makeItem({ id: 'a', title: 'Static', status: 'not_started' }) const columns = [{ status: 'not_started', items: [item] }] const { container } = render( ) const card = container.querySelector('.gantt-board-card') as HTMLElement // draggable attribute should be false (falsy) when onStatusChange is absent expect(card.draggable).toBe(false) }) it('cards are draggable when onStatusChange is provided', () => { const onStatusChange = jest.fn() const item = makeItem({ id: 'a', title: 'Draggable', status: 'not_started' }) const columns = [{ status: 'not_started', items: [item] }] const { container } = render( ) const card = container.querySelector('.gantt-board-card') as HTMLElement expect(card.draggable).toBe(true) }) }) // --------------------------------------------------------------------------- // Drag-over visual state // --------------------------------------------------------------------------- describe('GanttBoardView — drop-target styling', () => { it('applies gantt-board-drop-target class to all columns when boardDragItem is set', () => { const columns = [ { status: 'not_started', items: [] }, { status: 'completed', items: [] }, ] const { container } = render( ) const dropTargets = container.querySelectorAll('.gantt-board-drop-target') expect(dropTargets).toHaveLength(2) }) it('does not apply gantt-board-drop-target when boardDragItem is null', () => { const columns = [{ status: 'not_started', items: [] }] const { container } = render( ) expect(container.querySelector('.gantt-board-drop-target')).not.toBeInTheDocument() }) })