Task 09 shipped a powerful API that a non-technical clinic owner could not safely use. This closes that gap: activation now requires having seen what the rule actually does. - PolicySimulator runs a policy against real past appointments and writes nothing: evaluation works on facts (never entities), the whole run sits in a transaction rolled back and cleared in `finally`, and a test counts rows in five sensitive tables before and after - activate() now demands a simulation of the *same version* — a report for version 1 does not unlock version 2 - PolicyTemplateRegistry: six ready-made rules, so the common case never touches a raw condition - Severity from the affected ratio; 0% is a warning too, since a rule that changes nothing usually has a condition that never matches - An empty clinic still succeeds with a warning, otherwise a new clinic could never activate anything Admin: PoliciesPage, PolicyFormPage, PolicySimulationPage, and a PolicyConditionBuilder built entirely from GET /policy-schema — a test proves a field that exists only in the schema shows up with no frontend change, and that operators are filtered per field type. The schema response now carries per-field metadata (label, type, meaningful operators) so the form has one source of truth instead of two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { renderWithProviders } from '../test/utils';
|
|
|
|
vi.mock('../lib/api', () => ({
|
|
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
|
ApiError: class extends Error {},
|
|
}));
|
|
|
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
|
|
|
import { api } from '../lib/api';
|
|
import PolicyFormPage from './PolicyFormPage';
|
|
|
|
const get = api.get as ReturnType<typeof vi.fn>;
|
|
|
|
/**
|
|
* فیلدِ ساختگی: در هیچ کجای فرانت نوشته نشده. اگر در صفحه ظاهر شود، یعنی فرم واقعاً
|
|
* از schema ساخته میشود و فیلد تازهٔ بکاند بدون تغییر فرانت کار میکند.
|
|
*/
|
|
const schema = {
|
|
timing: {
|
|
label: 'مدت نوبت',
|
|
fields: ['made_up_field', 'patient_tags'],
|
|
operators: ['equals', 'not_equals', 'greater_than', 'less_than', 'in', 'contains'],
|
|
field_meta: [
|
|
{
|
|
key: 'made_up_field',
|
|
label: 'فیلد ساختگی آزمون',
|
|
type: 'int',
|
|
operators: ['equals', 'greater_than'],
|
|
},
|
|
{ key: 'patient_tags', label: 'برچسبهای بیمار', type: 'list', operators: ['contains'] },
|
|
],
|
|
effects: [
|
|
{ type: 'min_duration_minutes', label: 'حداقل مدت (دقیقه)', value_type: 'int', combination: 'max' },
|
|
],
|
|
},
|
|
};
|
|
|
|
function mockApi() {
|
|
get.mockImplementation((path: string) => {
|
|
if (path === '/api/v1/policy-schema') return Promise.resolve({ success: true, data: schema });
|
|
if (path === '/api/v1/policy-templates') return Promise.resolve({ success: true, data: [] });
|
|
return Promise.resolve({ success: true, data: null });
|
|
});
|
|
}
|
|
|
|
async function openAdvanced() {
|
|
renderWithProviders(<PolicyFormPage />, { route: '/admin/policies/new' });
|
|
fireEvent.click(screen.getByText('پیشرفته'));
|
|
await waitFor(() => expect(screen.getByText('افزودن شرط')).toBeInTheDocument());
|
|
fireEvent.click(screen.getByText('افزودن شرط'));
|
|
}
|
|
|
|
describe('PolicyFormPage', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockApi();
|
|
});
|
|
|
|
it('renders a field that exists only in the schema, with no frontend change', async () => {
|
|
await openAdvanced();
|
|
|
|
await waitFor(() => expect(screen.getByText('فیلد ساختگی آزمون')).toBeInTheDocument());
|
|
});
|
|
|
|
it('offers only the operators that make sense for the field type', async () => {
|
|
await openAdvanced();
|
|
|
|
await waitFor(() => expect(screen.getByText('برابر است با')).toBeInTheDocument());
|
|
|
|
// فیلد از نوع `int` است، پس «شامل» — که فقط برای فهرست معنا دارد — نباید باشد.
|
|
expect(screen.queryByText('شامل')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('renders the effects of the selected category from the schema', async () => {
|
|
renderWithProviders(<PolicyFormPage />, { route: '/admin/policies/new' });
|
|
fireEvent.click(screen.getByText('پیشرفته'));
|
|
|
|
await waitFor(() => expect(screen.getByText('حداقل مدت (دقیقه)')).toBeInTheDocument());
|
|
expect(screen.getByText(/ترکیب: max/)).toBeInTheDocument();
|
|
});
|
|
});
|