315 lines
8.3 KiB
TypeScript
315 lines
8.3 KiB
TypeScript
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");
|
|
});
|
|
});
|