feat(5.21): Tasks Plugin — activities with status, priority, due dates, ARQ reminders
- 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
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user