Files
agent/apps/web/tests/image-video.test.tsx

90 lines
7.5 KiB
TypeScript

import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ApprovalDialog } from "../src/components/approvals/ApprovalDialog.js";
import { ImageVideoPanel, ImageVideoTaskCard } from "../src/components/tools/ImageVideoPanel.js";
const id = "i2v_11111111-1111-1111-1111-111111111111";
const task = { id, title: "Window", prompt: "Turn naturally toward the window; ambient rain", durationSeconds: 10, state: "queued", referenceUrl: `/api/image-video/tasks/${id}/reference`, createdAt: "2026-09-15T00:00:00Z", updatedAt: "2026-09-15T00:00:00Z" };
afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers(); });
describe("image video UI", () => {
it("continues the exact task only after confirmation and never sends or generates automatically", async () => {
const onDraft = vi.fn(); const requests: { url: string; body: unknown }[] = [];
const pending = { ...task, state: "awaiting_submission", revision: 9 };
vi.stubGlobal("fetch", (url: string, init?: RequestInit) => {
if (init?.method === "POST") requests.push({ url, body: init.body });
return Promise.resolve(new Response(JSON.stringify(init?.method === "POST" ? { task: { ...pending, revision: 10 } } : { tasks: [pending] })));
});
render(<ImageVideoPanel sessionId="ses_new" onDraft={onDraft} />);
const user = userEvent.setup();
await user.click(await screen.findByRole("button", { name: "继续此任务" }));
expect(screen.getByRole("button", { name: "确认接续(不生成)" })).toBeDisabled();
expect(requests).toEqual([]);
await user.click(screen.getByRole("checkbox", { name: /确认将此任务接续到当前会话/ }));
await user.click(screen.getByRole("button", { name: "确认接续(不生成)" }));
await waitFor(() => expect(onDraft).toHaveBeenCalledWith(expect.objectContaining({ id, state: "awaiting_submission", revision: 10 })));
expect(requests).toEqual([{ url: `/api/sessions/ses_new/image-video/tasks/${id}/continue`, body: JSON.stringify({ confirmed: true, expectedRevision: 9 }) }]);
});
it("does not offer continuation for unknown submission/upload outcomes", async () => {
vi.stubGlobal("fetch", () => Promise.resolve(new Response(JSON.stringify({ tasks: [{ ...task, revision: 2, state: "unknown_upload" }] }))));
render(<ImageVideoPanel sessionId="ses_new" onDraft={vi.fn()} />);
expect(await screen.findByText(/上传状态未知/)).toBeVisible();
expect(screen.queryByRole("button", { name: "继续此任务" })).not.toBeInTheDocument();
});
it("ignores a late continuation response after switching sessions", async () => {
const onDraft = vi.fn(); const pending = { ...task, revision: 9, state: "awaiting_submission" };
let finish!: (response: Response) => void;
vi.stubGlobal("fetch", (_url: string, init?: RequestInit) => init?.method === "POST" ? new Promise<Response>(resolve => { finish = resolve; }) : Promise.resolve(new Response(JSON.stringify({ tasks: [pending] }))));
const view = render(<ImageVideoPanel sessionId="ses_old" onDraft={onDraft} />); const user = userEvent.setup();
await user.click(await screen.findByRole("button", { name: "继续此任务" }));
await user.click(screen.getByRole("checkbox", { name: /确认将此任务接续到当前会话/ }));
await user.click(screen.getByRole("button", { name: "确认接续(不生成)" }));
view.rerender(<ImageVideoPanel sessionId="ses_new" onDraft={onDraft} />);
await act(() => { finish(new Response(JSON.stringify({ task: pending }))); return Promise.resolve(); });
expect(onDraft).not.toHaveBeenCalled();
expect(screen.getByRole("button", { name: "继续此任务" })).toBeEnabled();
});
it("restores queued tasks, polls at 30 seconds, and cleans up after unmount", async () => {
vi.useFakeTimers();
const fetcher = vi.fn(() => Promise.resolve(new Response(JSON.stringify({ tasks: [task] }))));
vi.stubGlobal("fetch", fetcher); const view = render(<ImageVideoPanel />);
await act(() => vi.advanceTimersByTimeAsync(0));
expect(screen.getByText("Window")).toBeVisible(); expect(screen.getByText(/排队中/)).toBeVisible();
await act(() => vi.advanceTimersByTimeAsync(29_999)); expect(fetcher).toHaveBeenCalledTimes(1);
await act(() => vi.advanceTimersByTimeAsync(1)); expect(fetcher).toHaveBeenCalledTimes(2);
view.unmount(); await act(() => vi.advanceTimersByTimeAsync(60_000)); expect(fetcher).toHaveBeenCalledTimes(2);
});
it("creates a draft from a local image without needing model image capability", async () => {
const onDraft = vi.fn(); const requests: { url: string; body: unknown }[] = [];
vi.stubGlobal("fetch", (url: string, init?: RequestInit) => { requests.push({ url, body: init?.body }); return Promise.resolve(new Response(JSON.stringify(url.includes("attachments") ? { attachment: { attachmentId: "att_local" } } : init?.method === "POST" ? { task } : { tasks: [] }))); });
render(<ImageVideoPanel sessionId="ses_1" onDraft={onDraft} />); const user = userEvent.setup();
await user.upload(screen.getByLabelText("参考图片"), new File(["test"], "test.png", { type: "image/png" }));
await user.type(screen.getByLabelText("标题"), "Window"); await user.type(screen.getByLabelText("动作与声音"), task.prompt);
await user.click(screen.getByRole("button", { name: "准备本地任务" }));
await waitFor(() => expect(onDraft).toHaveBeenCalledWith(expect.objectContaining({ id })));
expect(requests.some(request => request.url.includes("/sessions/ses_1/attachments"))).toBe(true);
expect(requests.filter(request => request.url.endsWith("/image-video/tasks") && typeof request.body === "string")[0]?.body).toContain('"attachmentId":"att_local"');
expect(requests.some(request => /submit|upload_reference/.test(request.url))).toBe(false);
});
it("renders a native player/download only for this registered task video", () => {
const view = render(<ImageVideoTaskCard task={{ ...task, state: "ready", videoUrl: `/api/image-video/tasks/${id}/video` }} />);
expect(screen.getByLabelText("生成预览播放器")).toHaveAttribute("controls");
expect(screen.getByRole("link", { name: "下载 MP4" })).toHaveAttribute("href", `/api/image-video/tasks/${id}/video?download=1`);
expect(screen.getByRole("status")).toHaveTextContent("待视听检查");
view.rerender(<ImageVideoTaskCard task={{ ...task, state: "ready", videoUrl: "https://evil.example/video" }} />);
expect(screen.queryByRole("link", { name: "下载 MP4" })).not.toBeInTheDocument();
});
it("requires an unchecked confirmation after showing the frozen reference/prompt/duration", async () => {
vi.stubGlobal("fetch", () => Promise.resolve(new Response(JSON.stringify({ task }))));
const approve = vi.fn();
render(<ApprovalDialog request={{ approvalId: "ap_i2v", toolCallId: "tc", toolName: "i2v_submit", arguments: { taskId: id }, riskLevel: "medium", expiresAt: "2099-01-01T00:00:00Z", status: "pending" }} onApprove={approve} onDeny={vi.fn()} />);
expect(await screen.findByText(task.prompt)).toBeVisible();
expect(screen.getByRole("img")).toHaveAttribute("src", task.referenceUrl);
expect(screen.getByText(/10 秒/)).toBeVisible(); expect(screen.getByText(/费用未知/)).toBeVisible();
const button = screen.getByRole("button", { name: "批准 H3 生成" }); expect(button).toBeDisabled();
await userEvent.click(screen.getByRole("checkbox")); expect(button).toBeEnabled();
await userEvent.click(button); expect(approve).toHaveBeenCalledWith("ap_i2v");
});
});