diff --git a/apps/web/src/test/pages/generate/useTemplateSelection.test.tsx b/apps/web/src/test/pages/generate/useTemplateSelection.test.tsx deleted file mode 100644 index 44967c8bb..000000000 --- a/apps/web/src/test/pages/generate/useTemplateSelection.test.tsx +++ /dev/null @@ -1,174 +0,0 @@ -/** - * useTemplateSelection Hook 单测(#1777) - * - 自动选择跳过无片段/inactive 模板,只选第一个有效模板 - * - 传 validOnly=true 给后端 - * - 用户手动选择优先,自动逻辑不覆盖 - * - handleInvalidTemplate:失效时自动切到第一个有效模板 + toast;无有效模板时清空 - * - selectedTemplate 仅内存态,不写入 localStorage/sessionStorage - */ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" -import { renderHook, waitFor, act } from "@testing-library/react" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import type { ReactNode } from "react" - -// antd message mock(拦截 toast)——vi.hoisted 保证 mock 工厂可引用 -const { messageMock } = vi.hoisted(() => ({ - messageMock: { - warning: vi.fn(), - error: vi.fn(), - success: vi.fn(), - info: vi.fn(), - loading: vi.fn(() => vi.fn()), - }, -})) -vi.mock("antd", () => ({ message: messageMock })) - -vi.mock("@/api/editing-planner", () => ({ - getEditingTemplates: vi.fn(), -})) - -import { getEditingTemplates } from "@/api/editing-planner" -import type { EditingTemplate } from "@/api/editing-planner" -import { useTemplateSelection } from "@/pages/generate/hooks/useGenerateFormState/useTemplateSelection" - -const mockGetTemplates = vi.mocked(getEditingTemplates) - -function tpl(id: string, partial: Partial = {}): EditingTemplate { - return { - id, - name: id, - mode: "pip", - category: "默认", - tags: [], - title_config: { - ai_auto_select: false, - content: "", - font_preset: "", - font_color: "", - font_size: 28, - position: "top", - }, - subtitle_config: { - enabled: true, - position: "bottom", - font: "", - color: "", - size: 20, - animation: "", - }, - bgm_config: { enabled: false, music_id: "" }, - segments: [{ segment_order: 0, material_type: null }], - is_active: true, - created_at: "", - updated_at: "", - ...partial, - } as EditingTemplate -} - -function createWrapper() { - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false, gcTime: 0 } }, - }) - return ({ children }: { children: ReactNode }) => - ({children}) as ReactNode -} - -beforeEach(() => { - vi.clearAllMocks() - localStorage.clear() - sessionStorage.clear() -}) - -afterEach(() => { - localStorage.clear() - sessionStorage.clear() -}) - -describe("useTemplateSelection (#1777)", () => { - it("请求模板时传 validOnly=true,并自动选中第一个有片段的有效模板", async () => { - mockGetTemplates.mockResolvedValueOnce([ - tpl("empty", { segments: [] }), - tpl("inactive", { is_active: false }), - tpl("valid-a"), - tpl("valid-b"), - ]) - const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() }) - - await waitFor(() => expect(result.current.selectedTemplate).toBe("valid-a")) - expect(mockGetTemplates).toHaveBeenCalledWith({ validOnly: true }) - // 暴露给 UI 的 userTemplates 已过滤掉无效模板 - expect(result.current.userTemplates.map((t) => t.id)).toEqual(["valid-a", "valid-b"]) - }) - - it("列表全部无效时 selectedTemplate 为空(交空状态引导),不选中失效模板", async () => { - mockGetTemplates.mockResolvedValueOnce([ - tpl("empty", { segments: [] }), - tpl("inactive", { is_active: false }), - ]) - const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() }) - await waitFor(() => expect(mockGetTemplates).toHaveBeenCalled()) - // 给 effect 一个 tick - await waitFor(() => expect(result.current.selectedTemplate).toBe("")) - expect(result.current.userTemplates).toHaveLength(0) - }) - - it("用户手动选择优先:自动逻辑不会覆盖手动选择", async () => { - mockGetTemplates.mockResolvedValueOnce([tpl("a"), tpl("b")]) - const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() }) - await waitFor(() => expect(result.current.selectedTemplate).toBe("a")) - - act(() => result.current.setSelectedTemplate("b")) - expect(result.current.selectedTemplate).toBe("b") - - // 重新渲染 / refetch 后仍保持用户的手动选择 - await waitFor(() => expect(result.current.selectedTemplate).toBe("b")) - }) - - it("handleInvalidTemplate:当前模板失效时自动切到第一个有效模板并 toast", async () => { - mockGetTemplates.mockResolvedValueOnce([tpl("bad", { segments: [] }), tpl("good")]) - const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() }) - // 自动选中有效模板 good(bad 无片段不会被自动选中) - await waitFor(() => expect(result.current.selectedTemplate).toBe("good")) - messageMock.warning.mockClear() - - // 模拟运行时用户停留在一个已失效的模板 id(外部/草稿态),触发回退 - act(() => result.current.setSelectedTemplate("stale-id")) - expect(result.current.selectedTemplate).toBe("stale-id") - - act(() => { - const handled = result.current.handleInvalidTemplate() - expect(handled).toBe(true) - }) - await waitFor(() => expect(result.current.selectedTemplate).toBe("good")) - expect(messageMock.warning).toHaveBeenCalledWith("原模板已失效,已自动切换") - }) - - it("handleInvalidTemplate:无有效模板时清空选择并提示去创建", async () => { - mockGetTemplates.mockResolvedValueOnce([tpl("bad", { segments: [] })]) - const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() }) - await waitFor(() => expect(result.current.userTemplates).toHaveLength(0)) - - act(() => result.current.setSelectedTemplate("stale-id")) - act(() => { - result.current.handleInvalidTemplate() - }) - await waitFor(() => expect(result.current.selectedTemplate).toBe("")) - expect(messageMock.warning).toHaveBeenCalledWith(expect.stringContaining("没有可用模板")) - }) - - it("失效模板 ID 不写入任何持久化存储", async () => { - mockGetTemplates.mockResolvedValueOnce([tpl("good")]) - const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() }) - await waitFor(() => expect(result.current.selectedTemplate).toBe("good")) - - act(() => result.current.setSelectedTemplate("stale-invalid-id")) - act(() => result.current.handleInvalidTemplate()) - - const ls = JSON.stringify(localStorage) - const ss = JSON.stringify(sessionStorage) - expect(ls).not.toContain("stale-invalid-id") - expect(ss).not.toContain("stale-invalid-id") - // URL 也不含 - expect(window.location.href).not.toContain("stale-invalid-id") - }) -})