tududi/frontend/components/Task/__tests__/TaskItem.test.tsx
Chris Veleris 3cf9fbe22b Add tests
2025-07-23 12:22:06 +03:00

389 lines
14 KiB
TypeScript

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TaskItem } from '../TaskItem';
import {
mockTask,
mockParentTask,
mockSubtasks,
createMockTask,
} from '@/__tests__/testUtils';
// Mock the fetchSubtasks function
const mockFetchSubtasks = jest.fn();
jest.mock('@/utils/tasksService', () => ({
fetchSubtasks: mockFetchSubtasks,
}));
describe('TaskItem', () => {
const mockProps = {
task: mockTask,
onTaskClick: jest.fn(),
onTaskUpdate: jest.fn(),
onTaskDelete: jest.fn(),
isSelected: false,
showTaskOptions: true,
showProjectInfo: true,
showTags: true,
showDueDate: true,
showPriority: true,
showTodayToggle: true,
showPlayButton: true,
allowEdit: true,
allowDelete: true,
className: '',
priorityIconSize: 'sm' as const,
};
beforeEach(() => {
jest.clearAllMocks();
mockFetchSubtasks.mockResolvedValue([]);
});
describe('Subtasks Display', () => {
it('should not show subtasks by default', () => {
render(<TaskItem {...mockProps} />);
expect(
screen.queryByTestId('subtasks-display')
).not.toBeInTheDocument();
});
it('should show subtasks when showSubtasks is true and task has subtasks', async () => {
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(<TaskItem {...mockProps} task={mockParentTask} />);
// Wait for subtasks to load
await screen.findByText('Subtask 1');
// Click subtasks button to expand
const subtasksButton = screen.getByTitle(/show subtasks/i);
await userEvent.click(subtasksButton);
expect(screen.getByTestId('subtasks-display')).toBeInTheDocument();
});
it('should hide subtasks for archived tasks', () => {
const archivedTask = createMockTask({ status: 'archived' });
render(<TaskItem {...mockProps} task={archivedTask} />);
expect(
screen.queryByTestId('subtasks-display')
).not.toBeInTheDocument();
});
it('should show subtasks for completed tasks', async () => {
const completedTask = createMockTask({ status: 'done' });
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(<TaskItem {...mockProps} task={completedTask} />);
// Should show subtasks button for completed tasks
expect(screen.getByTitle(/show subtasks/i)).toBeInTheDocument();
});
});
describe('Subtasks Loading', () => {
it('should fetch subtasks on component mount', async () => {
render(<TaskItem {...mockProps} task={mockParentTask} />);
expect(mockFetchSubtasks).toHaveBeenCalledWith(mockParentTask.id);
});
it('should show loading state while fetching subtasks', async () => {
mockFetchSubtasks.mockImplementation(
() => new Promise((resolve) => setTimeout(resolve, 100))
);
render(<TaskItem {...mockProps} task={mockParentTask} />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it('should handle subtasks fetch error gracefully', async () => {
mockFetchSubtasks.mockRejectedValue(new Error('Failed to fetch'));
render(<TaskItem {...mockProps} task={mockParentTask} />);
await screen.findByText(/error loading subtasks/i);
});
it('should update hasSubtasks state based on fetched data', async () => {
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(<TaskItem {...mockProps} task={mockParentTask} />);
// Should show subtasks button after loading
await screen.findByTitle(/show subtasks/i);
});
});
describe('Subtasks Toggle', () => {
it('should toggle subtasks visibility when button is clicked', async () => {
const user = userEvent.setup();
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(<TaskItem {...mockProps} task={mockParentTask} />);
const subtasksButton = await screen.findByTitle(/show subtasks/i);
await user.click(subtasksButton);
expect(screen.getByTestId('subtasks-display')).toBeInTheDocument();
// Click again to hide
const hideButton = screen.getByTitle(/hide subtasks/i);
await user.click(hideButton);
expect(
screen.queryByTestId('subtasks-display')
).not.toBeInTheDocument();
});
it('should persist subtasks visibility state', async () => {
const user = userEvent.setup();
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(<TaskItem {...mockProps} task={mockParentTask} />);
const subtasksButton = await screen.findByTitle(/show subtasks/i);
await user.click(subtasksButton);
expect(screen.getByTestId('subtasks-display')).toBeInTheDocument();
// Re-render component - subtasks should still be visible
render(<TaskItem {...mockProps} task={mockParentTask} />);
expect(screen.getByTestId('subtasks-display')).toBeInTheDocument();
});
});
describe('Subtasks Progress Bar', () => {
it('should show progress bar when subtasks are expanded', async () => {
const user = userEvent.setup();
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(<TaskItem {...mockProps} task={mockParentTask} />);
const subtasksButton = await screen.findByTitle(/show subtasks/i);
await user.click(subtasksButton);
expect(screen.getByTestId('subtasks-progress')).toBeInTheDocument();
});
it('should not show progress bar when subtasks are collapsed', async () => {
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(<TaskItem {...mockProps} task={mockParentTask} />);
expect(
screen.queryByTestId('subtasks-progress')
).not.toBeInTheDocument();
});
it('should calculate progress correctly', async () => {
const user = userEvent.setup();
const subtasksWithProgress = [
createMockTask({
id: 2,
name: 'Subtask 1',
parent_task_id: 1,
status: 'done',
}),
createMockTask({
id: 3,
name: 'Subtask 2',
parent_task_id: 1,
status: 'not_started',
}),
createMockTask({
id: 4,
name: 'Subtask 3',
parent_task_id: 1,
status: 'done',
}),
];
mockFetchSubtasks.mockResolvedValue(subtasksWithProgress);
render(<TaskItem {...mockProps} task={mockParentTask} />);
const subtasksButton = await screen.findByTitle(/show subtasks/i);
await user.click(subtasksButton);
// 2 out of 3 subtasks are done = 66.67%
expect(screen.getByText('2 of 3 completed')).toBeInTheDocument();
});
});
describe('Subtasks Interaction', () => {
it('should handle subtask click events', async () => {
const user = userEvent.setup();
const onTaskUpdate = jest.fn();
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(
<TaskItem
{...mockProps}
task={mockParentTask}
onTaskUpdate={onTaskUpdate}
/>
);
const subtasksButton = await screen.findByTitle(/show subtasks/i);
await user.click(subtasksButton);
const subtaskItem = screen.getByText('Subtask 1');
await user.click(subtaskItem);
// Should handle subtask click appropriately
expect(onTaskUpdate).toHaveBeenCalled();
});
it('should prevent task selection when clicking on subtasks area', async () => {
const user = userEvent.setup();
const onTaskClick = jest.fn();
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(
<TaskItem
{...mockProps}
task={mockParentTask}
onTaskClick={onTaskClick}
/>
);
const subtasksButton = await screen.findByTitle(/show subtasks/i);
await user.click(subtasksButton);
const subtasksDisplay = screen.getByTestId('subtasks-display');
await user.click(subtasksDisplay);
// Should not trigger task selection
expect(onTaskClick).not.toHaveBeenCalled();
});
it('should update parent task when subtask is completed', async () => {
const user = userEvent.setup();
const onTaskUpdate = jest.fn();
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(
<TaskItem
{...mockProps}
task={mockParentTask}
onTaskUpdate={onTaskUpdate}
/>
);
const subtasksButton = await screen.findByTitle(/show subtasks/i);
await user.click(subtasksButton);
const subtaskCheckbox = screen.getByTestId('subtask-checkbox-2');
await user.click(subtaskCheckbox);
expect(onTaskUpdate).toHaveBeenCalledWith(
expect.objectContaining({
id: mockParentTask.id,
status: 'done', // Parent should be completed when all subtasks are done
})
);
});
});
describe('Subtasks Layout', () => {
it('should render subtasks with proper indentation', async () => {
const user = userEvent.setup();
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(<TaskItem {...mockProps} task={mockParentTask} />);
const subtasksButton = await screen.findByTitle(/show subtasks/i);
await user.click(subtasksButton);
const subtasksDisplay = screen.getByTestId('subtasks-display');
expect(subtasksDisplay).toHaveClass('ml-6'); // Indented subtasks
});
it('should not show task options for subtasks', async () => {
const user = userEvent.setup();
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(<TaskItem {...mockProps} task={mockParentTask} />);
const subtasksButton = await screen.findByTitle(/show subtasks/i);
await user.click(subtasksButton);
// Subtasks should not have the same options as parent tasks
const subtaskItems = screen.getAllByText(/subtask/i);
expect(subtaskItems).toHaveLength(2);
// Should not show edit/delete options for subtasks
expect(
screen.queryByTestId('subtask-edit-button')
).not.toBeInTheDocument();
});
});
describe('Task Modal Integration', () => {
it('should show subtasks section in task modal', async () => {
const user = userEvent.setup();
mockFetchSubtasks.mockResolvedValue(mockSubtasks);
render(<TaskItem {...mockProps} task={mockParentTask} />);
// Click on task to open modal
const taskHeader = screen.getByText(mockParentTask.name);
await user.click(taskHeader);
// Modal should contain subtasks section
expect(screen.getByText('Subtasks')).toBeInTheDocument();
});
});
describe('Edge Cases', () => {
it('should handle empty subtasks array', async () => {
mockFetchSubtasks.mockResolvedValue([]);
render(<TaskItem {...mockProps} task={mockParentTask} />);
// Should not show subtasks button if no subtasks
await screen.findByText(mockParentTask.name);
expect(
screen.queryByTitle(/show subtasks/i)
).not.toBeInTheDocument();
});
it('should handle subtasks with null parent_task_id', async () => {
const invalidSubtasks = [
createMockTask({
id: 2,
name: 'Invalid Subtask',
parent_task_id: null,
}),
];
mockFetchSubtasks.mockResolvedValue(invalidSubtasks);
render(<TaskItem {...mockProps} task={mockParentTask} />);
// Should handle gracefully and not crash
await screen.findByText(mockParentTask.name);
});
it('should handle very long subtask names', async () => {
const user = userEvent.setup();
const longSubtask = createMockTask({
id: 2,
name: 'This is a very long subtask name that should be handled properly without breaking the layout',
parent_task_id: 1,
});
mockFetchSubtasks.mockResolvedValue([longSubtask]);
render(<TaskItem {...mockProps} task={mockParentTask} />);
const subtasksButton = await screen.findByTitle(/show subtasks/i);
await user.click(subtasksButton);
expect(screen.getByText(longSubtask.name)).toBeInTheDocument();
});
});
});