feat: bootstrap auto virtual tryon admin frontend
This commit is contained in:
99
tests/features/orders/order-detail.test.tsx
Normal file
99
tests/features/orders/order-detail.test.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { test, expect } from "vitest";
|
||||
|
||||
import { OrderDetail } from "@/features/orders/order-detail";
|
||||
import type { OrderDetailVM } from "@/lib/types/view-models";
|
||||
|
||||
const BASE_ORDER_DETAIL: OrderDetailVM = {
|
||||
orderId: 101,
|
||||
workflowId: "wf-101",
|
||||
customerLevel: "mid",
|
||||
serviceMode: "semi_pro",
|
||||
status: "waiting_review",
|
||||
statusMeta: {
|
||||
label: "待审核",
|
||||
tone: "warning",
|
||||
},
|
||||
currentStep: "review",
|
||||
currentStepLabel: "人工审核",
|
||||
modelId: 1001,
|
||||
poseId: 2002,
|
||||
garmentAssetId: 3003,
|
||||
sceneRefAssetId: 4004,
|
||||
currentRevisionAssetId: null,
|
||||
currentRevisionVersion: null,
|
||||
latestRevisionAssetId: null,
|
||||
latestRevisionVersion: null,
|
||||
revisionCount: 0,
|
||||
reviewTaskStatus: null,
|
||||
pendingManualConfirm: false,
|
||||
createdAt: "2026-03-27T00:00:00Z",
|
||||
updatedAt: "2026-03-27T00:10:00Z",
|
||||
finalAsset: {
|
||||
id: 88,
|
||||
orderId: 101,
|
||||
type: "final",
|
||||
stepName: null,
|
||||
parentAssetId: null,
|
||||
rootAssetId: null,
|
||||
versionNo: 0,
|
||||
isCurrentVersion: false,
|
||||
stepLabel: "最终图",
|
||||
label: "最终图",
|
||||
uri: "mock://final-preview",
|
||||
metadata: null,
|
||||
createdAt: "2026-03-27T00:10:00Z",
|
||||
isMock: true,
|
||||
},
|
||||
finalAssetState: {
|
||||
kind: "ready",
|
||||
},
|
||||
assets: [
|
||||
{
|
||||
id: 77,
|
||||
orderId: 101,
|
||||
type: "fusion",
|
||||
stepName: "fusion",
|
||||
parentAssetId: null,
|
||||
rootAssetId: null,
|
||||
versionNo: 0,
|
||||
isCurrentVersion: false,
|
||||
stepLabel: "融合",
|
||||
label: "融合产物",
|
||||
uri: "mock://fusion-preview",
|
||||
metadata: null,
|
||||
createdAt: "2026-03-27T00:09:00Z",
|
||||
isMock: true,
|
||||
},
|
||||
],
|
||||
assetGalleryState: {
|
||||
kind: "ready",
|
||||
},
|
||||
hasMockAssets: true,
|
||||
};
|
||||
|
||||
test("shows a mock asset banner when the current order contains mock assets", () => {
|
||||
render(<OrderDetail viewModel={BASE_ORDER_DETAIL} />);
|
||||
|
||||
expect(screen.getByText("当前资产来自 mock 流程")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("最终图").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders the business-empty final-result state when no final asset exists", () => {
|
||||
render(
|
||||
<OrderDetail
|
||||
viewModel={{
|
||||
...BASE_ORDER_DETAIL,
|
||||
finalAsset: null,
|
||||
finalAssetState: {
|
||||
kind: "business-empty",
|
||||
title: "最终图暂未生成",
|
||||
description: "当前订单还没有可展示的最终结果。",
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("最终图暂未生成")).toBeInTheDocument();
|
||||
expect(screen.getByText("当前订单还没有可展示的最终结果。")).toBeInTheDocument();
|
||||
});
|
||||
61
tests/features/orders/orders-home.test.tsx
Normal file
61
tests/features/orders/orders-home.test.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { expect, test, vi } from "vitest";
|
||||
|
||||
import { OrdersHome } from "@/features/orders/orders-home";
|
||||
import type { OrderSummaryVM } from "@/lib/types/view-models";
|
||||
|
||||
const RECENT_ORDERS: OrderSummaryVM[] = [
|
||||
{
|
||||
orderId: 4201,
|
||||
workflowId: "wf-4201",
|
||||
status: "waiting_review",
|
||||
statusMeta: {
|
||||
label: "待审核",
|
||||
tone: "warning",
|
||||
},
|
||||
currentStep: "review",
|
||||
currentStepLabel: "人工审核",
|
||||
updatedAt: "2026-03-27T09:15:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
test("shows the real recent-orders entry state", () => {
|
||||
render(<OrdersHome recentOrders={RECENT_ORDERS} />);
|
||||
|
||||
expect(screen.getByText("最近订单已接入真实后端接口")).toBeInTheDocument();
|
||||
expect(screen.getByText("订单 #4201")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("supports status filtering and pagination actions", () => {
|
||||
const onStatusChange = vi.fn();
|
||||
const onPageChange = vi.fn();
|
||||
const onQuerySubmit = vi.fn();
|
||||
|
||||
render(
|
||||
<OrdersHome
|
||||
currentPage={2}
|
||||
onPageChange={onPageChange}
|
||||
onQuerySubmit={onQuerySubmit}
|
||||
onStatusChange={onStatusChange}
|
||||
recentOrders={RECENT_ORDERS}
|
||||
selectedStatus="waiting_review"
|
||||
selectedQuery=""
|
||||
totalPages={3}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("订单关键词搜索"), {
|
||||
target: { value: "order-4201" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "搜索订单" }));
|
||||
fireEvent.change(screen.getByLabelText("订单状态筛选"), {
|
||||
target: { value: "succeeded" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "上一页" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "下一页" }));
|
||||
|
||||
expect(onQuerySubmit).toHaveBeenCalledWith("order-4201");
|
||||
expect(onStatusChange).toHaveBeenCalledWith("succeeded");
|
||||
expect(onPageChange).toHaveBeenNthCalledWith(1, 1);
|
||||
expect(onPageChange).toHaveBeenNthCalledWith(2, 3);
|
||||
});
|
||||
232
tests/features/orders/submit-workbench.test.tsx
Normal file
232
tests/features/orders/submit-workbench.test.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
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(<SubmitWorkbench />);
|
||||
|
||||
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(<SubmitWorkbench />);
|
||||
|
||||
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(<SubmitWorkbench />);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user