Files
clinicpro/assets/admin/pages/PolicyFormPage.test.tsx
T
hamedandClaude Opus 5 bcfa87bfad feat(policy): rule builder and mandatory dry-run sandbox
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>
2026-07-31 10:44:28 +03:30

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