import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { MemoryRouter } from 'react-router' import { TaskList } from './TaskList' import type { Task } from '~/types/task' import { TaskStatus } from '~/types/task' import { useTasks } from '~/hooks/useTasks' // Mock the useTasks hook vi.mock('~/hooks/useTasks') const mockUseTasks = useTasks as unknown as ReturnType // Sample task data for testing const mockTasks: Task[] = [ { id: '1', title: 'Alpha Task', description: 'First task description', status: TaskStatus.Todo, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', completed_at: null, }, { id: '2', title: 'Beta Task', description: 'Second task description', status: TaskStatus.Done, created_at: '2023-01-02T00:00:00Z', updated_at: '2023-01-02T00:00:00Z', completed_at: '2023-01-02T01:00:00Z', }, { id: '3', title: 'Charlie Task', description: null, status: TaskStatus.Backlog, created_at: '2023-01-03T00:00:00Z', updated_at: '2023-01-03T00:00:00Z', completed_at: null, }, ] const defaultMockUseTasks = { tasks: mockTasks, loading: false, error: null, lastFetch: new Date(), fetchTasks: vi.fn(), createTask: vi.fn(), refreshTasks: vi.fn(), deleteTask: vi.fn().mockResolvedValue(true), getTaskById: vi.fn(), filterTasksByStatus: vi.fn(), clearError: vi.fn(), } const renderTaskList = (initialTasks?: Task[]) => { return render( ) } describe('TaskList Sorting Functionality', () => { beforeEach(() => { vi.clearAllMocks() mockUseTasks.mockReturnValue(defaultMockUseTasks) }) afterEach(() => { vi.restoreAllMocks() }) describe('Default Sorting', () => { it('should default to created_desc sorting (newest first)', () => { renderTaskList() // Tasks should be ordered by creation date descending (newest first) const taskRows = screen.getAllByRole('row') // Skip header row (index 0) const firstTaskRow = taskRows[1] const secondTaskRow = taskRows[2] const thirdTaskRow = taskRows[3] // Charlie Task (2023-01-03) should be first expect(firstTaskRow).toHaveTextContent('Charlie Task') // Beta Task (2023-01-02) should be second expect(secondTaskRow).toHaveTextContent('Beta Task') // Alpha Task (2023-01-01) should be third expect(thirdTaskRow).toHaveTextContent('Alpha Task') }) it('should show correct sort indicator for created column by default', () => { renderTaskList() const createdHeader = screen.getByRole('button', { name: /created/i }) expect(createdHeader).toHaveClass('Mui-active') // Should have descending indicator (direction class) expect(createdHeader).toHaveClass('MuiTableSortLabel-directionDesc') }) }) describe('Title Column Sorting', () => { it('should sort by title ascending when clicked first time', async () => { renderTaskList() const titleHeader = screen.getByRole('button', { name: /title/i }) fireEvent.click(titleHeader) await waitFor(() => { const taskRows = screen.getAllByRole('row') // Skip header row const firstTaskRow = taskRows[1] const secondTaskRow = taskRows[2] const thirdTaskRow = taskRows[3] // Should be in alphabetical order expect(firstTaskRow).toHaveTextContent('Alpha Task') expect(secondTaskRow).toHaveTextContent('Beta Task') expect(thirdTaskRow).toHaveTextContent('Charlie Task') }) }) it('should sort by title descending when clicked second time', async () => { renderTaskList() const titleHeader = screen.getByRole('button', { name: /title/i }) // First click - ascending fireEvent.click(titleHeader) // Second click - descending fireEvent.click(titleHeader) await waitFor(() => { const taskRows = screen.getAllByRole('row') const firstTaskRow = taskRows[1] const secondTaskRow = taskRows[2] const thirdTaskRow = taskRows[3] // Should be in reverse alphabetical order expect(firstTaskRow).toHaveTextContent('Charlie Task') expect(secondTaskRow).toHaveTextContent('Beta Task') expect(thirdTaskRow).toHaveTextContent('Alpha Task') }) }) it('should show correct sort indicators for title column', async () => { renderTaskList() const titleHeader = screen.getByRole('button', { name: /title/i }) // Initially not active expect(titleHeader).not.toHaveClass('Mui-active') // First click - ascending fireEvent.click(titleHeader) await waitFor(() => { expect(titleHeader).toHaveClass('Mui-active') expect(titleHeader).toHaveClass('MuiTableSortLabel-directionAsc') }) // Second click - descending fireEvent.click(titleHeader) await waitFor(() => { expect(titleHeader).toHaveClass('Mui-active') expect(titleHeader).toHaveClass('MuiTableSortLabel-directionDesc') }) }) }) describe('Status Column Sorting', () => { it('should sort by status priority (Todo, Backlog, Done) when clicked', async () => { renderTaskList() const statusHeader = screen.getByRole('button', { name: /status/i }) fireEvent.click(statusHeader) await waitFor(() => { const taskRows = screen.getAllByRole('row') const firstTaskRow = taskRows[1] const secondTaskRow = taskRows[2] const thirdTaskRow = taskRows[3] // Should be ordered by status priority expect(firstTaskRow).toHaveTextContent('Alpha Task') // Todo expect(secondTaskRow).toHaveTextContent('Charlie Task') // Backlog expect(thirdTaskRow).toHaveTextContent('Beta Task') // Done }) }) it('should show correct sort indicator for status column', async () => { renderTaskList() const statusHeader = screen.getByRole('button', { name: /status/i }) // Initially not active expect(statusHeader).not.toHaveClass('Mui-active') fireEvent.click(statusHeader) await waitFor(() => { expect(statusHeader).toHaveClass('Mui-active') expect(statusHeader).toHaveClass('MuiTableSortLabel-directionAsc') }) }) }) describe('Created Column Sorting', () => { it('should toggle between ascending and descending when clicked', async () => { renderTaskList() const createdHeader = screen.getByRole('button', { name: /created/i }) // First click should change to ascending (oldest first) fireEvent.click(createdHeader) await waitFor(() => { const taskRows = screen.getAllByRole('row') const firstTaskRow = taskRows[1] const thirdTaskRow = taskRows[3] expect(firstTaskRow).toHaveTextContent('Alpha Task') // 2023-01-01 expect(thirdTaskRow).toHaveTextContent('Charlie Task') // 2023-01-03 }) // Second click should change back to descending (newest first) fireEvent.click(createdHeader) await waitFor(() => { const taskRows = screen.getAllByRole('row') const firstTaskRow = taskRows[1] const thirdTaskRow = taskRows[3] expect(firstTaskRow).toHaveTextContent('Charlie Task') // 2023-01-03 expect(thirdTaskRow).toHaveTextContent('Alpha Task') // 2023-01-01 }) }) }) describe('Sort Persistence', () => { it('should maintain sort order after clicking different headers', async () => { renderTaskList() // Sort by title ascending const titleHeader = screen.getByRole('button', { name: /title/i }) fireEvent.click(titleHeader) await waitFor(() => { const taskRows = screen.getAllByRole('row') expect(taskRows[1]).toHaveTextContent('Alpha Task') expect(taskRows[2]).toHaveTextContent('Beta Task') expect(taskRows[3]).toHaveTextContent('Charlie Task') }) // Sort by status const statusHeader = screen.getByRole('button', { name: /status/i }) fireEvent.click(statusHeader) await waitFor(() => { const taskRows = screen.getAllByRole('row') // Should be ordered by status priority: Todo, Backlog, Done expect(taskRows[1]).toHaveTextContent('Alpha Task') // Todo expect(taskRows[2]).toHaveTextContent('Charlie Task') // Backlog expect(taskRows[3]).toHaveTextContent('Beta Task') // Done }) }) }) describe('Empty State', () => { it('should show empty state message when no tasks', () => { mockUseTasks.mockReturnValue({ ...defaultMockUseTasks, tasks: [], }) renderTaskList() // Should show empty state expect(screen.getByText(/No tasks found/i)).toBeInTheDocument() expect( screen.getByText(/Create your first task to get started!/i) ).toBeInTheDocument() // Table headers should not be rendered in empty state expect( screen.queryByRole('button', { name: /title/i }) ).not.toBeInTheDocument() }) }) describe('Mobile Responsive Sorting', () => { it('should show sortable headers on mobile', () => { renderTaskList() // All sortable headers should be visible expect(screen.getByRole('button', { name: /title/i })).toBeInTheDocument() expect( screen.getByRole('button', { name: /status/i }) ).toBeInTheDocument() expect( screen.getByRole('button', { name: /created/i }) ).toBeInTheDocument() // Description column should be visible but not sortable (no button) expect(screen.getByText('Description')).toBeInTheDocument() expect( screen.queryByRole('button', { name: /description/i }) ).not.toBeInTheDocument() }) }) describe('Tooltip Functionality', () => { it('should show cursor help styling on created date with tooltip', () => { renderTaskList() // Find created date elements (they should have relative dates like "2y ago") const dateElements = screen.getAllByText(/ago$|Today|Yesterday/) // Verify at least one date element exists with proper cursor styling expect(dateElements.length).toBeGreaterThan(0) // Check that date elements have cursor help styling dateElements.forEach(element => { expect(element).toHaveStyle('cursor: help') }) }) it('should have tooltip structure in place for completed dates', () => { renderTaskList() // Find the table to ensure it rendered const table = screen.getByRole('table') expect(table).toBeInTheDocument() // Verify that the TaskList component rendered without errors // The tooltip functionality would be tested in e2e tests for actual hover behavior expect(screen.getByText('Beta Task')).toBeInTheDocument() }) }) describe('Loading and Error States', () => { it('should not render sort headers during loading', () => { mockUseTasks.mockReturnValue({ ...defaultMockUseTasks, loading: true, tasks: [], }) renderTaskList() // Should show loading spinner instead of table expect( screen.queryByRole('button', { name: /title/i }) ).not.toBeInTheDocument() expect(screen.getByText('Loading...')).toBeInTheDocument() }) it('should not render sort headers during error state', () => { mockUseTasks.mockReturnValue({ ...defaultMockUseTasks, error: 'Failed to load tasks', tasks: [], }) renderTaskList() // Should show error message instead of table expect( screen.queryByRole('button', { name: /title/i }) ).not.toBeInTheDocument() expect(screen.getByText(/Error loading tasks/i)).toBeInTheDocument() }) }) })