tududi/frontend/__tests__/integration/taskWorkflows.test.tsx
Chris 3c1209a5a9
Express migration (#80)
* Initial migration

* Cleanup and create migration scripts

* Introduce test suite

* Fix test issues

* Correct CORS issue and update paths

* Update README
2025-06-16 21:50:44 +03:00

413 lines
No EOL
11 KiB
TypeScript

import React from 'react';
import { render, screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BrowserRouter } from 'react-router-dom';
import TaskList from '../../components/Task/TaskList';
import TaskModal from '../../components/Task/TaskModal';
import { Task } from '../../entities/Task';
import { Project } from '../../entities/Project';
// Mock data
const mockTasks: Task[] = [
{
id: 1,
name: 'Complete project setup',
status: 'not_started',
priority: 'high',
created_at: '2024-01-01T00:00:00.000Z'
},
{
id: 2,
name: 'Write documentation',
status: 'in_progress',
priority: 'medium',
due_date: '2024-12-25',
created_at: '2024-01-02T00:00:00.000Z'
}
];
const mockProjects: Project[] = [
{
id: 1,
name: 'Test Project',
active: true
}
];
// Integration test wrapper component
const TaskWorkflowWrapper: React.FC<{
tasks: Task[];
onTaskUpdate: (task: Task) => void;
onTaskDelete: (taskId: number) => void;
}> = ({ tasks, onTaskUpdate, onTaskDelete }) => {
const [isModalOpen, setIsModalOpen] = React.useState(false);
const [selectedTask, setSelectedTask] = React.useState<Task | null>(null);
const handleTaskClick = (task: Task) => {
setSelectedTask(task);
setIsModalOpen(true);
};
const handleModalSave = (updatedTask: Task) => {
onTaskUpdate(updatedTask);
setIsModalOpen(false);
};
const handleModalClose = () => {
setIsModalOpen(false);
setSelectedTask(null);
};
const handleModalDelete = (taskId: number) => {
onTaskDelete(taskId);
setIsModalOpen(false);
setSelectedTask(null);
};
const CustomTaskList: React.FC<any> = (props) => (
<div>
{props.tasks.map((task: Task) => (
<div key={task.id} onClick={() => handleTaskClick(task)} style={{ cursor: 'pointer', padding: '10px', border: '1px solid #ccc', margin: '5px' }}>
<div>{task.name}</div>
<div>Status: {task.status}</div>
<div>Priority: {task.priority}</div>
</div>
))}
</div>
);
return (
<BrowserRouter>
<div>
<CustomTaskList {...{ tasks, onTaskUpdate, onTaskDelete }} />
{isModalOpen && selectedTask && (
<TaskModal
isOpen={isModalOpen}
onClose={handleModalClose}
task={selectedTask}
onSave={handleModalSave}
onDelete={handleModalDelete}
projects={mockProjects}
onCreateProject={async (name: string) => ({ id: 999, name, active: true })}
/>
)}
</div>
</BrowserRouter>
);
};
describe('Task Workflows Integration Tests', () => {
let mockOnTaskUpdate: jest.Mock;
let mockOnTaskDelete: jest.Mock;
beforeEach(() => {
mockOnTaskUpdate = jest.fn();
mockOnTaskDelete = jest.fn();
jest.clearAllMocks();
// Mock fetch for tags service
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
json: async () => ({ tags: [] })
});
});
it('completes full task lifecycle workflow', async () => {
const user = userEvent.setup();
render(
<TaskWorkflowWrapper
tasks={mockTasks}
onTaskUpdate={mockOnTaskUpdate}
onTaskDelete={mockOnTaskDelete}
/>
);
// 1. View tasks in list
expect(screen.getByText('Complete project setup')).toBeInTheDocument();
expect(screen.getByText('Write documentation')).toBeInTheDocument();
// 2. Click on a task to open modal
const firstTask = screen.getByText('Complete project setup');
await act(async () => {
await user.click(firstTask);
});
// 3. Modal should open with task details
await waitFor(() => {
expect(screen.getByDisplayValue('Complete project setup')).toBeInTheDocument();
});
// 4. Edit task name
const nameInput = screen.getByDisplayValue('Complete project setup');
await act(async () => {
await user.clear(nameInput);
await user.type(nameInput, 'Updated project setup');
});
// 5. Save changes
const saveButton = screen.getByText(/save/i);
await act(async () => {
await user.click(saveButton);
});
// 6. Verify onTaskUpdate was called
expect(mockOnTaskUpdate).toHaveBeenCalledWith(
expect.objectContaining({
id: 1,
name: 'Updated project setup'
})
);
});
it('handles task deletion workflow', async () => {
const user = userEvent.setup();
render(
<TaskWorkflowWrapper
tasks={mockTasks}
onTaskUpdate={mockOnTaskUpdate}
onTaskDelete={mockOnTaskDelete}
/>
);
// 1. Click on a task to open modal
const firstTask = screen.getByText('Complete project setup');
await act(async () => {
await user.click(firstTask);
});
// 2. Click delete button
await waitFor(() => {
expect(screen.getByText(/delete/i)).toBeInTheDocument();
});
const deleteButton = screen.getByText(/delete/i);
await act(async () => {
await user.click(deleteButton);
});
// 3. Confirm deletion
await waitFor(() => {
expect(screen.getByText(/are you sure/i)).toBeInTheDocument();
});
const confirmButton = screen.getByText(/confirm/i);
await act(async () => {
await user.click(confirmButton);
});
// 4. Verify onTaskDelete was called
expect(mockOnTaskDelete).toHaveBeenCalledWith(1);
});
it('handles task status and priority updates', async () => {
const user = userEvent.setup();
render(
<TaskWorkflowWrapper
tasks={mockTasks}
onTaskUpdate={mockOnTaskUpdate}
onTaskDelete={mockOnTaskDelete}
/>
);
// Open task modal
const secondTask = screen.getByText('Write documentation');
await act(async () => {
await user.click(secondTask);
});
await waitFor(() => {
expect(screen.getByDisplayValue('Write documentation')).toBeInTheDocument();
});
// Change status and priority using dropdowns
const statusDropdown = screen.getByText(/status/i);
expect(statusDropdown).toBeInTheDocument();
const priorityDropdown = screen.getByText(/priority/i);
expect(priorityDropdown).toBeInTheDocument();
// Save changes
const saveButton = screen.getByText(/save/i);
await act(async () => {
await user.click(saveButton);
});
// Verify update was called
expect(mockOnTaskUpdate).toHaveBeenCalled();
});
it('handles due date modifications', async () => {
const user = userEvent.setup();
render(
<TaskWorkflowWrapper
tasks={mockTasks}
onTaskUpdate={mockOnTaskUpdate}
onTaskDelete={mockOnTaskDelete}
/>
);
// Open task with due date
const taskWithDueDate = screen.getByText('Write documentation');
await act(async () => {
await user.click(taskWithDueDate);
});
await waitFor(() => {
expect(screen.getByDisplayValue('2024-12-25')).toBeInTheDocument();
});
// Update due date
const dueDateInput = screen.getByDisplayValue('2024-12-25');
await act(async () => {
await user.clear(dueDateInput);
await user.type(dueDateInput, '2024-12-31');
});
// Save changes
const saveButton = screen.getByText(/save/i);
await act(async () => {
await user.click(saveButton);
});
// Verify update with new due date
expect(mockOnTaskUpdate).toHaveBeenCalledWith(
expect.objectContaining({
id: 2,
due_date: '2024-12-31'
})
);
});
it('handles modal cancellation without saving', async () => {
const user = userEvent.setup();
render(
<TaskWorkflowWrapper
tasks={mockTasks}
onTaskUpdate={mockOnTaskUpdate}
onTaskDelete={mockOnTaskDelete}
/>
);
// Open task modal
const firstTask = screen.getByText('Complete project setup');
await act(async () => {
await user.click(firstTask);
});
await waitFor(() => {
expect(screen.getByDisplayValue('Complete project setup')).toBeInTheDocument();
});
// Make changes
const nameInput = screen.getByDisplayValue('Complete project setup');
await act(async () => {
await user.clear(nameInput);
await user.type(nameInput, 'Changed but not saved');
});
// Cancel instead of saving
const cancelButton = screen.getByText(/cancel/i);
await act(async () => {
await user.click(cancelButton);
});
// Verify no update was called
expect(mockOnTaskUpdate).not.toHaveBeenCalled();
});
it('handles empty task list state', () => {
render(
<TaskWorkflowWrapper
tasks={[]}
onTaskUpdate={mockOnTaskUpdate}
onTaskDelete={mockOnTaskDelete}
/>
);
// Should render empty container
expect(screen.queryByText('Complete project setup')).not.toBeInTheDocument();
expect(screen.queryByText('Write documentation')).not.toBeInTheDocument();
});
it('maintains task selection state during editing', async () => {
const user = userEvent.setup();
render(
<TaskWorkflowWrapper
tasks={mockTasks}
onTaskUpdate={mockOnTaskUpdate}
onTaskDelete={mockOnTaskDelete}
/>
);
// Open first task
const firstTask = screen.getByText('Complete project setup');
await act(async () => {
await user.click(firstTask);
});
await waitFor(() => {
expect(screen.getByDisplayValue('Complete project setup')).toBeInTheDocument();
});
// Verify correct task is loaded
expect(screen.getByDisplayValue('Complete project setup')).toBeInTheDocument();
expect(screen.queryByDisplayValue('Write documentation')).not.toBeInTheDocument();
});
it('handles multiple rapid task interactions', async () => {
const user = userEvent.setup();
render(
<TaskWorkflowWrapper
tasks={mockTasks}
onTaskUpdate={mockOnTaskUpdate}
onTaskDelete={mockOnTaskDelete}
/>
);
// Rapidly interact with tasks
const firstTask = screen.getByText('Complete project setup');
const secondTask = screen.getByText('Write documentation');
// Click first task multiple times
await act(async () => {
await user.click(firstTask);
});
await waitFor(() => {
expect(screen.getByDisplayValue('Complete project setup')).toBeInTheDocument();
});
// Close modal
const cancelButton = screen.getByText(/cancel/i);
await act(async () => {
await user.click(cancelButton);
});
// Should close modal properly
await waitFor(() => {
expect(screen.queryByDisplayValue('Complete project setup')).not.toBeInTheDocument();
});
});
});