Files
clinicpro/assets/admin/pages/StaffTreatmentSessionsPage.test.tsx
hamedandClaude Opus 5 8875b8c64a refactor(admin): rebuild the staff sessions page around the operator's question
The page an operator opens to see the day's work did not say who any of it was
for. TreatmentSession::toArray() carries no patient, so the list showed a
service name, a time, and a repeated 40-char button — the same three lines for
every row, with the finished work leading.

The endpoint now sends patient_name and resource_name. They are added in the
controller next to case_uuid/service_name rather than in toArray(), so patient
identity does not leak into every other consumer of that method.

The list is now a queue: unfinished work first, settled work (done, cancelled,
no-show) below it, each group counted. Every row leads with its time, names the
patient, and carries the service, device, session number and area progress on
one meta line. The whole row is the link, so the repeated button is gone.

Also fixes three things the redesign checklist calls out: Latin digits in the
session and area counts (formatNumber), a date repeated on every row of a page
whose title is "today", and a missing error state — a failed request rendered
as "no sessions today", which reads as an empty day rather than a broken one.

Adds the test file the page never had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:51:26 +03:30

111 lines
4.4 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen, 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 {},
}));
import { api } from '../lib/api';
import StaffTreatmentSessionsPage from './StaffTreatmentSessionsPage';
const get = api.get as ReturnType<typeof vi.fn>;
const AT = Math.floor(new Date('2026-08-07T09:30:00Z').getTime() / 1000);
function session(over: Record<string, unknown> = {}) {
return {
uuid: 'ses-1',
case_uuid: 'case-1',
service_name: 'لیزر توتال',
patient_name: 'محمد رسولی',
resource_name: 'کندلا ۲۰۲۳',
session_number: 2,
total_sessions: 3,
status: 'booked',
due_at: AT,
started_at: null,
finished_at: null,
note: null,
performed_by: null,
appointment: { uuid: 'a-1', slot_start: AT, slot_end: AT + 1800, status: 'confirmed' },
areas: [{ status: 'completed' }, { status: 'pending' }],
...over,
};
}
beforeEach(() => {
get.mockReset();
});
describe('صفحهٔ جلسات امروز پرسنل', () => {
it('نام بیمار و دستگاه را نشان می‌دهد، نه فقط نام سرویس', async () => {
get.mockResolvedValue({ success: true, data: [session()] });
renderWithProviders(<StaffTreatmentSessionsPage />);
expect(await screen.findByText('محمد رسولی')).toBeInTheDocument();
expect(screen.getByText('کندلا ۲۰۲۳')).toBeInTheDocument();
expect(screen.getByText('لیزر توتال')).toBeInTheDocument();
});
/** صف است: کار باقی‌مانده باید بالای کارِ تمام‌شده بنشیند. */
it('کار باقی‌مانده را از کار تمام‌شده جدا می‌کند', async () => {
get.mockResolvedValue({
success: true,
data: [
session({ uuid: 's-done', status: 'done', patient_name: 'بیمار تمام‌شده' }),
session({ uuid: 's-open', status: 'booked', patient_name: 'بیمار در انتظار' }),
],
});
renderWithProviders(<StaffTreatmentSessionsPage />);
const pendingHead = await screen.findByText('در انتظار انجام');
const doneHead = screen.getByText('تمام‌شده');
expect(pendingHead.compareDocumentPosition(doneHead) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
const open = screen.getByText('بیمار در انتظار');
const done = screen.getByText('بیمار تمام‌شده');
expect(open.compareDocumentPosition(done) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
/** غیبت برای امروز بسته است، هرچند جلسه دوباره برنامه‌ریزی می‌شود. */
it('غیبت را در گروه تمام‌شده می‌گذارد', async () => {
get.mockResolvedValue({ success: true, data: [session({ status: 'no_show' })] });
renderWithProviders(<StaffTreatmentSessionsPage />);
expect(await screen.findByText('تمام‌شده')).toBeInTheDocument();
expect(screen.getByText('کار باقی‌مانده‌ای ندارید.')).toBeInTheDocument();
});
/** خطای سرور نباید «امروز جلسه‌ای ندارید» خوانده شود. */
it('خطا را از فهرست خالی جدا می‌کند', async () => {
get.mockRejectedValue(new Error('boom'));
renderWithProviders(<StaffTreatmentSessionsPage />);
await waitFor(() => expect(screen.getByText(/خواندن جلسات امروز ناموفق بود/)).toBeInTheDocument());
expect(screen.queryByText(/جلسه‌ای برای شما ثبت نشده/)).not.toBeInTheDocument();
});
it('فهرست خالی پیام خودش را دارد', async () => {
get.mockResolvedValue({ success: true, data: [] });
renderWithProviders(<StaffTreatmentSessionsPage />);
expect(await screen.findByText(/جلسه‌ای برای شما ثبت نشده/)).toBeInTheDocument();
});
it('کل ردیف لینکِ همان جلسه است', async () => {
get.mockResolvedValue({ success: true, data: [session()] });
renderWithProviders(<StaffTreatmentSessionsPage />);
const row = (await screen.findByText('محمد رسولی')).closest('a');
expect(row).toHaveAttribute('href', '/admin/my-sessions/ses-1');
});
});