import {
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { SubmitWorkbench } from "@/features/orders/submit-workbench";
import {
GARMENT_LIBRARY_FIXTURES,
MODEL_LIBRARY_FIXTURES,
SCENE_LIBRARY_FIXTURES,
} from "@/lib/mock/libraries";
const { pushMock } = vi.hoisted(() => ({
pushMock: vi.fn(),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: pushMock,
}),
}));
type LibraryType = "models" | "scenes" | "garments";
function createLibraryPayload(libraryType: LibraryType) {
const fixtureMap = {
models: MODEL_LIBRARY_FIXTURES,
scenes: SCENE_LIBRARY_FIXTURES,
garments: GARMENT_LIBRARY_FIXTURES,
};
return {
mode: "placeholder",
message: "资源库当前使用占位数据,真实后端接口尚未提供。",
data: {
items: fixtureMap[libraryType],
},
};
}
function createFetchMock({
orderResponse,
}: {
orderResponse?: Response;
} = {}) {
return vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "/api/libraries/models") {
return new Response(JSON.stringify(createLibraryPayload("models")), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url === "/api/libraries/scenes") {
return new Response(JSON.stringify(createLibraryPayload("scenes")), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url === "/api/libraries/garments") {
return new Response(JSON.stringify(createLibraryPayload("garments")), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url === "/api/orders" && init?.method === "POST" && orderResponse) {
return orderResponse;
}
throw new Error(`Unhandled fetch: ${url}`);
});
}
async function chooseSelectOption(label: string, optionName: string) {
fireEvent.click(screen.getByRole("combobox", { name: label }));
fireEvent.click(await screen.findByRole("option", { name: optionName }));
}
beforeEach(() => {
pushMock.mockReset();
});
afterEach(() => {
vi.unstubAllGlobals();
});
test("forces low customers to use auto_basic and mid customers to use semi_pro", async () => {
vi.stubGlobal("fetch", createFetchMock());
render();
await screen.findByText("Ava / Studio");
await chooseSelectOption("客户层级", "低客单 low");
expect(screen.getByRole("combobox", { name: "服务模式" })).toHaveTextContent(
"自动基础处理 auto_basic",
);
fireEvent.click(screen.getByRole("combobox", { name: "服务模式" }));
expect(
screen.getByRole("option", { name: "半人工专业处理 semi_pro" }),
).toHaveAttribute("data-disabled");
fireEvent.keyDown(document.activeElement ?? document.body, {
key: "Escape",
});
await chooseSelectOption("客户层级", "中客单 mid");
expect(screen.getByRole("combobox", { name: "服务模式" })).toHaveTextContent(
"半人工专业处理 semi_pro",
);
fireEvent.click(screen.getByRole("combobox", { name: "服务模式" }));
expect(
screen.getByRole("option", { name: "自动基础处理 auto_basic" }),
).toHaveAttribute("data-disabled");
});
test("preserves selected values when order submission fails", async () => {
vi.stubGlobal(
"fetch",
createFetchMock({
orderResponse: new Response(
JSON.stringify({
error: "BACKEND_UNAVAILABLE",
message: "后端暂时不可用,请稍后重试。",
}),
{
status: 502,
headers: { "content-type": "application/json" },
},
),
}),
);
render();
await screen.findByText("Ava / Studio");
await chooseSelectOption("客户层级", "低客单 low");
await chooseSelectOption("模特资源", "Ava / Studio");
await chooseSelectOption("场景资源", "Loft Window");
await chooseSelectOption("服装资源", "Structured Coat 01");
const summaryCard = screen.getByRole("region", { name: "提单摘要" });
expect(within(summaryCard).getByText("Ava / Studio")).toBeInTheDocument();
expect(within(summaryCard).getByText("Loft Window")).toBeInTheDocument();
expect(within(summaryCard).getByText("Structured Coat 01")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "提交订单" }));
expect(
await screen.findByText("后端暂时不可用,请稍后重试。"),
).toBeInTheDocument();
expect(screen.getByRole("combobox", { name: "客户层级" })).toHaveTextContent(
"低客单 low",
);
expect(screen.getByRole("combobox", { name: "服务模式" })).toHaveTextContent(
"自动基础处理 auto_basic",
);
expect(screen.getByRole("combobox", { name: "模特资源" })).toHaveTextContent(
"Ava / Studio",
);
expect(screen.getByRole("combobox", { name: "场景资源" })).toHaveTextContent(
"Loft Window",
);
expect(screen.getByRole("combobox", { name: "服装资源" })).toHaveTextContent(
"Structured Coat 01",
);
});
test("submits the selected resources, surfaces returned ids, and redirects to the order detail page", async () => {
const fetchMock = createFetchMock({
orderResponse: new Response(
JSON.stringify({
mode: "proxy",
data: {
orderId: 77,
workflowId: "wf-77",
status: "created",
},
}),
{
status: 201,
headers: { "content-type": "application/json" },
},
),
});
vi.stubGlobal("fetch", fetchMock);
render();
await screen.findByText("Ava / Studio");
await chooseSelectOption("客户层级", "低客单 low");
await chooseSelectOption("模特资源", "Ava / Studio");
await chooseSelectOption("场景资源", "Loft Window");
await chooseSelectOption("服装资源", "Structured Coat 01");
fireEvent.click(screen.getByRole("button", { name: "提交订单" }));
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(
"/api/orders",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
customer_level: "low",
service_mode: "auto_basic",
model_id: 101,
garment_asset_id: 303,
scene_ref_asset_id: 404,
}),
}),
);
});
expect(
await screen.findByText("订单 #77 已创建,正在跳转到详情页。"),
).toBeInTheDocument();
expect(screen.getByText("工作流 ID wf-77")).toBeInTheDocument();
await waitFor(() => {
expect(pushMock).toHaveBeenCalledWith("/orders/77");
});
});