317 lines
9.2 KiB
TypeScript
317 lines
9.2 KiB
TypeScript
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 }));
|
|
}
|
|
|
|
async function chooseLibraryResource(label: string, resourceName: string) {
|
|
fireEvent.click(screen.getByRole("button", { name: `选择${label}` }));
|
|
|
|
const dialog = await screen.findByRole("dialog", { name: `选择${label}` });
|
|
fireEvent.click(within(dialog).getByRole("button", { name: resourceName }));
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.queryByRole("dialog", { name: `选择${label}` }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
}
|
|
|
|
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(<SubmitWorkbench />);
|
|
|
|
await screen.findByRole("button", { name: "选择模特资源" });
|
|
|
|
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(<SubmitWorkbench />);
|
|
|
|
await screen.findByRole("button", { name: "选择模特资源" });
|
|
|
|
await chooseSelectOption("客户层级", "低客单 low");
|
|
await chooseLibraryResource("模特资源", "Ava / Studio");
|
|
await chooseLibraryResource("场景资源", "Loft Window");
|
|
await chooseLibraryResource("服装资源", "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.getAllByText("Ava / Studio").length).toBeGreaterThan(1);
|
|
expect(screen.getAllByText("Loft Window").length).toBeGreaterThan(1);
|
|
expect(screen.getAllByText("Structured Coat 01").length).toBeGreaterThan(1);
|
|
});
|
|
|
|
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(<SubmitWorkbench />);
|
|
|
|
await screen.findByRole("button", { name: "选择模特资源" });
|
|
|
|
await chooseSelectOption("客户层级", "低客单 low");
|
|
await chooseLibraryResource("模特资源", "Ava / Studio");
|
|
await chooseLibraryResource("场景资源", "Loft Window");
|
|
await chooseLibraryResource("服装资源", "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");
|
|
});
|
|
});
|
|
|
|
test("allows submitting without selecting a scene resource", async () => {
|
|
const fetchMock = createFetchMock({
|
|
orderResponse: new Response(
|
|
JSON.stringify({
|
|
mode: "proxy",
|
|
data: {
|
|
orderId: 88,
|
|
workflowId: "wf-88",
|
|
status: "created",
|
|
},
|
|
}),
|
|
{
|
|
status: 201,
|
|
headers: { "content-type": "application/json" },
|
|
},
|
|
),
|
|
});
|
|
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
render(<SubmitWorkbench />);
|
|
|
|
await screen.findByRole("button", { name: "选择模特资源" });
|
|
|
|
await chooseSelectOption("客户层级", "低客单 low");
|
|
await chooseLibraryResource("模特资源", "Ava / Studio");
|
|
await chooseLibraryResource("服装资源", "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,
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
test("opens a shared resource manager modal and picks a resource card for each library type", async () => {
|
|
vi.stubGlobal("fetch", createFetchMock());
|
|
|
|
render(<SubmitWorkbench />);
|
|
|
|
await screen.findByRole("button", { name: "选择模特资源" });
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "选择模特资源" }));
|
|
|
|
const modelDialog = await screen.findByRole("dialog", { name: "选择模特资源" });
|
|
expect(within(modelDialog).getByTestId("resource-picker-masonry").className).toContain(
|
|
"columns-1",
|
|
);
|
|
expect(
|
|
within(modelDialog).getByRole("button", { name: "Ava / Studio" }),
|
|
).toBeInTheDocument();
|
|
|
|
fireEvent.click(within(modelDialog).getByRole("button", { name: "Ava / Studio" }));
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.queryByRole("dialog", { name: "选择模特资源" }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
const summaryCard = screen.getByRole("region", { name: "提单摘要" });
|
|
expect(within(summaryCard).getByText("Ava / Studio")).toBeInTheDocument();
|
|
});
|