Remove broken frontend tests

This commit is contained in:
antanst 2025-07-17 12:38:24 +03:00 committed by Chris
parent 3cf9fbe22b
commit 2d4e65bb6e
5 changed files with 0 additions and 1394 deletions

View file

@ -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;
});

View file

@ -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 <div>{children}</div>;
};
const customRender = (
ui: React.ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => 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> = {}): 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',
}));
};

View file

@ -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(<TaskHeader {...mockProps} hasSubtasks={false} />);
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(<TaskHeader {...mockProps} hasSubtasks={true} />);
expect(screen.getByTitle(/show subtasks/i)).toBeInTheDocument();
});
it('should show hide subtasks button when subtasks are expanded', () => {
render(
<TaskHeader
{...mockProps}
hasSubtasks={true}
showSubtasks={true}
/>
);
expect(screen.getByTitle(/hide subtasks/i)).toBeInTheDocument();
});
it('should show subtasks button for completed tasks', () => {
render(
<TaskHeader
{...mockProps}
task={mockCompletedTask}
hasSubtasks={true}
/>
);
expect(screen.getByTitle(/show subtasks/i)).toBeInTheDocument();
});
it('should not show subtasks button for archived tasks', () => {
const archivedTask = createMockTask({ status: 'archived' });
render(
<TaskHeader
{...mockProps}
task={archivedTask}
hasSubtasks={true}
/>
);
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(
<TaskHeader
{...mockProps}
hasSubtasks={true}
onSubtasksToggle={onSubtasksToggle}
/>
);
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(
<TaskHeader
{...mockProps}
hasSubtasks={true}
onTaskClick={onTaskClick}
onSubtasksToggle={onSubtasksToggle}
/>
);
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(<TaskHeader {...mockProps} hasSubtasks={true} />);
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(
<TaskHeader
{...mockProps}
hasSubtasks={true}
showSubtasks={true}
/>
);
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(
<TaskHeader
{...mockProps}
hasSubtasks={true}
showSubtasks={false}
/>
);
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(
<TaskHeader
{...mockProps}
hasSubtasks={true}
showSubtasks={false}
/>
);
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(<TaskHeader {...mockProps} hasSubtasks={true} />);
// 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(
<TaskHeader
{...mockProps}
hasSubtasks={true}
onSubtasksToggle={onSubtasksToggle}
/>
);
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(<TaskHeader {...mockProps} hasSubtasks={true} />);
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(
<TaskHeader
{...mockProps}
task={taskWithProject}
hasSubtasks={true}
/>
);
// 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(
<TaskHeader
{...mockProps}
task={mockCompletedTask}
hasSubtasks={true}
/>
);
// 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(<TaskHeader {...mockProps} hasSubtasks={true} />);
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(
<TaskHeader
{...mockProps}
hasSubtasks={true}
showSubtasks={true}
/>
);
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(
<TaskHeader
{...mockProps}
hasSubtasks={true}
onSubtasksToggle={onSubtasksToggle}
/>
);
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(<TaskHeader {...mockProps} hasSubtasks={true} />);
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(<TaskHeader {...mockProps} hasSubtasks={true} />);
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(<TaskHeader {...mockProps} hasSubtasks={true} />);
const subtasksButton = screen.getByTitle(/show subtasks/i);
const buttonContainer = subtasksButton.closest('.flex');
expect(buttonContainer).toHaveClass('items-center');
});
});
});

View file

@ -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(<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();
});
});
});

View file

@ -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(<TaskSubtasksSection {...mockProps} />);
expect(screen.getByText('Subtasks')).toBeInTheDocument();
});
it('should render add subtask button', () => {
render(<TaskSubtasksSection {...mockProps} />);
expect(
screen.getByRole('button', { name: /add subtask/i })
).toBeInTheDocument();
});
it('should render existing subtasks', () => {
const propsWithSubtasks = {
...mockProps,
subtasks: mockSubtasks,
};
render(<TaskSubtasksSection {...propsWithSubtasks} />);
expect(screen.getByDisplayValue('Subtask 1')).toBeInTheDocument();
expect(screen.getByDisplayValue('Subtask 2')).toBeInTheDocument();
});
it('should show empty state when no subtasks', () => {
render(<TaskSubtasksSection {...mockProps} />);
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(
<TaskSubtasksSection
{...mockProps}
onSubtasksChange={onSubtasksChange}
/>
);
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(
<TaskSubtasksSection
{...mockProps}
onSubtasksChange={onSubtasksChange}
/>
);
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(
<TaskSubtasksSection
{...mockProps}
subtasks={[newSubtask] as any}
/>
);
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(
<TaskSubtasksSection
{...mockProps}
subtasks={[newSubtask] as any}
onSubtasksChange={onSubtasksChange}
/>
);
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(
<TaskSubtasksSection
{...mockProps}
subtasks={mockSubtasks}
onSubtasksChange={onSubtasksChange}
/>
);
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(
<TaskSubtasksSection
{...mockProps}
subtasks={mockSubtasks}
onSubtasksChange={onSubtasksChange}
/>
);
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(
<TaskSubtasksSection {...mockProps} subtasks={mockSubtasks} />
);
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(
<TaskSubtasksSection
{...mockProps}
subtasks={mockSubtasks}
onSubtasksChange={onSubtasksChange}
/>
);
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(
<TaskSubtasksSection
{...mockProps}
subtasks={[newSubtask] as any}
onSubtasksChange={onSubtasksChange}
/>
);
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(
<TaskSubtasksSection
{...mockProps}
subtasks={[subtaskWithEmptyName]}
/>
);
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(
<TaskSubtasksSection
{...mockProps}
subtasks={duplicateSubtasks}
/>
);
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(
<TaskSubtasksSection
{...mockProps}
subtasks={[subtaskWithEmptyName]}
/>
);
// 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(
<TaskSubtasksSection
{...mockProps}
subtasks={mockSubtasks}
isFormSubmitting={true}
/>
);
const inputs = screen.getAllByRole('textbox');
inputs.forEach((input) => {
expect(input).toBeDisabled();
});
});
it('should disable add button when form is submitting', () => {
render(
<TaskSubtasksSection {...mockProps} isFormSubmitting={true} />
);
const addButton = screen.getByRole('button', {
name: /add subtask/i,
});
expect(addButton).toBeDisabled();
});
it('should disable remove buttons when form is submitting', () => {
render(
<TaskSubtasksSection
{...mockProps}
subtasks={mockSubtasks}
isFormSubmitting={true}
/>
);
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(
<TaskSubtasksSection {...mockProps} subtasks={mockSubtasks} />
);
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(
<TaskSubtasksSection
{...mockProps}
onSubtasksChange={onSubtasksChange}
/>
);
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(
<TaskSubtasksSection {...mockProps} subtasks={mockSubtasks} />
);
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(
<TaskSubtasksSection {...mockProps} subtasks={mockSubtasks} />
);
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(
<TaskSubtasksSection
{...mockProps}
subtasks={[subtaskWithEmptyName]}
/>
);
const errorMessage = screen.getByText(/subtask name is required/i);
expect(errorMessage).toHaveAttribute('role', 'alert');
});
});
});