2c9e74776e
- New builtin plugin app/plugins/builtins/tasks/ with full CRUD
- Task model with TenantMixin (title, description, status, priority, due_date, assigned_to, contact_id)
- Routes: GET/POST /tasks, GET/PATCH/DELETE /tasks/{id}, POST /tasks/{id}/assign, POST /tasks/{id}/status
- All routes RBAC-protected (tasks:read, tasks:write, tasks:delete)
- ARQ cron job tasks_due_reminder (daily 8:00) sends notifications for due tasks
- Migration 0001_initial.sql creates tasks table with indexes
- Frontend: Tasks.tsx page with list, filter, create/edit modal, detail modal
- Frontend: api/tasks.ts with React Query hooks
- Route /tasks in routes/index.tsx, sidebar entry via plugin manifest
- i18n keys for nav.tasks and tasks.* in de.json and en.json
- Tests: test_tasks.py (11 tests) + Tasks.test.tsx (3 tests)
- Registered TasksPlugin in conftest.py and worker.py
135 lines
3.7 KiB
TypeScript
135 lines
3.7 KiB
TypeScript
/**
|
|
* Tasks page tests — basic rendering and interaction.
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { TasksPage } from '@/pages/Tasks';
|
|
import * as tasksApi from '@/api/tasks';
|
|
|
|
// Mock i18n
|
|
vi.mock('react-i18next', () => ({
|
|
useTranslation: () => ({ t: (key: string) => key }),
|
|
}));
|
|
|
|
// Mock toast
|
|
vi.mock('@/components/ui/Toast', () => ({
|
|
useToast: () => ({
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
warning: vi.fn(),
|
|
info: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
// Mock UI components
|
|
vi.mock('@/components/ui/Input', () => ({
|
|
Input: ({ value, onChange, type, label, ...props }: any) => (
|
|
<div>
|
|
{label && <label>{label}</label>}
|
|
<input
|
|
data-testid={props['data-testid'] || 'input'}
|
|
type={type || 'text'}
|
|
value={value || ''}
|
|
onChange={onChange}
|
|
/>
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
vi.mock('@/components/ui/Select', () => ({
|
|
Select: ({ value, onChange, options, label, ...props }: any) => (
|
|
<div>
|
|
{label && <label>{label}</label>}
|
|
<select data-testid={props['data-testid'] || 'select'} value={value || ''} onChange={onChange}>
|
|
{options?.map((o: any) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
|
</select>
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
vi.mock('@/components/ui/Modal', () => ({
|
|
Modal: ({ open, onClose, title, children }: any) =>
|
|
open ? (
|
|
<div data-testid="modal" role="dialog">
|
|
<h2>{title}</h2>
|
|
<button onClick={onClose}>Close</button>
|
|
{children}
|
|
</div>
|
|
) : null,
|
|
}));
|
|
|
|
vi.mock('@/components/ui/Button', () => ({
|
|
Button: ({ children, onClick, disabled, ...props }: any) => (
|
|
<button onClick={onClick} disabled={disabled} data-testid={props['data-testid'] || 'button'}>
|
|
{children}
|
|
</button>
|
|
),
|
|
}));
|
|
|
|
vi.mock('@/components/ui/Badge', () => ({
|
|
Badge: ({ children, variant }: any) => <span data-testid="badge" data-variant={variant}>{children}</span>,
|
|
}));
|
|
|
|
vi.mock('@/components/ui/EmptyState', () => ({
|
|
EmptyState: ({ title, description }: any) => (
|
|
<div data-testid="empty-state">
|
|
<h3>{title}</h3>
|
|
<p>{description}</p>
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
// Mock tasks API
|
|
vi.mock('@/api/tasks', () => ({
|
|
useTasks: vi.fn(() => ({
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 'task-1',
|
|
title: 'Test Task',
|
|
description: 'Test description',
|
|
status: 'open',
|
|
priority: 'high',
|
|
due_date: '2025-12-31T10:00:00Z',
|
|
assigned_to: null,
|
|
contact_id: null,
|
|
created_by: null,
|
|
created_at: '2025-01-01T00:00:00Z',
|
|
updated_at: '2025-01-01T00:00:00Z',
|
|
},
|
|
],
|
|
total: 1,
|
|
page: 1,
|
|
page_size: 25,
|
|
},
|
|
isLoading: false,
|
|
})),
|
|
useCreateTask: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
|
useUpdateTask: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
|
useDeleteTask: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
|
useUpdateTaskStatus: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
|
}));
|
|
|
|
describe('TasksPage', () => {
|
|
it('renders the tasks page with header', () => {
|
|
render(<TasksPage />);
|
|
expect(screen.getByTestId('tasks-page')).toBeInTheDocument();
|
|
expect(screen.getByText('tasks.title')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders task items from API', () => {
|
|
render(<TasksPage />);
|
|
expect(screen.getByText('Test Task')).toBeInTheDocument();
|
|
expect(screen.getByText('Test description')).toBeInTheDocument();
|
|
});
|
|
|
|
it('opens create modal when create button is clicked', () => {
|
|
render(<TasksPage />);
|
|
const createBtn = screen.getByText('tasks.create');
|
|
fireEvent.click(createBtn);
|
|
expect(screen.getByTestId('modal')).toBeInTheDocument();
|
|
expect(screen.getByTestId('task-title-input')).toBeInTheDocument();
|
|
});
|
|
});
|