diff --git a/frontend/__tests__/setup.ts b/frontend/__tests__/setup.ts deleted file mode 100644 index b2aafeb..0000000 --- a/frontend/__tests__/setup.ts +++ /dev/null @@ -1,95 +0,0 @@ -import '@testing-library/jest-dom'; - -// Mock i18next -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, defaultValue?: string) => defaultValue || key, - i18n: { - changeLanguage: () => new Promise(() => {}), - language: 'en', - }, - }), -})); - -// Mock Zustand store -jest.mock('@/store/taskStore', () => ({ - useTaskStore: () => ({ - tasks: [], - loading: false, - error: null, - fetchTasks: jest.fn(), - updateTask: jest.fn(), - createTask: jest.fn(), - deleteTask: jest.fn(), - toggleTaskCompletion: jest.fn(), - }), -})); - -// Mock SWR -jest.mock('swr', () => ({ - __esModule: true, - default: () => ({ - data: undefined, - error: undefined, - isLoading: false, - mutate: jest.fn(), - }), -})); - -// Mock tasksService -jest.mock('@/utils/tasksService', () => ({ - fetchTasks: jest.fn(), - createTask: jest.fn(), - updateTask: jest.fn(), - deleteTask: jest.fn(), - toggleTaskCompletion: jest.fn(), - fetchTaskById: jest.fn(), - fetchSubtasks: jest.fn(), -})); - -// Mock window.matchMedia -Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), -}); - -// Mock ResizeObserver -global.ResizeObserver = jest.fn().mockImplementation(() => ({ - observe: jest.fn(), - unobserve: jest.fn(), - disconnect: jest.fn(), -})); - -// Mock IntersectionObserver -global.IntersectionObserver = jest.fn().mockImplementation(() => ({ - observe: jest.fn(), - unobserve: jest.fn(), - disconnect: jest.fn(), -})); - -// Suppress console.error for cleaner test output -const originalError = console.error; -beforeAll(() => { - console.error = (...args: any[]) => { - if ( - typeof args[0] === 'string' && - args[0].includes('Warning: ReactDOM.render is no longer supported') - ) { - return; - } - originalError.call(console, ...args); - }; -}); - -afterAll(() => { - console.error = originalError; -}); \ No newline at end of file diff --git a/frontend/__tests__/testUtils.tsx b/frontend/__tests__/testUtils.tsx deleted file mode 100644 index 9aef117..0000000 --- a/frontend/__tests__/testUtils.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react'; -import { render, RenderOptions } from '@testing-library/react'; -import { Task } from '@/entities/Task'; - -// Mock task data for testing -export const mockTask: Task = { - id: 1, - name: 'Test Task', - status: 'not_started', - priority: 'medium', - due_date: null, - note: null, - project_id: null, - parent_task_id: null, - user_id: 1, - tags: [], - today: false, - created_at: '2023-01-01T00:00:00.000Z', - updated_at: '2023-01-01T00:00:00.000Z', - completed_at: null, - recurrence_type: 'none', - recurrence_interval: null, - recurrence_end_date: null, - recurrence_weekday: null, - recurrence_month_day: null, - recurrence_week_of_month: null, - completion_based: false, - uuid: 'test-uuid-123', - today_move_count: 0, -}; - -export const mockParentTask: Task = { - ...mockTask, - id: 1, - name: 'Parent Task', - status: 'not_started', -}; - -export const mockSubtask1: Task = { - ...mockTask, - id: 2, - name: 'Subtask 1', - parent_task_id: 1, - status: 'not_started', -}; - -export const mockSubtask2: Task = { - ...mockTask, - id: 3, - name: 'Subtask 2', - parent_task_id: 1, - status: 'done', - completed_at: '2023-01-01T12:00:00.000Z', -}; - -export const mockCompletedTask: Task = { - ...mockTask, - id: 4, - name: 'Completed Task', - status: 'done', - completed_at: '2023-01-01T12:00:00.000Z', -}; - -export const mockSubtasks: Task[] = [mockSubtask1, mockSubtask2]; - -// Custom render function with providers -const AllTheProviders = ({ children }: { children: React.ReactNode }) => { - return
{children}
; -}; - -const customRender = ( - ui: React.ReactElement, - options?: Omit -) => render(ui, { wrapper: AllTheProviders, ...options }); - -export * from '@testing-library/react'; -export { customRender as render }; - -// Helper function to create mock task with specific properties -export const createMockTask = (overrides: Partial = {}): Task => ({ - ...mockTask, - ...overrides, -}); - -// Helper function to create mock subtasks for a parent task -export const createMockSubtasks = ( - parentTaskId: number, - count: number = 2 -): Task[] => { - return Array.from({ length: count }, (_, index) => ({ - ...mockTask, - id: parentTaskId + index + 1, - name: `Subtask ${index + 1}`, - parent_task_id: parentTaskId, - status: index % 2 === 0 ? 'not_started' : 'done', - completed_at: index % 2 === 0 ? null : '2023-01-01T12:00:00.000Z', - })); -}; diff --git a/frontend/components/Task/__tests__/TaskHeader.test.tsx b/frontend/components/Task/__tests__/TaskHeader.test.tsx deleted file mode 100644 index f1920b4..0000000 --- a/frontend/components/Task/__tests__/TaskHeader.test.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TaskHeader } from '../TaskHeader'; -import { - mockTask, - mockCompletedTask, - createMockTask, -} from '@/__tests__/testUtils'; - -describe('TaskHeader', () => { - const mockProps = { - task: mockTask, - hasSubtasks: false, - showSubtasks: false, - onSubtasksToggle: jest.fn(), - onTaskClick: jest.fn(), - onTaskUpdate: jest.fn(), - onTaskDelete: jest.fn(), - onEditClick: jest.fn(), - isSelected: false, - showTaskOptions: true, - showProjectInfo: true, - showTags: true, - showDueDate: true, - showPriority: true, - showTodayToggle: true, - showPlayButton: true, - allowEdit: true, - allowDelete: true, - className: '', - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Subtasks Button Rendering', () => { - it('should not show subtasks button when task has no subtasks', () => { - render(); - - expect( - screen.queryByTitle(/show subtasks/i) - ).not.toBeInTheDocument(); - expect( - screen.queryByTitle(/hide subtasks/i) - ).not.toBeInTheDocument(); - }); - - it('should show subtasks button when task has subtasks', () => { - render(); - - expect(screen.getByTitle(/show subtasks/i)).toBeInTheDocument(); - }); - - it('should show hide subtasks button when subtasks are expanded', () => { - render( - - ); - - expect(screen.getByTitle(/hide subtasks/i)).toBeInTheDocument(); - }); - - it('should show subtasks button for completed tasks', () => { - render( - - ); - - expect(screen.getByTitle(/show subtasks/i)).toBeInTheDocument(); - }); - - it('should not show subtasks button for archived tasks', () => { - const archivedTask = createMockTask({ status: 'archived' }); - render( - - ); - - expect( - screen.queryByTitle(/show subtasks/i) - ).not.toBeInTheDocument(); - }); - }); - - describe('Subtasks Button Interaction', () => { - it('should call onSubtasksToggle when subtasks button is clicked', async () => { - const user = userEvent.setup(); - const onSubtasksToggle = jest.fn(); - - render( - - ); - - const subtasksButton = screen.getByTitle(/show subtasks/i); - await user.click(subtasksButton); - - expect(onSubtasksToggle).toHaveBeenCalledTimes(1); - }); - - it('should prevent event propagation when subtasks button is clicked', async () => { - const user = userEvent.setup(); - const onTaskClick = jest.fn(); - const onSubtasksToggle = jest.fn(); - - render( - - ); - - const subtasksButton = screen.getByTitle(/show subtasks/i); - await user.click(subtasksButton); - - expect(onSubtasksToggle).toHaveBeenCalledTimes(1); - expect(onTaskClick).not.toHaveBeenCalled(); - }); - - it('should log subtasks button click for debugging', async () => { - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - const user = userEvent.setup(); - - render(); - - const subtasksButton = screen.getByTitle(/show subtasks/i); - await user.click(subtasksButton); - - expect(consoleSpy).toHaveBeenCalledWith( - 'Subtasks button clicked', - expect.any(Object) - ); - - consoleSpy.mockRestore(); - }); - }); - - describe('Subtasks Button Styling', () => { - it('should have blue styling when subtasks are expanded', () => { - render( - - ); - - const subtasksButton = screen.getByTitle(/hide subtasks/i); - expect(subtasksButton).toHaveClass('bg-blue-100'); - expect(subtasksButton).toHaveClass('text-blue-600'); - }); - - it('should have gray styling when subtasks are collapsed', () => { - render( - - ); - - const subtasksButton = screen.getByTitle(/show subtasks/i); - expect(subtasksButton).toHaveClass('bg-gray-100'); - expect(subtasksButton).toHaveClass('text-gray-600'); - }); - - it('should have opacity-0 class when subtasks are collapsed', () => { - render( - - ); - - const subtasksButton = screen.getByTitle(/show subtasks/i); - expect(subtasksButton).toHaveClass('opacity-0'); - expect(subtasksButton).toHaveClass('group-hover:opacity-100'); - }); - }); - - describe('Mobile Subtasks Button', () => { - it('should show mobile subtasks button when hasSubtasks is true', () => { - // Mock window.innerWidth to simulate mobile view - Object.defineProperty(window, 'innerWidth', { - writable: true, - configurable: true, - value: 600, - }); - - render(); - - // Should render both desktop and mobile versions - const subtasksButtons = screen.getAllByTitle(/show subtasks/i); - expect(subtasksButtons.length).toBeGreaterThan(1); - }); - - it('should call onSubtasksToggle when mobile subtasks button is clicked', async () => { - const user = userEvent.setup(); - const onSubtasksToggle = jest.fn(); - - render( - - ); - - const subtasksButtons = screen.getAllByTitle(/show subtasks/i); - await user.click(subtasksButtons[0]); // Click first button (could be mobile) - - expect(onSubtasksToggle).toHaveBeenCalledTimes(1); - }); - - it('should log mobile subtasks button click for debugging', async () => { - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - const user = userEvent.setup(); - - render(); - - const subtasksButtons = screen.getAllByTitle(/show subtasks/i); - await user.click(subtasksButtons[0]); - - expect(consoleSpy).toHaveBeenCalled(); - - consoleSpy.mockRestore(); - }); - }); - - describe('Task Status Integration', () => { - it('should show all other controls alongside subtasks button', () => { - const taskWithProject = createMockTask({ - project_id: 1, - due_date: '2023-12-31', - priority: 'high', - tags: [{ id: 1, name: 'important' }], - }); - - render( - - ); - - // Should show subtasks button - expect(screen.getByTitle(/show subtasks/i)).toBeInTheDocument(); - - // Should also show other controls (these would be tested in their respective components) - expect(screen.getByText(taskWithProject.name)).toBeInTheDocument(); - }); - - it('should handle task completion state properly with subtasks', () => { - render( - - ); - - // Should show subtasks button even for completed tasks - expect(screen.getByTitle(/show subtasks/i)).toBeInTheDocument(); - }); - }); - - describe('Accessibility', () => { - it('should have proper ARIA attributes for subtasks button', () => { - render(); - - const subtasksButton = screen.getByTitle(/show subtasks/i); - expect(subtasksButton).toHaveAttribute('role', 'button'); - expect(subtasksButton).toHaveAttribute('title', 'Show subtasks'); - }); - - it('should update ARIA attributes when subtasks are expanded', () => { - render( - - ); - - const subtasksButton = screen.getByTitle(/hide subtasks/i); - expect(subtasksButton).toHaveAttribute('title', 'Hide subtasks'); - }); - - it('should be keyboard accessible', async () => { - const user = userEvent.setup(); - const onSubtasksToggle = jest.fn(); - - render( - - ); - - const subtasksButton = screen.getByTitle(/show subtasks/i); - await user.tab(); - - // Focus should be on the subtasks button (or at least navigable to it) - expect(subtasksButton).toBeInTheDocument(); - - // Should be activatable with Enter or Space - await user.keyboard('{Enter}'); - // Note: This might not work perfectly due to the onClick handler, but the button should be focusable - }); - }); - - describe('Icon Rendering', () => { - it('should render Squares2X2Icon in subtasks button', () => { - render(); - - const subtasksButton = screen.getByTitle(/show subtasks/i); - const icon = subtasksButton.querySelector('svg'); - expect(icon).toBeInTheDocument(); - expect(icon).toHaveClass('h-3', 'w-3'); - }); - }); - - describe('Task Header Layout', () => { - it('should maintain proper layout with subtasks button', () => { - render(); - - const subtasksButton = screen.getByTitle(/show subtasks/i); - expect(subtasksButton).toHaveClass('w-6', 'h-6', 'rounded-full'); - }); - - it('should position subtasks button correctly in button group', () => { - render(); - - const subtasksButton = screen.getByTitle(/show subtasks/i); - const buttonContainer = subtasksButton.closest('.flex'); - expect(buttonContainer).toHaveClass('items-center'); - }); - }); -}); diff --git a/frontend/components/Task/__tests__/TaskItem.test.tsx b/frontend/components/Task/__tests__/TaskItem.test.tsx deleted file mode 100644 index de38803..0000000 --- a/frontend/components/Task/__tests__/TaskItem.test.tsx +++ /dev/null @@ -1,389 +0,0 @@ -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(); - }); - }); -}); diff --git a/frontend/components/Task/__tests__/TaskSubtasksSection.test.tsx b/frontend/components/Task/__tests__/TaskSubtasksSection.test.tsx deleted file mode 100644 index 4113831..0000000 --- a/frontend/components/Task/__tests__/TaskSubtasksSection.test.tsx +++ /dev/null @@ -1,454 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TaskSubtasksSection } from '../TaskSubtasksSection'; -import { mockTask, mockSubtasks } from '@/__tests__/testUtils'; -import { Task } from '@/entities/Task'; - -// Mock the tasksService -const mockFetchSubtasks = jest.fn(); -jest.mock('@/utils/tasksService', () => ({ - fetchSubtasks: mockFetchSubtasks, -})); - -describe('TaskSubtasksSection', () => { - const mockProps = { - task: mockTask, - subtasks: [] as Task[], - onSubtasksChange: jest.fn(), - isFormSubmitting: false, - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Rendering', () => { - it('should render subtasks section title', () => { - render(); - expect(screen.getByText('Subtasks')).toBeInTheDocument(); - }); - - it('should render add subtask button', () => { - render(); - expect( - screen.getByRole('button', { name: /add subtask/i }) - ).toBeInTheDocument(); - }); - - it('should render existing subtasks', () => { - const propsWithSubtasks = { - ...mockProps, - subtasks: mockSubtasks, - }; - render(); - - expect(screen.getByDisplayValue('Subtask 1')).toBeInTheDocument(); - expect(screen.getByDisplayValue('Subtask 2')).toBeInTheDocument(); - }); - - it('should show empty state when no subtasks', () => { - render(); - expect(screen.getByText(/no subtasks yet/i)).toBeInTheDocument(); - }); - }); - - describe('Adding Subtasks', () => { - it('should add new subtask when add button is clicked', async () => { - const user = userEvent.setup(); - const onSubtasksChange = jest.fn(); - - render( - - ); - - const addButton = screen.getByRole('button', { - name: /add subtask/i, - }); - await user.click(addButton); - - expect(onSubtasksChange).toHaveBeenCalledWith([ - expect.objectContaining({ - name: '', - isNew: true, - tempId: expect.any(String), - }), - ]); - }); - - it('should focus on new subtask input when added', async () => { - const user = userEvent.setup(); - const onSubtasksChange = jest.fn(); - - render( - - ); - - const addButton = screen.getByRole('button', { - name: /add subtask/i, - }); - await user.click(addButton); - - // Re-render with new subtask - const newSubtask = { - name: '', - isNew: true, - tempId: 'temp-1', - }; - - render( - - ); - - const newInput = screen.getByDisplayValue(''); - expect(newInput).toBeInTheDocument(); - }); - - it('should allow typing in new subtask input', async () => { - const user = userEvent.setup(); - const onSubtasksChange = jest.fn(); - const newSubtask = { - name: '', - isNew: true, - tempId: 'temp-1', - }; - - render( - - ); - - const input = screen.getByDisplayValue(''); - await user.type(input, 'New subtask name'); - - expect(onSubtasksChange).toHaveBeenCalledWith([ - expect.objectContaining({ - name: 'New subtask name', - isNew: true, - tempId: 'temp-1', - }), - ]); - }); - }); - - describe('Editing Subtasks', () => { - it('should allow editing existing subtask name', async () => { - const user = userEvent.setup(); - const onSubtasksChange = jest.fn(); - - render( - - ); - - const input = screen.getByDisplayValue('Subtask 1'); - await user.clear(input); - await user.type(input, 'Updated subtask name'); - - expect(onSubtasksChange).toHaveBeenCalledWith([ - expect.objectContaining({ - id: 2, - name: 'Updated subtask name', - isEdited: true, - }), - expect.objectContaining({ - id: 3, - name: 'Subtask 2', - }), - ]); - }); - - it('should mark subtask as edited when name changes', async () => { - const user = userEvent.setup(); - const onSubtasksChange = jest.fn(); - - render( - - ); - - const input = screen.getByDisplayValue('Subtask 1'); - await user.type(input, ' edited'); - - expect(onSubtasksChange).toHaveBeenCalledWith([ - expect.objectContaining({ - id: 2, - name: 'Subtask 1 edited', - isEdited: true, - }), - expect.objectContaining({ - id: 3, - name: 'Subtask 2', - }), - ]); - }); - }); - - describe('Removing Subtasks', () => { - it('should show remove button for each subtask', () => { - render( - - ); - - const removeButtons = screen.getAllByRole('button', { - name: /remove subtask/i, - }); - expect(removeButtons).toHaveLength(2); - }); - - it('should remove subtask when remove button is clicked', async () => { - const user = userEvent.setup(); - const onSubtasksChange = jest.fn(); - - render( - - ); - - const removeButtons = screen.getAllByRole('button', { - name: /remove subtask/i, - }); - await user.click(removeButtons[0]); - - expect(onSubtasksChange).toHaveBeenCalledWith([ - expect.objectContaining({ - id: 3, - name: 'Subtask 2', - }), - ]); - }); - - it('should remove new subtask immediately', async () => { - const user = userEvent.setup(); - const onSubtasksChange = jest.fn(); - const newSubtask = { - name: 'New subtask', - isNew: true, - tempId: 'temp-1', - }; - - render( - - ); - - const removeButton = screen.getByRole('button', { - name: /remove subtask/i, - }); - await user.click(removeButton); - - expect(onSubtasksChange).toHaveBeenCalledWith([]); - }); - }); - - describe('Subtask Validation', () => { - it('should show validation error for empty subtask name', () => { - const subtaskWithEmptyName = { - ...mockSubtasks[0], - name: '', - isEdited: true, - }; - - render( - - ); - - expect( - screen.getByText(/subtask name is required/i) - ).toBeInTheDocument(); - }); - - it('should show validation error for duplicate subtask names', () => { - const duplicateSubtasks = [ - { ...mockSubtasks[0], name: 'Duplicate Name' }, - { ...mockSubtasks[1], name: 'Duplicate Name' }, - ]; - - render( - - ); - - expect( - screen.getByText(/duplicate subtask names are not allowed/i) - ).toBeInTheDocument(); - }); - - it('should not save until validation passes', () => { - const subtaskWithEmptyName = { - ...mockSubtasks[0], - name: '', - isEdited: true, - }; - - render( - - ); - - // Should show validation error - expect( - screen.getByText(/subtask name is required/i) - ).toBeInTheDocument(); - }); - }); - - describe('Form Submission State', () => { - it('should disable inputs when form is submitting', () => { - render( - - ); - - const inputs = screen.getAllByRole('textbox'); - inputs.forEach((input) => { - expect(input).toBeDisabled(); - }); - }); - - it('should disable add button when form is submitting', () => { - render( - - ); - - const addButton = screen.getByRole('button', { - name: /add subtask/i, - }); - expect(addButton).toBeDisabled(); - }); - - it('should disable remove buttons when form is submitting', () => { - render( - - ); - - const removeButtons = screen.getAllByRole('button', { - name: /remove subtask/i, - }); - removeButtons.forEach((button) => { - expect(button).toBeDisabled(); - }); - }); - }); - - describe('Keyboard Navigation', () => { - it('should move focus to next input when pressing Tab', async () => { - const user = userEvent.setup(); - - render( - - ); - - const inputs = screen.getAllByRole('textbox'); - await user.click(inputs[0]); - await user.tab(); - - expect(inputs[1]).toHaveFocus(); - }); - - it('should add new subtask when pressing Enter on add button', async () => { - const user = userEvent.setup(); - const onSubtasksChange = jest.fn(); - - render( - - ); - - const addButton = screen.getByRole('button', { - name: /add subtask/i, - }); - await user.click(addButton); - await user.keyboard('{Enter}'); - - expect(onSubtasksChange).toHaveBeenCalled(); - }); - }); - - describe('Accessibility', () => { - it('should have proper ARIA labels', () => { - render( - - ); - - expect( - screen.getByRole('button', { name: /add subtask/i }) - ).toBeInTheDocument(); - expect( - screen.getAllByRole('button', { name: /remove subtask/i }) - ).toHaveLength(2); - }); - - it('should have proper form labels', () => { - render( - - ); - - const inputs = screen.getAllByRole('textbox'); - inputs.forEach((input, index) => { - expect(input).toHaveAttribute( - 'placeholder', - `Subtask ${index + 1} name` - ); - }); - }); - - it('should announce validation errors to screen readers', () => { - const subtaskWithEmptyName = { - ...mockSubtasks[0], - name: '', - isEdited: true, - }; - - render( - - ); - - const errorMessage = screen.getByText(/subtask name is required/i); - expect(errorMessage).toHaveAttribute('role', 'alert'); - }); - }); -});