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(); 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(); 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(resolve => { finish = resolve; }) : Promise.resolve(new Response(JSON.stringify({ tasks: [pending] })))); const view = render(); 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(); 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(); 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(); 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(); 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(); 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(); 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"); }); });