Adding a secretary meant deciding all 17 permission resources in the same dialog. The full-height capture showed the form running past 1300px with the save button below 16 accordions, and the dynamic registry makes that worse: every page added in future lengthens this one modal. The add/edit modal now carries only doctors, profile and address, and fits on screen with its footer visible. Permissions move to SecretaryPermissionsModal, reachable from a row action and opened automatically right after a successful add, since a new secretary starts on the role defaults and the owner usually wants to set them. Neither create nor update sends permissions any more — the backend seeds the role defaults on create, and the permissions modal owns the writes, fanning out over every link row so a secretary shared across doctors stays consistent. PermissionAccordions moves to components/ui as a shared component. Sections now start collapsed with a granted/total badge on each header, so the panel opens at a fixed height and still says which sections are on. Two design-system slips caught by re-screenshotting rather than by the audit: - a text button as a third row action pushed the name column out of the table, so the desktop row uses an icon with a title and the mobile card keeps the label - .btn.secondary is not defined in styles.css (variants are primary/ghost/soft/ danger/accent), so it renders as a bare .btn. Used ghost here. 25 other files have the same dead class; left alone as a separate sweep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
188 lines
8.8 KiB
TypeScript
188 lines
8.8 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||
import { screen, fireEvent } from "@testing-library/react";
|
||
import { renderWithProviders } from "../test/utils";
|
||
|
||
vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||
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("../stores/authStore", () => ({
|
||
useAuthStore: () => ({ doctorUuid: "doc-1", dbUuid: "doc-1", primaryRole: "doctor" }),
|
||
}));
|
||
vi.mock("../hooks/useSubscription", () => ({
|
||
useSubscription: () => ({ maxSecretaries: 5 }),
|
||
}));
|
||
|
||
import { api } from "../lib/api";
|
||
import MySecretariesPage from "./MySecretariesPage";
|
||
|
||
const get = api.get as ReturnType<typeof vi.fn>;
|
||
|
||
const fullPerms = {
|
||
appointments: { view: true, create: false, cancel: false, update_status: false },
|
||
patients: { view: false, create: false, update: false, delete: false },
|
||
payments: { view: false, create: false, update: false, delete: false },
|
||
insurances: { view: false, create: false, update: false, delete: false },
|
||
addresses: { view: false, create: false, update: false, delete: false },
|
||
clinic_info: { view: false, update: false },
|
||
};
|
||
|
||
const activeSecretary = {
|
||
uuid: "sec-1",
|
||
user_name: "سارا احمدی",
|
||
mobile_number: "09121234567",
|
||
doctor_name: "دکتر تست",
|
||
doctor_uuid: "doc-1",
|
||
is_active: true,
|
||
national_code: "1234567890",
|
||
address: "یزد",
|
||
permissions: fullPerms,
|
||
created_at: 1700000000,
|
||
};
|
||
|
||
const previousSecretary = {
|
||
...activeSecretary,
|
||
uuid: "sec-2",
|
||
user_name: "مینا رضایی",
|
||
mobile_number: "09129876543",
|
||
is_active: false,
|
||
national_code: "9999999999",
|
||
};
|
||
|
||
/** فهرست بخشها از GET /api/v1/permission-catalog میآید، نه از کد کامپوننت. */
|
||
const CATALOG = [
|
||
{
|
||
key: "appointments", label: "مدیریت نوبتها", clinic_only: false,
|
||
actions: [{ key: "view", label: "مشاهده نوبتها" }, { key: "create", label: "ایجاد نوبت" }],
|
||
},
|
||
{
|
||
key: "patients", label: "پرونده بیماران", clinic_only: false,
|
||
actions: [{ key: "view", label: "مشاهده بیماران" }],
|
||
},
|
||
{
|
||
key: "clinic_doctors", label: "مدیریت پزشکان کلینیک", clinic_only: true,
|
||
actions: [{ key: "view", label: "مشاهده پزشکان" }],
|
||
},
|
||
];
|
||
|
||
function mockData(rows = [activeSecretary, previousSecretary], catalog: unknown[] = CATALOG) {
|
||
get.mockImplementation((url: string) => {
|
||
if (url.includes("permission-catalog")) {
|
||
return Promise.resolve({ success: true, data: { version: 1, resources: catalog } });
|
||
}
|
||
if (url.includes("/secretaries/")) return Promise.resolve({ success: true, data: rows });
|
||
return Promise.resolve({ success: true, data: [] });
|
||
});
|
||
}
|
||
|
||
beforeEach(() => {
|
||
get.mockReset();
|
||
mockData();
|
||
});
|
||
|
||
describe("MySecretariesPage", () => {
|
||
it("renders the title and both tabs", async () => {
|
||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||
expect(await screen.findByText("لیست منشی ها")).toBeInTheDocument();
|
||
expect(screen.getByText("منشی های فعلی")).toBeInTheDocument();
|
||
expect(screen.getByText("منشی های قبلی")).toBeInTheDocument();
|
||
});
|
||
|
||
it("shows active secretaries with national code on the default tab", async () => {
|
||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||
expect(await screen.findAllByText("سارا احمدی")).not.toHaveLength(0);
|
||
expect(screen.getAllByText("1234567890").length).toBeGreaterThan(0);
|
||
// inactive secretary is hidden on the active tab
|
||
expect(screen.queryByText("مینا رضایی")).not.toBeInTheDocument();
|
||
});
|
||
|
||
it("switches to the previous tab and lists inactive secretaries", async () => {
|
||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||
await screen.findAllByText("سارا احمدی");
|
||
|
||
fireEvent.click(screen.getByText("منشی های قبلی"));
|
||
|
||
expect(await screen.findAllByText("مینا رضایی")).not.toHaveLength(0);
|
||
expect(screen.queryByText("سارا احمدی")).not.toBeInTheDocument();
|
||
});
|
||
|
||
it("shows an empty state when there are no active secretaries", async () => {
|
||
mockData([previousSecretary]);
|
||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||
expect(await screen.findByText("هنوز منشی فعالی اضافه نشده است")).toBeInTheDocument();
|
||
});
|
||
|
||
// ── جداسازی فرمِ منشی از دسترسیها ──────────────────────────────────────
|
||
// مودالِ افزودن دیگر مجوز ندارد؛ مجوزها مودالِ خودشان را دارند تا افزودنِ یک
|
||
// منشی به تصمیمگیری دربارهٔ همهٔ منابع گره نخورد.
|
||
|
||
it("keeps permissions out of the add form", async () => {
|
||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||
await screen.findAllByText("سارا احمدی");
|
||
|
||
fireEvent.click(screen.getByText("اضافه کردن منشی"));
|
||
|
||
expect(await screen.findByText("اضافه کردن منشی جدید")).toBeInTheDocument();
|
||
expect(screen.queryByText("مدیریت نوبتها")).not.toBeInTheDocument();
|
||
expect(screen.queryByText("پرونده بیماران")).not.toBeInTheDocument();
|
||
});
|
||
|
||
it("opens a separate permissions modal from the row action", async () => {
|
||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||
await screen.findAllByText("سارا احمدی");
|
||
|
||
fireEvent.click(screen.getAllByRole("button", { name: "دسترسیها" })[0]);
|
||
|
||
expect(await screen.findByText("دسترسیهای سارا احمدی")).toBeInTheDocument();
|
||
expect(await screen.findByText("مدیریت نوبتها")).toBeInTheDocument();
|
||
expect(screen.getByText("پرونده بیماران")).toBeInTheDocument();
|
||
});
|
||
|
||
/**
|
||
* همان تضمینِ «داینامیک بودن»: منبع تازه فقط به mock اضافه میشود و هیچ خطی از
|
||
* کد کامپوننت عوض نمیشود.
|
||
*/
|
||
it("shows a resource added to the catalog with no code change", async () => {
|
||
mockData(undefined, [
|
||
...CATALOG,
|
||
{ key: "brand_new_page", label: "صفحهٔ کاملاً تازه", clinic_only: false, actions: [{ key: "view", label: "مشاهده" }] },
|
||
]);
|
||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||
await screen.findAllByText("سارا احمدی");
|
||
|
||
fireEvent.click(screen.getAllByRole("button", { name: "دسترسیها" })[0]);
|
||
|
||
expect(await screen.findByText("صفحهٔ کاملاً تازه")).toBeInTheDocument();
|
||
});
|
||
|
||
/** منبعِ clinic_only برای پزشکِ مستقل (primaryRole=doctor) نباید دیده شود. */
|
||
it("hides clinic-only resources for a standalone doctor", async () => {
|
||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||
await screen.findAllByText("سارا احمدی");
|
||
|
||
fireEvent.click(screen.getAllByRole("button", { name: "دسترسیها" })[0]);
|
||
|
||
expect(await screen.findByText("مدیریت نوبتها")).toBeInTheDocument();
|
||
expect(screen.queryByText("مدیریت پزشکان کلینیک")).not.toBeInTheDocument();
|
||
});
|
||
|
||
/** آکاردئونها بسته باز میشوند و شمارِ روشنها روی هدر دیده میشود. */
|
||
it("starts every section collapsed with a granted counter", async () => {
|
||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||
await screen.findAllByText("سارا احمدی");
|
||
|
||
fireEvent.click(screen.getAllByRole("button", { name: "دسترسیها" })[0]);
|
||
await screen.findByText("مدیریت نوبتها");
|
||
|
||
// appointments.view=true در fullPerms → «۱ از ۲» با ارقام فارسی
|
||
expect(screen.getByText("۱ از ۲")).toBeInTheDocument();
|
||
// بسته است، پس سوییچِ داخلش رندر نشده
|
||
expect(screen.queryByLabelText("مدیریت نوبتها — مشاهده نوبتها")).not.toBeInTheDocument();
|
||
|
||
fireEvent.click(screen.getByText("مدیریت نوبتها"));
|
||
expect(await screen.findByLabelText("مدیریت نوبتها — مشاهده نوبتها")).toBeInTheDocument();
|
||
});
|
||
|
||
});
|