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}`); }); } 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(); const customerLevelSelect = await screen.findByLabelText("客户层级"); const serviceModeSelect = screen.getByLabelText("服务模式"); fireEvent.change(customerLevelSelect, { target: { value: "low" }, }); expect(serviceModeSelect).toHaveValue("auto_basic"); expect( screen.getByRole("option", { name: "半人工专业处理 semi_pro" }), ).toBeDisabled(); fireEvent.change(customerLevelSelect, { target: { value: "mid" }, }); expect(serviceModeSelect).toHaveValue("semi_pro"); expect( screen.getByRole("option", { name: "自动基础处理 auto_basic" }), ).toBeDisabled(); }); 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"); fireEvent.change(screen.getByLabelText("客户层级"), { target: { value: "low" }, }); fireEvent.change(screen.getByLabelText("模特资源"), { target: { value: "model-ava" }, }); fireEvent.change(screen.getByLabelText("场景资源"), { target: { value: "scene-loft" }, }); fireEvent.change(screen.getByLabelText("服装资源"), { target: { value: "garment-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.getByLabelText("客户层级")).toHaveValue("low"); expect(screen.getByLabelText("服务模式")).toHaveValue("auto_basic"); expect(screen.getByLabelText("模特资源")).toHaveValue("model-ava"); expect(screen.getByLabelText("场景资源")).toHaveValue("scene-loft"); expect(screen.getByLabelText("服装资源")).toHaveValue("garment-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"); fireEvent.change(screen.getByLabelText("客户层级"), { target: { value: "low" }, }); fireEvent.change(screen.getByLabelText("模特资源"), { target: { value: "model-ava" }, }); fireEvent.change(screen.getByLabelText("场景资源"), { target: { value: "scene-loft" }, }); fireEvent.change(screen.getByLabelText("服装资源"), { target: { value: "garment-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, pose_id: 202, 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"); }); });