380 lines
12 KiB
TypeScript
380 lines
12 KiB
TypeScript
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<typeof vi.fn>
|
|
|
|
// 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(
|
|
<MemoryRouter>
|
|
<TaskList initialTasks={initialTasks} />
|
|
</MemoryRouter>
|
|
)
|
|
}
|
|
|
|
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()
|
|
})
|
|
})
|
|
})
|