feat: bootstrap auto virtual tryon admin frontend

This commit is contained in:
afei A
2026-03-27 23:38:50 +08:00
commit 98c6b741d6
119 changed files with 19046 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
import { LibraryPage } from "@/features/libraries/library-page";
import type { LibraryItemVM } from "@/lib/types/view-models";
const MODEL_ITEMS: LibraryItemVM[] = [
{
id: "model-ava",
libraryType: "models",
name: "Ava / Studio",
description: "中性棚拍模特占位数据,用于提交页联调。",
previewUri: "mock://libraries/models/ava",
tags: ["女装", "半身", "mock"],
isMock: true,
},
];
test("states that the resource library is still backed by mock data", () => {
render(<LibraryPage libraryType="models" items={MODEL_ITEMS} />);
expect(screen.getByText("当前资源库仍使用 mock 数据")).toBeInTheDocument();
expect(screen.getByText("Ava / Studio")).toBeInTheDocument();
});

View 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();
});

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

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

View File

@@ -0,0 +1,314 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, expect, test, vi } from "vitest";
import { ReviewWorkbenchDetailScreen } from "@/features/reviews/review-workbench-detail";
const pushMock = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: pushMock,
}),
}));
function createOrderDetailPayload(
orderId = 101,
options: { pendingManualConfirm?: boolean } = {},
) {
return {
mode: "proxy",
data: {
orderId,
workflowId: `wf-${orderId}`,
customerLevel: "mid",
serviceMode: "semi_pro",
status: "waiting_review",
statusMeta: {
label: "待审核",
tone: "warning",
},
currentStep: "review",
currentStepLabel: "人工审核",
modelId: 101,
poseId: 202,
garmentAssetId: 303,
sceneRefAssetId: 404,
currentRevisionAssetId: null,
currentRevisionVersion: null,
latestRevisionAssetId: null,
latestRevisionVersion: null,
revisionCount: 0,
reviewTaskStatus: "pending",
pendingManualConfirm: options.pendingManualConfirm ?? false,
createdAt: "2026-03-27T00:00:00Z",
updatedAt: "2026-03-27T00:10:00Z",
finalAsset: null,
finalAssetState: {
kind: "business-empty",
title: "最终图暂未生成",
description: "当前订单还没有可展示的最终结果。",
},
assets: [
{
id: 11,
orderId,
type: "fusion",
stepName: "fusion",
stepLabel: "融合",
label: "融合产物",
uri: "mock://fusion-preview",
metadata: null,
createdAt: "2026-03-27T00:09:00Z",
isMock: true,
},
],
assetGalleryState: {
kind: "ready",
},
hasMockAssets: true,
},
};
}
function createWorkflowPayload(
orderId = 101,
options: { pendingManualConfirm?: boolean } = {},
) {
return {
mode: "proxy",
data: {
orderId,
workflowId: `wf-${orderId}`,
workflowType: "mid_end",
status: "waiting_review",
currentRevisionAssetId: null,
currentRevisionVersion: null,
latestRevisionAssetId: null,
latestRevisionVersion: null,
revisionCount: 0,
reviewTaskStatus: "pending",
pendingManualConfirm: options.pendingManualConfirm ?? false,
statusMeta: {
label: "待审核",
tone: "warning",
},
currentStep: "review",
currentStepLabel: "人工审核",
createdAt: "2026-03-27T00:00:00Z",
updatedAt: "2026-03-27T00:10:00Z",
steps: [
{
id: 1,
workflowRunId: 9001,
name: "review",
label: "人工审核",
status: "waiting",
statusMeta: {
label: "等待人工处理",
tone: "warning",
},
input: null,
output: null,
errorMessage: null,
startedAt: "2026-03-27T00:09:00Z",
endedAt: null,
containsMockAssets: true,
mockAssetUris: ["mock://fusion-preview"],
isCurrent: true,
isFailed: false,
},
],
stepTimelineState: {
kind: "ready",
},
failureCount: 0,
hasMockAssets: true,
},
};
}
function createFetchMock(options: { pendingManualConfirm?: boolean } = {}) {
return vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "/api/orders/101") {
return new Response(JSON.stringify(createOrderDetailPayload(101, options)), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url === "/api/workflows/101") {
return new Response(JSON.stringify(createWorkflowPayload(101, options)), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url === "/api/reviews/101/submit" && init?.method === "POST") {
return new Response(
JSON.stringify({
mode: "proxy",
data: {
orderId: 101,
workflowId: "wf-101",
decision: "approve",
decisionMeta: {
label: "通过",
tone: "success",
},
status: "queued",
},
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
);
}
if (url === "/api/orders/101/revisions" && init?.method === "POST") {
return new Response(
JSON.stringify({
mode: "proxy",
data: {
orderId: 101,
workflowId: "wf-101",
assetId: 501,
parentAssetId: 11,
rootAssetId: 11,
versionNo: 1,
reviewTaskStatus: "revision_uploaded",
latestRevisionAssetId: 501,
revisionCount: 1,
},
}),
{
status: 201,
headers: { "content-type": "application/json" },
},
);
}
if (url === "/api/reviews/101/confirm-revision" && init?.method === "POST") {
return new Response(
JSON.stringify({
mode: "proxy",
data: {
orderId: 101,
workflowId: "wf-101",
revisionAssetId: 501,
decision: "approve",
decisionMeta: {
label: "通过",
tone: "success",
},
status: "submitted",
},
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
);
}
throw new Error(`Unhandled fetch: ${url}`);
});
}
afterEach(() => {
vi.unstubAllGlobals();
pushMock.mockReset();
});
test("requires a comment before rerun_face submission in detail view", async () => {
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
render(<ReviewWorkbenchDetailScreen orderId={101} />);
await screen.findByRole("heading", { level: 1, name: "订单 #101" });
fireEvent.click(screen.getByRole("button", { name: "重跑 Face" }));
expect(await screen.findByText("请填写审核备注")).toBeInTheDocument();
expect(fetchMock).not.toHaveBeenCalledWith(
"/api/reviews/101/submit",
expect.anything(),
);
});
test("submits approve then returns to the workbench list", async () => {
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
render(<ReviewWorkbenchDetailScreen orderId={101} />);
await screen.findByRole("heading", { level: 1, name: "订单 #101" });
fireEvent.click(screen.getByRole("button", { name: "审核通过" }));
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(
"/api/reviews/101/submit",
expect.objectContaining({
method: "POST",
}),
);
});
await waitFor(() => {
expect(pushMock).toHaveBeenCalledWith("/reviews/workbench");
});
});
test("registers a manual revision asset from detail view", async () => {
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
render(<ReviewWorkbenchDetailScreen orderId={101} />);
await screen.findByRole("heading", { level: 1, name: "订单 #101" });
fireEvent.change(screen.getByLabelText("修订稿 URI"), {
target: { value: "mock://manual-revision-v1" },
});
fireEvent.change(screen.getByLabelText("修订说明"), {
target: { value: "人工修订第一版" },
});
fireEvent.click(screen.getByRole("button", { name: "登记人工修订稿" }));
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(
"/api/orders/101/revisions",
expect.objectContaining({
method: "POST",
}),
);
});
});
test("confirms an uploaded manual revision and returns to the workbench list", async () => {
const fetchMock = createFetchMock({ pendingManualConfirm: true });
vi.stubGlobal("fetch", fetchMock);
render(<ReviewWorkbenchDetailScreen orderId={101} />);
await screen.findByRole("heading", { level: 1, name: "订单 #101" });
fireEvent.click(screen.getByRole("button", { name: "确认继续流水线" }));
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(
"/api/reviews/101/confirm-revision",
expect.objectContaining({
method: "POST",
}),
);
});
await waitFor(() => {
expect(pushMock).toHaveBeenCalledWith("/reviews/workbench");
});
});

View File

@@ -0,0 +1,62 @@
import { render, screen } from "@testing-library/react";
import { afterEach, expect, test, vi } from "vitest";
import { ReviewWorkbenchListScreen } from "@/features/reviews/review-workbench-list";
function createPendingPayload() {
return {
mode: "proxy",
data: {
items: [
{
reviewTaskId: 301,
orderId: 101,
workflowId: "wf-101",
workflowType: "mid_end",
currentStep: "review",
currentStepLabel: "人工审核",
createdAt: "2026-03-27T00:00:00Z",
status: "waiting_review",
statusMeta: {
label: "待审核",
tone: "warning",
},
hasMockAssets: true,
failureCount: 2,
},
],
state: {
kind: "ready",
},
},
};
}
afterEach(() => {
vi.unstubAllGlobals();
});
test("renders medium-density list rows that link into independent review detail pages", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify(createPendingPayload()), {
status: 200,
headers: {
"content-type": "application/json",
},
}),
);
vi.stubGlobal("fetch", fetchMock);
render(<ReviewWorkbenchListScreen />);
expect(await screen.findByText("审核目标 #101")).toBeInTheDocument();
expect(screen.getByText(/工作流 wf-101/)).toBeInTheDocument();
expect(screen.getByText("Mock 资产")).toBeInTheDocument();
expect(screen.getByText("失败 2")).toBeInTheDocument();
expect(screen.queryByText("审核动作面板")).not.toBeInTheDocument();
expect(screen.getByRole("link", { name: /审核目标 #101/ })).toHaveAttribute(
"href",
"/reviews/workbench/101",
);
});

View File

@@ -0,0 +1,84 @@
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
import { WorkflowDetail } from "@/features/workflows/workflow-detail";
import type { WorkflowDetailVM } from "@/lib/types/view-models";
const BASE_WORKFLOW_DETAIL: WorkflowDetailVM = {
orderId: 101,
workflowId: "wf-101",
workflowType: "mid_end",
status: "waiting_review",
statusMeta: {
label: "待审核",
tone: "warning",
},
currentStep: "review",
currentStepLabel: "人工审核",
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",
steps: [
{
id: 11,
workflowRunId: 9001,
name: "fusion",
label: "融合",
status: "failed",
statusMeta: {
label: "失败",
tone: "danger",
},
input: null,
output: null,
errorMessage: "Temporal activity timed out.",
startedAt: "2026-03-27T00:06:00Z",
endedAt: "2026-03-27T00:07:00Z",
containsMockAssets: false,
mockAssetUris: [],
isCurrent: false,
isFailed: true,
},
{
id: 12,
workflowRunId: 9001,
name: "review",
label: "人工审核",
status: "waiting",
statusMeta: {
label: "等待人工处理",
tone: "warning",
},
input: null,
output: {
preview_uri: "mock://fusion-preview",
},
errorMessage: null,
startedAt: "2026-03-27T00:09:00Z",
endedAt: null,
containsMockAssets: true,
mockAssetUris: ["mock://fusion-preview"],
isCurrent: true,
isFailed: false,
},
],
stepTimelineState: {
kind: "ready",
},
failureCount: 1,
hasMockAssets: true,
};
test("highlights failed steps and mock asset hints in the workflow timeline", () => {
render(<WorkflowDetail viewModel={BASE_WORKFLOW_DETAIL} />);
expect(screen.getByText("失败步骤 1 个")).toBeInTheDocument();
expect(screen.getByText("Temporal activity timed out.")).toBeInTheDocument();
expect(screen.getByText("当前流程包含 mock 资产")).toBeInTheDocument();
});

View File

@@ -0,0 +1,62 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import { WorkflowLookup } from "@/features/workflows/workflow-lookup";
import type { WorkflowLookupItemVM } from "@/lib/types/view-models";
const WORKFLOW_ITEMS: WorkflowLookupItemVM[] = [
{
orderId: 4201,
workflowId: "wf-4201",
workflowType: "MidEndPipelineWorkflow",
status: "waiting_review",
statusMeta: {
label: "待审核",
tone: "warning",
},
currentStep: "review",
currentStepLabel: "人工审核",
updatedAt: "2026-03-27T09:15:00Z",
},
];
test("shows the real recent-workflows entry state", () => {
render(<WorkflowLookup items={WORKFLOW_ITEMS} />);
expect(screen.getByText("流程追踪首页当前显示真实后端最近流程。")).toBeInTheDocument();
expect(screen.getByText("订单 #4201")).toBeInTheDocument();
});
test("supports workflow status filtering and pagination actions", () => {
const onStatusChange = vi.fn();
const onPageChange = vi.fn();
const onQuerySubmit = vi.fn();
render(
<WorkflowLookup
currentPage={2}
items={WORKFLOW_ITEMS}
onPageChange={onPageChange}
onQuerySubmit={onQuerySubmit}
onStatusChange={onStatusChange}
selectedStatus="waiting_review"
selectedQuery=""
totalPages={3}
/>,
);
fireEvent.change(screen.getByLabelText("流程关键词搜索"), {
target: { value: "4201" },
});
fireEvent.click(screen.getByRole("button", { name: "搜索流程" }));
fireEvent.change(screen.getByLabelText("流程状态筛选"), {
target: { value: "failed" },
});
fireEvent.click(screen.getByRole("button", { name: "上一页" }));
fireEvent.click(screen.getByRole("button", { name: "下一页" }));
expect(onQuerySubmit).toHaveBeenCalledWith("4201");
expect(onStatusChange).toHaveBeenCalledWith("failed");
expect(onPageChange).toHaveBeenNthCalledWith(1, 1);
expect(onPageChange).toHaveBeenNthCalledWith(2, 3);
});