import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { MemoryRouter } from "react-router-dom"; import { ApiError, api } from "../api"; const { refreshMock } = vi.hoisted(() => ({ refreshMock: vi.fn() })); vi.mock("../components/AppShell", () => ({ AppShell: ({ children }: { children: React.ReactNode }) =>
{children}
})); vi.mock("../hooks/usePollingResource", () => ({ usePollingResource: (_loader: unknown, options: { intervalMs: number }) => ({ data: options.intervalMs === 30_000 ? { users: [ { id: "user-1", username: "staff01", role: "STAFF", active: true, project_ids: ["project-1"] }, { id: "super-1", username: "xqkwljtadmin", role: "ADMIN", active: true, protected: true, project_ids: [] }, ], } : { summary: { running_projects: 1, waiting_count: 1, waiting_ticket_count: 1, waiting_people_count: 3, anomaly_projects: 0, offline_devices: 0 }, projects: [{ id: "project-1", code: "EAST-RIDE", name: "东门观光车", timezone: "Asia/Shanghai", ticket_prefix: "A", status: "RUNNING", batch_size: 5, call_batch_size: 5, call_mode: "BOTH", default_call_ticket_count: 5, max_call_ticket_count: 20, default_call_people_count: 8, max_call_people_count: 30, min_party_size: 1, max_party_size: 8, grace_period_minutes: 5, experienced_people_start: 10, visitor_notice: "请在入口附近等候。", eta: { interval_per_person_seconds: 120 }, waiting_count: 1, waiting_ticket_count: 1, waiting_people_count: 3, issued_ticket_count: 15, latest_ticket_number: "00015", current_batch: { id: "batch-1", batch_number: 4, status: "ACTIVE", tickets: [ { id: "ticket-2", ticket_number: "00013", phone: "13800138000", status: "CALLED", party_size: 2 }, { id: "ticket-3", ticket_number: "00014", phone: "13800138001", status: "CALLED", party_size: 3 }, ], ticket_count: 2, people_count: 5, }, estimated_wait: { available: true, lower_minutes: 5, upper_minutes: 15 }, experienced_people: 12, device_status: { label: "模拟设备执行成功" }, }], active_tickets: [{ id: "ticket-1", project_id: "project-1", project_name: "东门观光车", ticket_number: "00012", phone: "13800138000", last_name: "张", honorific: "先生", party_size: 3, status: "WAITING", created_at: "2026-07-10T09:00:00Z", }], }, loading: false, refreshing: false, error: null, offline: false, lastClientSuccessAt: new Date().toISOString(), refresh: refreshMock, }), })); import { AdminPage } from "./AdminPage"; beforeEach(() => { refreshMock.mockReset(); }); describe("AdminPage active tickets", () => { it("保留核心排队数据并删除辅助概览模块", () => { render(); expect(screen.getByRole("list", { name: "项目队列" })).toHaveTextContent("东门观光车"); expect(screen.getByRole("list", { name: "项目队列" })).toHaveTextContent("00013 至 00014"); expect(screen.getByText("运行项目").parentElement).toHaveTextContent("1"); expect(screen.getByText("总等待人数").parentElement).toHaveTextContent("3"); expect(screen.getByRole("list", { name: "项目队列" })).toHaveTextContent("3人 · 1 个号码"); expect(screen.queryByText("最近操作")).not.toBeInTheDocument(); expect(screen.queryByText("异常与提醒")).not.toBeInTheDocument(); expect(screen.queryByText("最新叫号")).not.toBeInTheDocument(); expect(screen.queryByText("异常项目")).not.toBeInTheDocument(); expect(screen.queryByText("离线设备")).not.toBeInTheDocument(); expect(screen.queryByRole("group", { name: "数据视图" })).not.toBeInTheDocument(); expect(screen.queryByText("演示视图")).not.toBeInTheDocument(); expect(screen.queryByText("实时数据")).not.toBeInTheDocument(); }); it("在项目管理中列出项目并提供创建与维护入口", () => { render(); expect(screen.getByRole("heading", { name: "项目管理" })).toBeVisible(); expect(screen.getByRole("table", { name: "项目列表" })).toHaveTextContent("东门观光车"); expect(screen.getByRole("link", { name: "创建项目" })).toHaveAttribute("href", "/admin/projects/new"); expect(screen.getByRole("link", { name: "维护" })).toHaveAttribute("href", "/admin/projects/project-1"); expect(screen.queryByRole("form", { name: "规则配置" })).not.toBeInTheDocument(); }); it("将上线超级管理员显示为不可编辑", () => { render(); const row = screen.getByText("xqkwljtadmin").closest('[role="row"]'); expect(row).toHaveTextContent("超级管理员"); expect(row).toHaveTextContent("不可编辑"); expect(row?.querySelector('a[href="/admin/accounts/super-1"]')).toBeNull(); }); it("在同一表单中维护项目全部字段", () => { render(); expect(screen.getByRole("heading", { name: "东门观光车" })).toBeVisible(); expect(screen.queryByRole("heading", { name: "维护项目" })).not.toBeInTheDocument(); expect(screen.getByRole("form", { name: "维护项目" })).toBeVisible(); expect(screen.getByRole("form", { name: "维护项目" })).toHaveTextContent("项目名称"); expect(screen.getByRole("form", { name: "维护项目" })).toHaveTextContent("项目状态"); expect(screen.getByRole("region", { name: "基础信息" })).toBeVisible(); expect(screen.getByRole("region", { name: "叫号规则" })).toBeVisible(); expect(screen.getByRole("region", { name: "其他规则" })).toBeVisible(); expect(screen.getByRole("group", { name: "支持方式" })).toBeVisible(); expect(screen.getByRole("group", { name: "单号人数" })).toBeVisible(); expect(screen.getByRole("group", { name: "批量叫号" })).toBeVisible(); expect(screen.getByRole("group", { name: "批量叫人" })).toBeVisible(); expect(screen.getByRole("combobox", { name: "票号格式" })).toHaveDisplayValue("00000"); expect(screen.queryByRole("textbox", { name: "时区" })).not.toBeInTheDocument(); expect(screen.queryByRole("textbox", { name: "票号前缀" })).not.toBeInTheDocument(); expect(screen.getByRole("combobox", { name: "支持的叫号方式" })).toHaveDisplayValue("两种都支持"); expect(screen.getByRole("spinbutton", { name: "单号最少人数" })).toHaveValue(1); expect(screen.getByRole("spinbutton", { name: "单号最多人数" })).toHaveValue(8); expect(screen.getByRole("spinbutton", { name: "批量叫号默认数量" })).toHaveValue(5); expect(screen.getByRole("spinbutton", { name: "批量叫号单次上限" })).toHaveValue(20); expect(screen.getByRole("spinbutton", { name: "批量叫人默认人数" })).toHaveValue(8); expect(screen.getByRole("spinbutton", { name: "批量叫人单次上限" })).toHaveValue(30); expect(screen.getByRole("spinbutton", { name: "已体验人数起始展示数" })).toHaveValue(10); expect(screen.getByRole("spinbutton", { name: "每人预计间隔时间(秒)" })).toHaveValue(120); expect(screen.queryByRole("combobox", { name: "预计等待时间方式" })).not.toBeInTheDocument(); expect(screen.getByRole("textbox", { name: "游客官方提示" })).toHaveValue("请在入口附近等候。"); expect(screen.getByRole("button", { name: "保存项目" })).toBeVisible(); expect(screen.getByRole("button", { name: "移除项目" })).toBeVisible(); expect(screen.getByText("有运营历史的已结束项目将从所有项目列表隐藏,历史记录仍保留;无运营历史项目会永久删除。")).toBeVisible(); }); it("确认后归档或移除项目,刷新数据并返回列表", async () => { let resolveDelete!: () => void; const deleteProject = vi.spyOn(api, "deleteProject").mockImplementation(() => new Promise((resolve) => { resolveDelete = resolve; })); refreshMock.mockResolvedValueOnce(undefined); try { render(); fireEvent.click(screen.getByRole("button", { name: "移除项目" })); expect(deleteProject).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole("button", { name: "确认移除" })); await waitFor(() => expect(deleteProject).toHaveBeenCalledWith("project-1")); const deletingButton = screen.getByRole("button", { name: "正在移除" }); expect(deletingButton).toBeDisabled(); fireEvent.click(deletingButton); expect(deleteProject).toHaveBeenCalledTimes(1); resolveDelete(); await waitFor(() => expect(refreshMock).toHaveBeenCalled()); await waitFor(() => expect(screen.getByRole("heading", { name: "项目管理" })).toBeVisible()); } finally { vi.restoreAllMocks(); } }); it("归档或移除项目失败时留在维护页并显示服务端消息", async () => { const deleteProject = vi.spyOn(api, "deleteProject").mockRejectedValue(new ApiError("项目存在活动排队,无法删除", 409)); try { render(); fireEvent.click(screen.getByRole("button", { name: "移除项目" })); fireEvent.click(screen.getByRole("button", { name: "确认移除" })); expect(await screen.findByText("项目存在活动排队,无法删除")).toBeVisible(); expect(screen.getByText("移除失败")).toBeVisible(); expect(screen.getByRole("heading", { name: "东门观光车" })).toBeVisible(); } finally { vi.restoreAllMocks(); } }); it("创建项目与维护项目使用相同字段", () => { render(); expect(screen.getByRole("form", { name: "创建项目" })).toBeVisible(); expect(screen.getByRole("region", { name: "基础信息" })).toBeVisible(); expect(screen.getByRole("region", { name: "叫号规则" })).toBeVisible(); expect(screen.getByRole("region", { name: "其他规则" })).toBeVisible(); expect(screen.getByRole("textbox", { name: "项目名称" })).toBeVisible(); expect(screen.getByRole("textbox", { name: "项目编码" })).toBeVisible(); expect(screen.getByRole("combobox", { name: "票号格式" })).toHaveDisplayValue("00000"); expect(screen.getByRole("combobox", { name: "项目状态" })).toHaveDisplayValue("未开放"); expect(screen.getByRole("combobox", { name: "支持的叫号方式" })).toHaveDisplayValue("两种都支持"); expect(screen.getByRole("spinbutton", { name: "单号最少人数" })).toHaveValue(1); expect(screen.getByRole("spinbutton", { name: "单号最多人数" })).toHaveValue(10); expect(screen.getByRole("spinbutton", { name: "批量叫号默认数量" })).toHaveValue(1); expect(screen.getByRole("spinbutton", { name: "批量叫号单次上限" })).toHaveValue(100); expect(screen.getByRole("spinbutton", { name: "批量叫人默认人数" })).toHaveValue(1); expect(screen.getByRole("spinbutton", { name: "批量叫人单次上限" })).toHaveValue(100); expect(screen.getByRole("spinbutton", { name: "已体验人数起始展示数" })).toHaveValue(0); expect(screen.getByRole("spinbutton", { name: "每人预计间隔时间(秒)" })).toHaveValue(60); expect(screen.getByRole("textbox", { name: "游客官方提示" })).toHaveValue("请您在景区附近等候,注意听从工作人员指引。"); expect(screen.queryByRole("textbox", { name: "时区" })).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: "创建项目" })).toBeVisible(); }); it("创建项目提交完整的默认配置", async () => { const createdProject = { id: "project-2", code: "NEW-RIDE", name: "新项目", status: "NOT_OPEN", batch_size: 1, timezone: "Asia/Shanghai", ticket_prefix: "A" }; const createProject = vi.spyOn(api, "createProject").mockResolvedValue({ project: createdProject }); const updateProjectSettings = vi.spyOn(api, "updateProjectSettings").mockResolvedValue({ project: createdProject }); try { render(); fireEvent.change(screen.getByRole("textbox", { name: "项目名称" }), { target: { value: "新项目" } }); fireEvent.change(screen.getByRole("textbox", { name: "项目编码" }), { target: { value: "new-ride" } }); fireEvent.submit(screen.getByRole("form", { name: "创建项目" })); await waitFor(() => expect(createProject).toHaveBeenCalledTimes(1)); expect(createProject).toHaveBeenCalledWith({ name: "新项目", code: "NEW-RIDE", timezone: "Asia/Shanghai", ticket_prefix: "A" }); expect(updateProjectSettings).toHaveBeenCalledWith("project-2", { status: "NOT_OPEN", call_mode: "BOTH", default_call_ticket_count: 1, max_call_ticket_count: 100, default_call_people_count: 1, max_call_people_count: 100, min_party_size: 1, max_party_size: 10, grace_period_minutes: 5, experienced_people_start: 0, eta_interval_seconds: 60, visitor_notice: "请您在景区附近等候,注意听从工作人员指引。", }); } finally { vi.restoreAllMocks(); } }); it("keeps the screen center free of contact details", () => { render(); expect(screen.getAllByRole("button", { name: "全屏展示" }).length).toBeGreaterThan(0); expect(screen.getByRole("region", { name: "项目实时监控墙" })).toBeVisible(); expect(screen.getByText("00013 至 00014")).toBeVisible(); expect(screen.queryByText("00013、00014")).not.toBeInTheDocument(); expect(screen.queryByText("13800138000")).not.toBeInTheDocument(); expect(screen.queryByText("全项目实时监控")).not.toBeInTheDocument(); expect(screen.queryByText("当前批次")).not.toBeInTheDocument(); expect(screen.getByText("当前叫号")).toBeInTheDocument(); expect(screen.queryByText("当前叫号 · 5 人")).not.toBeInTheDocument(); expect(screen.queryByText("设备状态")).not.toBeInTheDocument(); expect(screen.queryByText("大屏中心只展示公开票号与运行状态,不展示游客联系方式。")).not.toBeInTheDocument(); expect(screen.getByLabelText(/当前时间/)).toBeInTheDocument(); expect(screen.queryByText("当前项目")).not.toBeInTheDocument(); expect(screen.getByRole("heading", { name: "东门观光车" })).toBeVisible(); expect(screen.getByRole("heading", { name: "最新取号码:00015号" })).toBeInTheDocument(); expect(screen.getByLabelText("累计取号数:15 个")).toBeInTheDocument(); expect(screen.getByLabelText("累计等待人数:3 人")).toBeInTheDocument(); expect(screen.getByLabelText("已体验人数:12 人")).toBeInTheDocument(); expect(document.querySelectorAll("[data-project-screen] .screen-tile__current-metrics .screen-tile__metric")).toHaveLength(4); expect(document.querySelector("[data-project-screen] .screen-tile__clock .screen-tile__logo")).toBeInTheDocument(); expect(document.querySelector("[data-project-screen] .screen-tile__project .screen-tile__logo")).toBeNull(); expect(screen.getByText("预计等待时间")).toBeVisible(); expect(screen.queryByText("后续号段预计")).not.toBeInTheDocument(); expect(screen.queryByText(/\d+ \/ \d+/)).not.toBeInTheDocument(); expect(screen.queryByText("运行中")).not.toBeInTheDocument(); }); it("先展示账号列表,不在列表页展示创建表单", () => { render(); expect(screen.getByRole("heading", { name: "账号管理" })).toBeVisible(); expect(screen.getByRole("table", { name: "账号列表" })).toBeVisible(); expect(screen.getByText("staff01")).toBeVisible(); expect(screen.getByRole("link", { name: "创建账号" })).toHaveAttribute("href", "/admin/accounts/new"); expect(screen.getByRole("link", { name: "维护" })).toHaveAttribute("href", "/admin/accounts/user-1"); expect(screen.queryByRole("combobox", { name: "角色" })).not.toBeInTheDocument(); expect(screen.queryByRole("form", { name: "创建账号" })).not.toBeInTheDocument(); }); it("点击创建后由独立路由维护新账号", () => { render(); expect(screen.getByRole("form", { name: "创建账号" })).toBeVisible(); expect(screen.getByRole("textbox", { name: "账号" })).toBeVisible(); expect(screen.getByLabelText("初始密码")).toBeVisible(); expect(screen.getByRole("link", { name: "取消" })).toHaveAttribute("href", "/admin/accounts"); expect(screen.queryByText("staff01")).not.toBeInTheDocument(); }); it("账号维护控件只在独立维护页展示", () => { render(); expect(screen.getByRole("heading", { name: "维护账号" })).toBeVisible(); expect(screen.getByRole("radiogroup", { name: "角色" })).toBeVisible(); expect(screen.getByRole("radio", { name: "员工" })).toBeChecked(); expect(screen.getByRole("radio", { name: "管理员" })).not.toBeChecked(); expect(screen.getByRole("button", { name: "保存修改" })).toBeDisabled(); fireEvent.click(screen.getByRole("radio", { name: "管理员" })); expect(screen.getByRole("radio", { name: "管理员" })).toBeChecked(); expect(screen.getByText("有未保存的修改")).toBeVisible(); expect(screen.getByRole("button", { name: "保存修改" })).toBeEnabled(); expect(screen.getByRole("button", { name: "放弃修改" })).toBeEnabled(); expect(screen.getByRole("group", { name: "所属项目" })).toBeVisible(); expect(screen.getByRole("checkbox", { name: /东门观光车/ })).toBeChecked(); expect(screen.getByText("已授权")).toBeVisible(); expect(screen.getByRole("link", { name: "返回列表" })).toHaveAttribute("href", "/admin/accounts"); expect(screen.queryByRole("table", { name: "账号列表" })).not.toBeInTheDocument(); }); });