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(); expect( screen.queryByTestId('subtasks-display') ).not.toBeInTheDocument(); }); it('should show subtasks when showSubtasks is true and task has subtasks', async () => { mockFetchSubtasks.mockResolvedValue(mockSubtasks); render(); // 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(); expect( screen.queryByTestId('subtasks-display') ).not.toBeInTheDocument(); }); it('should show subtasks for completed tasks', async () => { const completedTask = createMockTask({ status: 'done' }); mockFetchSubtasks.mockResolvedValue(mockSubtasks); render(); // 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(); expect(mockFetchSubtasks).toHaveBeenCalledWith(mockParentTask.id); }); it('should show loading state while fetching subtasks', async () => { mockFetchSubtasks.mockImplementation( () => new Promise((resolve) => setTimeout(resolve, 100)) ); render(); expect(screen.getByText(/loading/i)).toBeInTheDocument(); }); it('should handle subtasks fetch error gracefully', async () => { mockFetchSubtasks.mockRejectedValue(new Error('Failed to fetch')); render(); await screen.findByText(/error loading subtasks/i); }); it('should update hasSubtasks state based on fetched data', async () => { mockFetchSubtasks.mockResolvedValue(mockSubtasks); render(); // 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(); 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(); 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(); 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(); 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(); 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(); 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( ); 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( ); 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( ); 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(); 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(); 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(); // 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(); // 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(); // 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(); const subtasksButton = await screen.findByTitle(/show subtasks/i); await user.click(subtasksButton); expect(screen.getByText(longSubtask.name)).toBeInTheDocument(); }); }); });