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"); }); });