feat: add party-size queueing and call modes

This commit is contained in:
wangxuming committed 2026-07-16 11:40:34 +08:00
1 parent 7f751bebae
commit 66951b4dc3
53 files changed
+3348 -533

No files matched your search

+146 -16
View File
@@ -1,4 +1,4 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { MemoryRouter } from "react-router-dom";
@@ -11,8 +11,8 @@ vi.mock("../hooks/usePollingResource", () => ({
usePollingResource: (_load: unknown, options: { intervalMs: number }) => options.intervalMs === 60_000
? {
data: { projects: [
{ id: "project-1", name: "东门观光车", status: "RUNNING", batch_size: 2 },
{ id: "project-2", name: "西门观光车", status: "RUNNING", batch_size: 2 },
{ id: "project-1", name: "东门观光车", status: "RUNNING", batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
{ id: "project-2", name: "西门观光车", status: "RUNNING", batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
] },
loading: false,
refreshing: false,
@@ -23,17 +23,18 @@ vi.mock("../hooks/usePollingResource", () => ({
}
: {
data: {
project: { id: "project-1", name: "东门观光车", status: "RUNNING", batch_size: 2 },
project: { id: "project-1", name: "东门观光车", status: "RUNNING", batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
revision: 8,
waiting: [{
id: "waiting-1",
ticket_number: "00013",
waiting: Array.from({ length: 25 }, (_, index) => ({
id: `waiting-${index + 1}`,
ticket_number: String(index + 13).padStart(5, "0"),
phone: "13900139000",
last_name: "李",
honorific: "女士",
status: "WAITING",
party_size: index === 0 ? 4 : 2,
created_at: "2026-07-10T09:02:00Z",
}],
})),
current_batch: {
id: "batch-1",
batch_number: 4,
@@ -46,13 +47,18 @@ vi.mock("../hooks/usePollingResource", () => ({
last_name: "张",
honorific: "先生",
status: "CALLED",
party_size: 2,
}],
ticket_count: 1,
people_count: 2,
},
metrics: {
waiting_count: 1,
waiting_count: 25,
waiting_ticket_count: 25,
waiting_people_count: 52,
estimated_wait: { available: true, estimate_minutes: 15, min_minutes: 15, max_minutes: 15 },
last_ticket_number: "00013",
next_ticket_number: "00014",
last_ticket_number: "00037",
next_ticket_number: "00038",
},
last_success_at: new Date().toISOString(),
},
@@ -90,19 +96,136 @@ describe("StaffPage scene-focused H5", () => {
expect(screen.queryByRole("link", { name: "核验" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "到场" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "过号" })).not.toBeInTheDocument();
expect(screen.getByRole("heading", { name: "按号数/人数叫号" })).toBeVisible();
expect(screen.getByRole("region", { name: "按号数/人数叫号" }).parentElement).toHaveClass("staff-call-sticky");
expect(screen.queryByText("按号码叫号", { selector: ".staff-call-mode > strong" })).not.toBeInTheDocument();
expect(screen.queryByText("按人数叫号", { selector: ".staff-call-mode > strong" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "快速叫下一个号" })).toBeEnabled();
expect(screen.getByRole("button", { name: "叫号段" })).toBeEnabled();
expect(screen.getByRole("button", { name: "批量叫号" })).toBeEnabled();
expect(screen.getByRole("button", { name: "批量叫号" })).toHaveClass("button--secondary");
expect(screen.getByRole("button", { name: "批量叫人" })).toBeEnabled();
expect(screen.getByRole("button", { name: "批量叫人" })).toHaveClass("button--secondary");
expect(screen.queryByRole("button", { name: "按号码叫号" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "按人数叫号" })).not.toBeInTheDocument();
expect(screen.getByRole("region", { name: "叫号" })).toHaveTextContent("000134 人");
expect(screen.queryByText(/本次共叫/)).not.toBeInTheDocument();
expect(screen.queryByLabelText("等待队列表,可横向滚动")).not.toBeInTheDocument();
expect(screen.queryByRole("group", { name: "数据视图" })).not.toBeInTheDocument();
expect(screen.queryByText("演示")).not.toBeInTheDocument();
expect(screen.queryByText("实时")).not.toBeInTheDocument();
expect(screen.getByRole("region", { name: "叫号" })).toHaveTextContent("最末号预计时长约 15 分钟");
const callRegion = screen.getByRole("region", { name: "叫号" });
expect(callRegion).toHaveTextContent("下个号人数4 人");
expect(callRegion).toHaveTextContent("剩余未叫号25 个");
expect(callRegion).toHaveTextContent("剩余未叫人数52 人");
expect(callRegion).not.toHaveTextContent("最末号数");
expect(callRegion).not.toHaveTextContent("最末号预计时长");
});
it("reveals waiting tickets ten at a time and can collapse", () => {
renderScene("/staff");
const waitingList = screen.getByRole("list", { name: "待叫号码" });
expect(within(waitingList).getAllByRole("listitem")).toHaveLength(10);
expect(screen.queryByText("00023")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "查看更多" }));
expect(within(waitingList).getAllByRole("listitem")).toHaveLength(20);
expect(screen.getByText("00032")).toBeVisible();
expect(screen.queryByText("00033")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "查看更多" })).toBeVisible();
expect(screen.getByRole("button", { name: "收起" })).toBeVisible();
fireEvent.click(screen.getByRole("button", { name: "查看更多" }));
expect(within(waitingList).getAllByRole("listitem")).toHaveLength(25);
expect(screen.getByText("00037")).toBeVisible();
expect(screen.queryByRole("button", { name: "查看更多" })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "收起" }));
expect(within(waitingList).getAllByRole("listitem")).toHaveLength(10);
expect(screen.getByRole("button", { name: "查看更多" })).toBeVisible();
expect(screen.queryByText("00023")).not.toBeInTheDocument();
});
it("keeps business success primary when a device acknowledgement fails", async () => {
const callNext = vi.spyOn(api, "callNext").mockResolvedValueOnce({
batch: {
id: "batch-2",
batch_number: 5,
status: "CALLED",
called_at: "2026-07-10T09:03:00Z",
ticket_count: 1,
people_count: 4,
tickets: [{ id: "called-2", ticket_number: "00013", status: "CALLED", party_size: 4 }],
},
revision: 9,
device_results: [{ channel: "SIMULATOR", status: "FAILURE", message: "模拟设备执行失败;业务叫号已成功" }],
});
try {
renderScene("/staff");
fireEvent.click(screen.getByRole("button", { name: "快速叫下一个号" }));
await waitFor(() => expect(screen.getByRole("status")).toHaveTextContent("叫号已生效:00013"));
expect(screen.getByRole("status")).toHaveTextContent("业务叫号已生效;1 个设备通道执行失败");
expect(screen.queryByText("叫号未生效")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "删除叫号通知" }));
expect(screen.queryByText("叫号已生效:00013")).not.toBeInTheDocument();
} finally {
callNext.mockRestore();
}
});
it("automatically removes a call notice after five seconds", async () => {
vi.useFakeTimers();
const callNext = vi.spyOn(api, "callNext").mockResolvedValueOnce({
batch: {
id: "batch-2",
batch_number: 5,
status: "CALLED",
called_at: "2026-07-10T09:03:00Z",
ticket_count: 1,
people_count: 4,
tickets: [{ id: "called-2", ticket_number: "00013", status: "CALLED", party_size: 4 }],
},
revision: 9,
device_results: [{ channel: "SIMULATOR", status: "SUCCESS" }],
});
try {
renderScene("/staff");
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: "快速叫下一个号" }));
await Promise.resolve();
});
expect(screen.getByText("叫号已生效:00013")).toBeVisible();
act(() => vi.advanceTimersByTime(4_999));
expect(screen.getByText("叫号已生效:00013")).toBeVisible();
act(() => vi.advanceTimersByTime(1));
expect(screen.queryByText("叫号已生效:00013")).not.toBeInTheDocument();
} finally {
callNext.mockRestore();
vi.useRealTimers();
}
});
it("rejects an over-limit people target instead of silently capping it", async () => {
const callNext = vi.spyOn(api, "callNext");
try {
renderScene("/staff");
fireEvent.change(screen.getByRole("spinbutton", { name: "按人数叫号数量" }), { target: { value: "31" } });
fireEvent.click(screen.getByRole("button", { name: "批量叫人" }));
expect(callNext).not.toHaveBeenCalled();
expect(screen.getByRole("alert")).toHaveTextContent("人数请输入 1 到 30 的整数");
} finally {
callNext.mockRestore();
}
});
it("updates number indicators after creation without showing a success card", async () => {
vi.spyOn(api, "createTicket").mockResolvedValueOnce({
ticket: { id: "ticket-30", ticket_number: "00030", status: "WAITING" },
ticket: { id: "ticket-30", ticket_number: "00030", status: "WAITING", party_size: 3 },
revision: 9,
});
renderScene("/staff/tickets");
@@ -111,15 +234,22 @@ describe("StaffPage scene-focused H5", () => {
expect(screen.getByRole("region", { name: "取号" })).toBeVisible();
expect(screen.getByLabelText("手机号")).toBeVisible();
expect(screen.getByRole("button", { name: "创建排队号码" })).toBeVisible();
expect(screen.getByRole("region", { name: "取号指标" })).toHaveTextContent("上个取号00013");
expect(screen.getByRole("region", { name: "取号指标" })).toHaveTextContent("下个取号00014");
expect(screen.getByRole("region", { name: "取号指标" })).toHaveTextContent("上个取号00037");
expect(screen.getByRole("region", { name: "取号指标" })).toHaveTextContent("下个取号00038");
expect(screen.queryByLabelText("等待队列表,可横向滚动")).not.toBeInTheDocument();
expect(screen.queryByText("叫下一批(1 个号码)")).not.toBeInTheDocument();
fireEvent.change(screen.getByLabelText("手机号"), { target: { value: "13800138000" } });
const partySizeInput = screen.getByRole("spinbutton", { name: "同行人数" });
expect(partySizeInput).toHaveValue(1);
fireEvent.change(partySizeInput, { target: { value: "" } });
expect(partySizeInput).toHaveValue(null);
fireEvent.change(partySizeInput, { target: { value: "3" } });
expect(partySizeInput).toHaveValue(3);
fireEvent.click(screen.getByRole("button", { name: "创建排队号码" }));
await waitFor(() => expect(screen.getByRole("region", { name: "取号指标" })).toHaveTextContent("上个取号00030"));
expect(screen.getByRole("region", { name: "取号指标" })).toHaveTextContent("3 人");
expect(screen.getByRole("region", { name: "取号指标" })).toHaveTextContent("下个取号00031");
expect(screen.queryByText("取号成功:00030")).not.toBeInTheDocument();
expect(screen.queryByText("打开游客状态页")).not.toBeInTheDocument();