("react-router-dom")
return {
...actual,
- useNavigate: () => vi.fn(),
+ useNavigate: () => mockNavigate,
}
})
-// Test wrapper
-const createWrapper = () => {
- const queryClient = new QueryClient({
- defaultOptions: {
- queries: { retry: false },
- mutations: { retry: false },
- },
- })
- return ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
- )
-}
+vi.mock("@/store/authStore", () => ({
+ useAuthStore: (selector: any) =>
+ selector({
+ user: { id: "1", username: "testuser" },
+ token: "mock-token",
+ isAuthenticated: true,
+ setAuth: mockSetAuth,
+ clearAuth: mockClearAuth,
+ }),
+}))
-describe.skip("useAuth", () => {
+vi.mock("@tanstack/react-query", () => ({
+ useMutation: () => ({
+ mutate: vi.fn(),
+ mutateAsync: mockMutateAsync,
+ isLoading: false,
+ isError: false,
+ isSuccess: false,
+ data: null,
+ reset: vi.fn(),
+ }),
+ useQuery: ({ queryKey, queryFn, enabled }: any) => ({
+ data: enabled ? { id: "1", username: "testuser" } : undefined,
+ isLoading: false,
+ isError: false,
+ refetch: vi.fn(),
+ }),
+ useQueryClient: () => ({
+ clear: mockQueryClear,
+ invalidateQueries: vi.fn(),
+ setQueryData: vi.fn(),
+ }),
+ QueryClient: class {},
+}))
+
+vi.mock("@/api/auth", () => ({
+ login: vi.fn(),
+ register: vi.fn(),
+ logout: vi.fn(),
+ getCurrentUser: vi.fn().mockResolvedValue({ id: "1", username: "testuser" }),
+}))
+
+import { useLogin, useRegister, useLogout, useCurrentUser } from "@/hooks/useAuth"
+import * as authApi from "@/api/auth"
+
+const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+)
+
+describe("useAuth hooks", () => {
beforeEach(() => {
vi.clearAllMocks()
+ mockMutateAsync.mockReset()
+ // 清空 localStorage
localStorage.clear()
})
- it("should login successfully", async () => {
- const mockResponse = {
- access_token: "mock-token",
- refresh_token: "refresh-token",
- token_type: "bearer",
- user_id: "1",
- email: "test@example.com",
- username: "testuser",
- display_name: "Test User",
- }
-
- vi.mocked(authApi.login).mockResolvedValue(mockResponse)
- vi.mocked(authApi.getCurrentUser).mockResolvedValue({
- id: "1",
- email: "test@example.com",
- username: "testuser",
- display_name: "Test User",
+ describe("useLogin", () => {
+ it("应该返回 mutation 对象", () => {
+ const { result } = renderHook(() => useLogin(), { wrapper })
+ expect(result.current).toHaveProperty("mutateAsync")
+ expect(typeof result.current.mutateAsync).toBe("function")
})
- const { result } = renderHook(() => useLogin(), {
- wrapper: createWrapper(),
+ it("登录成功时保存 token 并调用 setAuth", async () => {
+ mockMutateAsync.mockResolvedValue({
+ access_token: "access-123",
+ refresh_token: "refresh-456",
+ })
+
+ const { result } = renderHook(() => useLogin(), { wrapper })
+
+ await act(async () => {
+ await result.current.mutateAsync({ username: "test", password: "123" })
+ })
+
+ expect(localStorage.getItem("access_token")).toBe("access-123")
+ expect(localStorage.getItem("refresh_token")).toBe("refresh-456")
+ expect(mockSetAuth).toHaveBeenCalled()
+ expect(mockNavigate).toHaveBeenCalledWith("/")
})
- await waitFor(() => {
- expect(result.current).toBeDefined()
- })
+ it("没有 refresh_token 时从 localStorage 移除", async () => {
+ mockMutateAsync.mockResolvedValue({
+ access_token: "access-123",
+ })
- expect(authApi.login).toBeDefined()
+ const { result } = renderHook(() => useLogin(), { wrapper })
+
+ await act(async () => {
+ await result.current.mutateAsync({ username: "test", password: "123" })
+ })
+
+ expect(localStorage.getItem("access_token")).toBe("access-123")
+ expect(localStorage.getItem("refresh_token")).toBeNull()
+ })
})
- it("should register successfully", async () => {
- const mockResponse = {
- user_id: "1",
- email: "test@example.com",
- username: "testuser",
- display_name: "Test User",
- message: "注册成功",
- }
-
- vi.mocked(authApi.register).mockResolvedValue(mockResponse)
-
- const { result } = renderHook(() => useRegister(), {
- wrapper: createWrapper(),
+ describe("useRegister", () => {
+ it("应该返回 mutation 对象", () => {
+ const { result } = renderHook(() => useRegister(), { wrapper })
+ expect(result.current).toHaveProperty("mutateAsync")
})
- await waitFor(() => {
- expect(result.current).toBeDefined()
- })
+ it("注册成功后跳转到登录页", async () => {
+ mockMutateAsync.mockResolvedValue({ success: true })
- expect(authApi.register).toBeDefined()
+ const { result } = renderHook(() => useRegister(), { wrapper })
+
+ await act(async () => {
+ await result.current.mutateAsync({ username: "test", password: "123", email: "a@b.com" })
+ })
+
+ expect(mockNavigate).toHaveBeenCalledWith(
+ "/login",
+ expect.objectContaining({ state: expect.any(Object) }),
+ )
+ })
})
- it("should logout successfully", async () => {
- localStorage.setItem("access_token", "mock-token")
-
- vi.mocked(authApi.logout).mockResolvedValue(undefined)
-
- const { result } = renderHook(() => useLogout(), {
- wrapper: createWrapper(),
+ describe("useLogout", () => {
+ it("应该返回 mutation 对象", () => {
+ const { result } = renderHook(() => useLogout(), { wrapper })
+ expect(result.current).toHaveProperty("mutateAsync")
})
- await waitFor(() => {
- expect(result.current).toBeDefined()
+ it("登出成功时清除认证状态并跳转", async () => {
+ mockMutateAsync.mockResolvedValue({ success: true })
+
+ const { result } = renderHook(() => useLogout(), { wrapper })
+
+ await act(async () => {
+ await result.current.mutateAsync()
+ })
+
+ expect(mockClearAuth).toHaveBeenCalled()
+ expect(mockQueryClear).toHaveBeenCalled()
+ expect(mockNavigate).toHaveBeenCalledWith("/")
})
- expect(authApi.logout).toBeDefined()
+ it("登出失败时仍然清除本地状态", async () => {
+ mockMutateAsync.mockRejectedValue(new Error("logout failed"))
+
+ const { result } = renderHook(() => useLogout(), { wrapper })
+
+ await act(async () => {
+ // 即使失败也不抛异常
+ try {
+ await result.current.mutateAsync()
+ } catch {
+ // expected
+ }
+ })
+
+ expect(mockClearAuth).toHaveBeenCalled()
+ expect(mockQueryClear).toHaveBeenCalled()
+ expect(mockNavigate).toHaveBeenCalledWith("/")
+ })
+ })
+
+ describe("useCurrentUser", () => {
+ it("应该返回 useQuery 结果", () => {
+ const { result } = renderHook(() => useCurrentUser(), { wrapper })
+ expect(result.current).toHaveProperty("data")
+ expect(result.current).toHaveProperty("isLoading")
+ })
})
})
diff --git a/apps/web/src/test/pages/DuplicationDetail.test.tsx b/apps/web/src/test/pages/DuplicationDetail.test.tsx
new file mode 100644
index 000000000..ac2403612
--- /dev/null
+++ b/apps/web/src/test/pages/DuplicationDetail.test.tsx
@@ -0,0 +1,91 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render, screen } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: { items: [], total: 0 },
+ isLoading: false,
+ isError: false,
+ refetch: vi.fn(),
+ }),
+ useMutation: () => ({
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ isLoading: false,
+ }),
+ useQueryClient: () => ({
+ invalidateQueries: vi.fn(),
+ setQueryData: vi.fn(),
+ }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick, disabled }: any) => (
+
+ ),
+ Card: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ Input: ({ placeholder, value, onChange }: any) => (
+
+ ),
+ Tooltip: ({ title, children }: any) => {children},
+ Select: ({ children }: any) => ,
+}))
+
+// mock antd
+vi.mock("antd", () => ({
+ Table: ({ columns, dataSource }: any) => (
+
+ {columns?.map((c: any) => {c.title})}
+
+ ),
+ Tabs: ({ items }: any) => (
+
+ {items?.map((t: any) => {t.label})}
+
+ ),
+ Pagination: ({ total }: any) => {total}
,
+ message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
+ Popconfirm: ({ children }: any) => {children},
+ Descriptions: ({ children }: any) => {children}
,
+ Empty: () => Empty
,
+ Spin: ({ spinning }: any) => (spinning ? Loading...
: <>>),
+ Avatar: ({ src }: any) =>
,
+ Badge: ({ children }: any) => {children},
+ Drawer: ({ open, children }: any) => (open ? {children}
: null),
+ Upload: ({ children }: any) => {children}
,
+ Progress: ({ percent }: any) => {percent}%
,
+ Switch: ({ checked }: any) => ,
+ Radio: ({ children }: any) => {children},
+ RadioGroup: ({ children }: any) => {children}
,
+}))
+
+vi.mock("@/api/duplication", () => ({
+ getDuplicationDetail: vi.fn().mockResolvedValue({ id: "1", segments: [], status: "completed" }),
+ retryDuplication: vi.fn().mockResolvedValue({ success: true }),
+}))
+vi.mock("react-router-dom", async () => {
+ const actual = await vi.importActual("react-router-dom")
+ return { ...actual, useParams: () => ({ id: "1" }) }
+})
+vi.mock("@/pages/duplication/duplication.css", () => ({}))
+
+import DuplicationDetail from "@/pages/duplication/DuplicationDetail"
+
+describe("DuplicationDetail Page", () => {
+ it("should render without crashing", () => {
+ render(
+
+
+ ,
+ )
+ expect(screen.getByTestId("page-head")).toBeInTheDocument()
+ })
+})
diff --git a/apps/web/src/test/pages/DuplicationResults.test.tsx b/apps/web/src/test/pages/DuplicationResults.test.tsx
new file mode 100644
index 000000000..b384430a3
--- /dev/null
+++ b/apps/web/src/test/pages/DuplicationResults.test.tsx
@@ -0,0 +1,52 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: { items: [], total: 0 },
+ isLoading: false,
+ isError: false,
+ refetch: vi.fn(),
+ }),
+ useMutation: () => ({
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ isLoading: false,
+ }),
+ useQueryClient: () => ({
+ invalidateQueries: vi.fn(),
+ }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick }: any) => ,
+ Tag: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+}))
+
+vi.mock("@/api/duplication", () => ({
+ getDuplicationRecords: vi.fn().mockResolvedValue({ items: [], total: 0 }),
+ deleteDuplicationRecord: vi.fn().mockResolvedValue({ success: true }),
+ retryDuplication: vi.fn().mockResolvedValue({ success: true }),
+}))
+
+vi.mock("@/pages/duplication/duplication.css", () => ({}))
+
+import DuplicationResults from "@/pages/duplication/DuplicationResults"
+
+describe("DuplicationResults Page", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.firstChild).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/DuplicationUpload.test.tsx b/apps/web/src/test/pages/DuplicationUpload.test.tsx
new file mode 100644
index 000000000..5bd53767e
--- /dev/null
+++ b/apps/web/src/test/pages/DuplicationUpload.test.tsx
@@ -0,0 +1,46 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render, screen } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@/api/duplication", () => ({
+ uploadForDuplication: vi.fn().mockResolvedValue({ id: "123", message: "success" }),
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useMutation: () => ({
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ isLoading: false,
+ isError: false,
+ isSuccess: false,
+ data: null,
+ }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick, disabled }: any) => (
+
+ ),
+ Card: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+}))
+
+vi.mock("@/pages/duplication/duplication.css", () => ({}))
+
+import DuplicationUpload from "@/pages/duplication/DuplicationUpload"
+
+describe("DuplicationUpload Page", () => {
+ it("should render without crashing", () => {
+ render(
+
+
+ ,
+ )
+ expect(screen.getByTestId("page-head")).toBeInTheDocument()
+ })
+})
diff --git a/apps/web/src/test/pages/MyVoices.test.tsx b/apps/web/src/test/pages/MyVoices.test.tsx
new file mode 100644
index 000000000..783ee8ef2
--- /dev/null
+++ b/apps/web/src/test/pages/MyVoices.test.tsx
@@ -0,0 +1,88 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render, screen } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: { items: [], total: 0 },
+ isLoading: false,
+ isError: false,
+ refetch: vi.fn(),
+ }),
+ useMutation: () => ({
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ isLoading: false,
+ }),
+ useQueryClient: () => ({
+ invalidateQueries: vi.fn(),
+ setQueryData: vi.fn(),
+ }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick, disabled }: any) => (
+
+ ),
+ Card: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ Input: ({ placeholder, value, onChange }: any) => (
+
+ ),
+ Tooltip: ({ title, children }: any) => {children},
+ Select: ({ children }: any) => ,
+}))
+
+// mock antd
+vi.mock("antd", () => ({
+ Table: ({ columns, dataSource }: any) => (
+
+ {columns?.map((c: any) => {c.title})}
+
+ ),
+ Tabs: ({ items }: any) => (
+
+ {items?.map((t: any) => {t.label})}
+
+ ),
+ Pagination: ({ total }: any) => {total}
,
+ message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
+ Popconfirm: ({ children }: any) => {children},
+ Descriptions: ({ children }: any) => {children}
,
+ Empty: () => Empty
,
+ Spin: ({ spinning }: any) => (spinning ? Loading...
: <>>),
+ Avatar: ({ src }: any) =>
,
+ Badge: ({ children }: any) => {children},
+ Drawer: ({ open, children }: any) => (open ? {children}
: null),
+ Upload: ({ children }: any) => {children}
,
+ Progress: ({ percent }: any) => {percent}%
,
+ Switch: ({ checked }: any) => ,
+ Radio: ({ children }: any) => {children},
+ RadioGroup: ({ children }: any) => {children}
,
+}))
+
+vi.mock("@/api/voiceClone", () => ({
+ getVoiceCloneList: vi.fn().mockResolvedValue({ items: [], total: 0 }),
+ deleteVoiceClone: vi.fn().mockResolvedValue({ success: true }),
+ createVoiceClone: vi.fn().mockResolvedValue({ success: true }),
+}))
+vi.mock("@/pages/my-voices/MyVoices.css", () => ({}))
+
+import MyVoices from "@/pages/my-voices/MyVoices"
+
+describe("MyVoices Page", () => {
+ it("should render without crashing", () => {
+ render(
+
+
+ ,
+ )
+ expect(screen.getByTestId("page-head")).toBeInTheDocument()
+ })
+})
diff --git a/apps/web/src/test/pages/ProductDetail.test.tsx b/apps/web/src/test/pages/ProductDetail.test.tsx
new file mode 100644
index 000000000..010ca0aa4
--- /dev/null
+++ b/apps/web/src/test/pages/ProductDetail.test.tsx
@@ -0,0 +1,68 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: { id: "1", title: "test", description: "", videos: [] },
+ isLoading: false,
+ isError: false,
+ refetch: vi.fn(),
+ }),
+ useMutation: () => ({
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ isLoading: false,
+ }),
+ useQueryClient: () => ({
+ invalidateQueries: vi.fn(),
+ }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick }: any) => ,
+ Tag: ({ children }: any) => {children},
+ Card: ({ children }: any) => {children}
,
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ Tooltip: ({ children }: any) => {children},
+}))
+
+vi.mock("antd", () => ({
+ message: { success: vi.fn(), error: vi.fn() },
+ Popconfirm: ({ children }: any) => {children},
+ Descriptions: ({ children }: any) => {children}
,
+ Space: ({ children }: any) => {children}
,
+ Empty: () => Empty
,
+ Divider: () =>
,
+}))
+
+vi.mock("@/api/products", () => ({
+ getProductDetail: vi.fn().mockResolvedValue({ id: "1", title: "test" }),
+ deleteProduct: vi.fn().mockResolvedValue({ success: true }),
+ getProductDownloadUrl: vi.fn().mockResolvedValue({ download_url: "" }),
+}))
+
+vi.mock("react-router-dom", async () => {
+ const actual = await vi.importActual("react-router-dom")
+ return { ...actual, useParams: () => ({ id: "1" }) }
+})
+
+vi.mock("@/pages/products/ProductDetail.css", () => ({}))
+
+import ProductDetail from "@/pages/products/ProductDetail"
+
+describe("ProductDetail Page", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.firstChild).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/ProductLibrary.test.tsx b/apps/web/src/test/pages/ProductLibrary.test.tsx
new file mode 100644
index 000000000..26341848e
--- /dev/null
+++ b/apps/web/src/test/pages/ProductLibrary.test.tsx
@@ -0,0 +1,102 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({ data: [], isLoading: false, isError: false, refetch: vi.fn() }),
+ useMutation: () => ({ mutate: vi.fn(), mutateAsync: vi.fn(), isLoading: false }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn(), setQueryData: vi.fn() }),
+ useInfiniteQuery: () => ({ data: { pages: [] }, isLoading: false, fetchNextPage: vi.fn(), hasNextPage: false }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick }: any) => ,
+ Input: ({ placeholder }: any) => ,
+ Select: ({ options }: any) => (
+
+ ),
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ Empty: () => Empty
,
+ Card: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+ Upload: ({ children }: any) => {children}
,
+ Progress: () => ,
+}))
+
+vi.mock("antd", () => ({
+ Table: () => ,
+ Pagination: () => ,
+ Tabs: () => ,
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
+ Popconfirm: ({ children }: any) => {children},
+ Form: ({ children }: any) => ,
+ Input: ({ placeholder }: any) => ,
+ InputNumber: () => ,
+ Select: () => ,
+ Empty: () => Empty
,
+ Space: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Badge: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+ Upload: ({ children }: any) => {children}
,
+ Progress: () => ,
+ Switch: () => ,
+ Radio: ({ children }: any) => {children},
+ RadioGroup: ({ children }: any) => {children}
,
+ Checkbox: ({ children }: any) => ,
+ DatePicker: () => ,
+ Col: ({ children }: any) => {children}
,
+ Row: ({ children }: any) => {children}
,
+ Card: ({ children }: any) => {children}
,
+ List: () => ,
+ Grid: { useBreakpoint: () => ({ xs: true, sm: true, md: true, lg: true }) },
+ Descriptions: ({ children }: any) => {children}
,
+ Divider: () =>
,
+ Avatar: () => ,
+ Dropdown: ({ children }: any) => {children},
+ Menu: ({ children }: any) => ,
+ Drawer: ({ open, children }: any) => (open ? {children}
: null),
+ Typography: {
+ Title: ({ children }: any) => {children}
,
+ Text: ({ children }: any) => {children},
+ Paragraph: ({ children }: any) => {children}
,
+ },
+ Spin: ({ spinning }: any) => (spinning ? Loading...
: <>>),
+ Slider: () => ,
+ Rate: () => ,
+ Collapse: ({ children }: any) => {children}
,
+ Steps: ({ children }: any) => {children}
,
+ Button: ({ children }: any) => ,
+}))
+
+vi.mock("@ant-design/icons", () => ({
+ CheckOutlined: () => , CloseOutlined: () => , CloudUploadOutlined: () => , DeleteOutlined: () => , DownloadOutlined: () => , EyeOutlined: () => , PauseCircleOutlined: () => , PlayCircleOutlined: () => , SearchOutlined: () => , ShareAltOutlined: () => , VideoCameraOutlined: () => ,
+}))
+
+vi.mock("@/store/authStore", () => ({ useAuthStore: (sel: any) => sel({ user: { id: "1" }, isAuthenticated: true }) }))
+
+vi.mock("@/api/products", () => ({
+ getProducts: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), getProduct: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), deleteProduct: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), getProductDownloadUrl: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), updateReviewStatus: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), batchDownload: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), getBatchDownloadStatus: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }),
+}))
+
+vi.mock("@/pages/products/ProductLibrary.css", () => ({}))
+
+import ProductLibrary from "@/pages/products/ProductLibrary"
+
+describe("ProductLibrary Page", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.firstChild).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/TaskCenter.test.tsx b/apps/web/src/test/pages/TaskCenter.test.tsx
new file mode 100644
index 000000000..8b0ced8d3
--- /dev/null
+++ b/apps/web/src/test/pages/TaskCenter.test.tsx
@@ -0,0 +1,102 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({ data: [], isLoading: false, isError: false, refetch: vi.fn() }),
+ useMutation: () => ({ mutate: vi.fn(), mutateAsync: vi.fn(), isLoading: false }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn(), setQueryData: vi.fn() }),
+ useInfiniteQuery: () => ({ data: { pages: [] }, isLoading: false, fetchNextPage: vi.fn(), hasNextPage: false }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick }: any) => ,
+ Input: ({ placeholder }: any) => ,
+ Select: ({ options }: any) => (
+
+ ),
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ Empty: () => Empty
,
+ Card: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+ Upload: ({ children }: any) => {children}
,
+ Progress: () => ,
+}))
+
+vi.mock("antd", () => ({
+ Table: () => ,
+ Pagination: () => ,
+ Tabs: () => ,
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
+ Popconfirm: ({ children }: any) => {children},
+ Form: ({ children }: any) => ,
+ Input: ({ placeholder }: any) => ,
+ InputNumber: () => ,
+ Select: () => ,
+ Empty: () => Empty
,
+ Space: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Badge: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+ Upload: ({ children }: any) => {children}
,
+ Progress: () => ,
+ Switch: () => ,
+ Radio: ({ children }: any) => {children},
+ RadioGroup: ({ children }: any) => {children}
,
+ Checkbox: ({ children }: any) => ,
+ DatePicker: () => ,
+ Col: ({ children }: any) => {children}
,
+ Row: ({ children }: any) => {children}
,
+ Card: ({ children }: any) => {children}
,
+ List: () => ,
+ Grid: { useBreakpoint: () => ({ xs: true, sm: true, md: true, lg: true }) },
+ Descriptions: ({ children }: any) => {children}
,
+ Divider: () =>
,
+ Avatar: () => ,
+ Dropdown: ({ children }: any) => {children},
+ Menu: ({ children }: any) => ,
+ Drawer: ({ open, children }: any) => (open ? {children}
: null),
+ Typography: {
+ Title: ({ children }: any) => {children}
,
+ Text: ({ children }: any) => {children},
+ Paragraph: ({ children }: any) => {children}
,
+ },
+ Spin: ({ spinning }: any) => (spinning ? Loading...
: <>>),
+ Slider: () => ,
+ Rate: () => ,
+ Collapse: ({ children }: any) => {children}
,
+ Steps: ({ children }: any) => {children}
,
+ Button: ({ children }: any) => ,
+}))
+
+vi.mock("@ant-design/icons", () => ({
+ CheckCircleOutlined: () => , ClockCircleOutlined: () => , CloseCircleOutlined: () => , ExclamationCircleOutlined: () => , InfoCircleOutlined: () => , MinusCircleOutlined: () => , RedoOutlined: () => , SyncOutlined: () => ,
+}))
+
+vi.mock("@/store/authStore", () => ({ useAuthStore: (sel: any) => sel({ user: { id: "1" }, isAuthenticated: true }) }))
+
+vi.mock("@/api/tasks", () => ({
+ getTasks: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), getUserTasks: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), getTask: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), retryTask: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), cancelTask: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), deleteTask: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }),
+}))
+
+vi.mock("@/pages/tasks/TaskCenter.css", () => ({}))
+
+import TaskCenter from "@/pages/tasks/TaskCenter"
+
+describe("TaskCenter Page", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.firstChild).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/TaskHistory.test.tsx b/apps/web/src/test/pages/TaskHistory.test.tsx
new file mode 100644
index 000000000..42bf2eeda
--- /dev/null
+++ b/apps/web/src/test/pages/TaskHistory.test.tsx
@@ -0,0 +1,63 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: [],
+ isLoading: false,
+ isError: false,
+ refetch: vi.fn(),
+ }),
+ useMutation: () => ({
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ isLoading: false,
+ }),
+ useQueryClient: () => ({
+ invalidateQueries: vi.fn(),
+ }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick }: any) => ,
+}))
+
+vi.mock("antd", () => ({
+ Table: () => ,
+ Pagination: () => ,
+ Select: () => ,
+ Tabs: () => ,
+ Empty: () => Empty
,
+ Space: ({ children }: any) => {children}
,
+ message: { success: vi.fn(), error: vi.fn() },
+ Popconfirm: ({ children }: any) => {children},
+ Tag: ({ children }: any) => {children},
+ Badge: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+}))
+
+vi.mock("@/api/tasks", () => ({
+ getUserTasks: vi.fn().mockResolvedValue([]),
+ retryTask: vi.fn().mockResolvedValue({ success: true }),
+}))
+
+vi.mock("@/pages/history/history.css", () => ({}))
+
+import TaskHistory from "@/pages/history/TaskHistory"
+
+describe("TaskHistory Page", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.firstChild).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/TemplateLibrary.test.tsx b/apps/web/src/test/pages/TemplateLibrary.test.tsx
new file mode 100644
index 000000000..7f516485e
--- /dev/null
+++ b/apps/web/src/test/pages/TemplateLibrary.test.tsx
@@ -0,0 +1,102 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({ data: [], isLoading: false, isError: false, refetch: vi.fn() }),
+ useMutation: () => ({ mutate: vi.fn(), mutateAsync: vi.fn(), isLoading: false }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn(), setQueryData: vi.fn() }),
+ useInfiniteQuery: () => ({ data: { pages: [] }, isLoading: false, fetchNextPage: vi.fn(), hasNextPage: false }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick }: any) => ,
+ Input: ({ placeholder }: any) => ,
+ Select: ({ options }: any) => (
+
+ ),
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ Empty: () => Empty
,
+ Card: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+ Upload: ({ children }: any) => {children}
,
+ Progress: () => ,
+}))
+
+vi.mock("antd", () => ({
+ Table: () => ,
+ Pagination: () => ,
+ Tabs: () => ,
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
+ Popconfirm: ({ children }: any) => {children},
+ Form: ({ children }: any) => ,
+ Input: ({ placeholder }: any) => ,
+ InputNumber: () => ,
+ Select: () => ,
+ Empty: () => Empty
,
+ Space: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Badge: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+ Upload: ({ children }: any) => {children}
,
+ Progress: () => ,
+ Switch: () => ,
+ Radio: ({ children }: any) => {children},
+ RadioGroup: ({ children }: any) => {children}
,
+ Checkbox: ({ children }: any) => ,
+ DatePicker: () => ,
+ Col: ({ children }: any) => {children}
,
+ Row: ({ children }: any) => {children}
,
+ Card: ({ children }: any) => {children}
,
+ List: () => ,
+ Grid: { useBreakpoint: () => ({ xs: true, sm: true, md: true, lg: true }) },
+ Descriptions: ({ children }: any) => {children}
,
+ Divider: () =>
,
+ Avatar: () => ,
+ Dropdown: ({ children }: any) => {children},
+ Menu: ({ children }: any) => ,
+ Drawer: ({ open, children }: any) => (open ? {children}
: null),
+ Typography: {
+ Title: ({ children }: any) => {children}
,
+ Text: ({ children }: any) => {children},
+ Paragraph: ({ children }: any) => {children}
,
+ },
+ Spin: ({ spinning }: any) => (spinning ? Loading...
: <>>),
+ Slider: () => ,
+ Rate: () => ,
+ Collapse: ({ children }: any) => {children}
,
+ Steps: ({ children }: any) => {children}
,
+ Button: ({ children }: any) => ,
+}))
+
+vi.mock("@ant-design/icons", () => ({
+ CopyOutlined: () => , ExclamationCircleOutlined: () => , InboxOutlined: () => , LoadingOutlined: () => , SearchOutlined: () => , ThunderboltOutlined: () => ,
+}))
+
+vi.mock("@/store/authStore", () => ({ useAuthStore: (sel: any) => sel({ user: { id: "1", vip_level: 0 }, isAuthenticated: true }) }))
+
+vi.mock("@/api/templates", () => ({
+ getTemplates: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), getTemplatesList: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), getTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), toggleFavoriteTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), copyTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), generateFromTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }),
+}))
+
+vi.mock("@/pages/templates/TemplateLibrary.css", () => ({}))
+
+import TemplateLibrary from "@/pages/templates/TemplateLibrary"
+
+describe("TemplateLibrary Page", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.firstChild).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/TitleLibrary.test.tsx b/apps/web/src/test/pages/TitleLibrary.test.tsx
new file mode 100644
index 000000000..8515d6bbd
--- /dev/null
+++ b/apps/web/src/test/pages/TitleLibrary.test.tsx
@@ -0,0 +1,74 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({ data: [], isLoading: false, isError: false, refetch: vi.fn() }),
+ useMutation: () => ({ mutate: vi.fn(), mutateAsync: vi.fn(), isLoading: false }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn() }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick }: any) => ,
+ Input: ({ placeholder }: any) => ,
+ Select: ({ options }: any) => (
+
+ ),
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ Empty: () => Empty
,
+ Card: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+}))
+
+vi.mock("antd", () => ({
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ message: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
+ Popconfirm: ({ children }: any) => {children},
+ Form: ({ children }: any) => ,
+ Input: ({ placeholder }: any) => ,
+ InputNumber: () => ,
+}))
+
+vi.mock("@ant-design/icons", () => ({
+ PlusOutlined: () => ,
+ EditOutlined: () => ,
+ DeleteOutlined: () => ,
+ SearchOutlined: () => ,
+ FileTextOutlined: () => ,
+ CopyOutlined: () => ,
+ RobotOutlined: () => ,
+ CheckOutlined: () => ,
+ StarOutlined: () => ,
+}))
+
+vi.mock("@/api/titles", () => ({
+ getTitles: vi.fn().mockResolvedValue([]),
+ createTitle: vi.fn().mockResolvedValue({ success: true }),
+ updateTitle: vi.fn().mockResolvedValue({ success: true }),
+ deleteTitle: vi.fn().mockResolvedValue({ success: true }),
+}))
+
+vi.mock("@/store/authStore", () => ({
+ useAuthStore: (sel: any) => sel({ user: { id: "1", vip_level: 0 }, isAuthenticated: true }),
+}))
+
+vi.mock("@/pages/titles/titles.css", () => ({}))
+
+import TitleLibrary from "@/pages/titles/TitleLibrary"
+
+describe("TitleLibrary Page", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.firstChild).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/UpgradeSubscription.test.tsx b/apps/web/src/test/pages/UpgradeSubscription.test.tsx
new file mode 100644
index 000000000..604917f6a
--- /dev/null
+++ b/apps/web/src/test/pages/UpgradeSubscription.test.tsx
@@ -0,0 +1,53 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: { plan: "free", status: "active", auto_renew: true },
+ isLoading: false,
+ isError: false,
+ refetch: vi.fn(),
+ }),
+ useMutation: () => ({
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ isLoading: false,
+ }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick }: any) => ,
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+}))
+
+vi.mock("antd", () => ({
+ message: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
+}))
+
+vi.mock("@/api/subscription", () => ({
+ getCurrentSubscription: vi.fn().mockResolvedValue({ plan: "free", status: "active" }),
+ changePlan: vi.fn().mockResolvedValue({ success: true }),
+ toggleAutoRenew: vi.fn().mockResolvedValue({ success: true }),
+ cancelSubscription: vi.fn().mockResolvedValue({ success: true }),
+}))
+
+vi.mock("@/pages/subscription/UpgradeSubscription.css", () => ({}))
+
+import UpgradeSubscription from "@/pages/subscription/UpgradeSubscription"
+
+describe("UpgradeSubscription Page", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.querySelector(".xx-upgrade-page")).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/VoiceClone.test.tsx b/apps/web/src/test/pages/VoiceClone.test.tsx
new file mode 100644
index 000000000..866039563
--- /dev/null
+++ b/apps/web/src/test/pages/VoiceClone.test.tsx
@@ -0,0 +1,89 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render, screen } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: { items: [], total: 0 },
+ isLoading: false,
+ isError: false,
+ refetch: vi.fn(),
+ }),
+ useMutation: () => ({
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ isLoading: false,
+ }),
+ useQueryClient: () => ({
+ invalidateQueries: vi.fn(),
+ setQueryData: vi.fn(),
+ }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick, disabled }: any) => (
+
+ ),
+ Card: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ Input: ({ placeholder, value, onChange }: any) => (
+
+ ),
+ Tooltip: ({ title, children }: any) => {children},
+ Select: ({ children }: any) => ,
+}))
+
+// mock antd
+vi.mock("antd", () => ({
+ Table: ({ columns, dataSource }: any) => (
+
+ {columns?.map((c: any) => {c.title})}
+
+ ),
+ Tabs: ({ items }: any) => (
+
+ {items?.map((t: any) => {t.label})}
+
+ ),
+ Pagination: ({ total }: any) => {total}
,
+ message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
+ Popconfirm: ({ children }: any) => {children},
+ Descriptions: ({ children }: any) => {children}
,
+ Empty: () => Empty
,
+ Spin: ({ spinning }: any) => (spinning ? Loading...
: <>>),
+ Avatar: ({ src }: any) =>
,
+ Badge: ({ children }: any) => {children},
+ Drawer: ({ open, children }: any) => (open ? {children}
: null),
+ Upload: ({ children }: any) => {children}
,
+ Progress: ({ percent }: any) => {percent}%
,
+ Switch: ({ checked }: any) => ,
+ Radio: ({ children }: any) => {children},
+ RadioGroup: ({ children }: any) => {children}
,
+}))
+
+vi.mock("@/api/voiceClone", () => ({
+ getVoiceCloneList: vi.fn().mockResolvedValue({ items: [], total: 0 }),
+ createVoiceClone: vi.fn().mockResolvedValue({ success: true, id: "1" }),
+ deleteVoiceClone: vi.fn().mockResolvedValue({ success: true }),
+ uploadVoiceMaterial: vi.fn().mockResolvedValue({ success: true }),
+}))
+vi.mock("@/pages/voice-clone/VoiceClone.css", () => ({}))
+
+import VoiceClone from "@/pages/voice-clone/VoiceClone"
+
+describe("VoiceClone Page", () => {
+ it("should render without crashing", () => {
+ render(
+
+
+ ,
+ )
+ expect(screen.getByTestId("page-head")).toBeInTheDocument()
+ })
+})
diff --git a/apps/web/src/test/pages/VoiceLibrary.test.tsx b/apps/web/src/test/pages/VoiceLibrary.test.tsx
new file mode 100644
index 000000000..5ecff254b
--- /dev/null
+++ b/apps/web/src/test/pages/VoiceLibrary.test.tsx
@@ -0,0 +1,103 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+
+vi.mock("@/components/layout/PageHead", () => ({
+ default: ({ title }: { title: string }) => {title}
,
+}))
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({ data: [], isLoading: false, isError: false, refetch: vi.fn() }),
+ useMutation: () => ({ mutate: vi.fn(), mutateAsync: vi.fn(), isLoading: false }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn(), setQueryData: vi.fn() }),
+ useInfiniteQuery: () => ({ data: { pages: [] }, isLoading: false, fetchNextPage: vi.fn(), hasNextPage: false }),
+}))
+
+vi.mock("@/components/ui", () => ({
+ Button: ({ children, onClick }: any) => ,
+ Input: ({ placeholder }: any) => ,
+ Select: ({ options }: any) => (
+
+ ),
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ Empty: () => Empty
,
+ Card: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+ Upload: ({ children }: any) => {children}
,
+ Progress: () => ,
+}))
+
+vi.mock("antd", () => ({
+ Table: () => ,
+ Pagination: () => ,
+ Tabs: () => ,
+ Modal: ({ open, children }: any) => (open ? {children}
: null),
+ message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
+ Popconfirm: ({ children }: any) => {children},
+ Form: ({ children }: any) => ,
+ Input: ({ placeholder }: any) => ,
+ InputNumber: () => ,
+ Select: () => ,
+ Empty: () => Empty
,
+ Space: ({ children }: any) => {children}
,
+ Tag: ({ children }: any) => {children},
+ Badge: ({ children }: any) => {children},
+ Tooltip: ({ children }: any) => {children},
+ Upload: ({ children }: any) => {children}
,
+ Progress: () => ,
+ Switch: () => ,
+ Radio: ({ children }: any) => {children},
+ RadioGroup: ({ children }: any) => {children}
,
+ Checkbox: ({ children }: any) => ,
+ DatePicker: () => ,
+ Col: ({ children }: any) => {children}
,
+ Row: ({ children }: any) => {children}
,
+ Card: ({ children }: any) => {children}
,
+ List: () => ,
+ Grid: { useBreakpoint: () => ({ xs: true, sm: true, md: true, lg: true }) },
+ Descriptions: ({ children }: any) => {children}
,
+ Divider: () =>
,
+ Avatar: () => ,
+ Dropdown: ({ children }: any) => {children},
+ Menu: ({ children }: any) => ,
+ Drawer: ({ open, children }: any) => (open ? {children}
: null),
+ Typography: {
+ Title: ({ children }: any) => {children}
,
+ Text: ({ children }: any) => {children},
+ Paragraph: ({ children }: any) => {children}
,
+ },
+ Spin: ({ spinning }: any) => (spinning ? Loading...
: <>>),
+ Slider: () => ,
+ Rate: () => ,
+ Collapse: ({ children }: any) => {children}
,
+ Steps: ({ children }: any) => {children}
,
+ Button: ({ children }: any) => ,
+}))
+
+vi.mock("@ant-design/icons", () => ({
+ AudioOutlined: () => , CloseCircleOutlined: () => , DeleteOutlined: () => , HeartOutlined: () => , PauseCircleOutlined: () => , PlayCircleOutlined: () => , PlusOutlined: () => , ReloadOutlined: () => , RobotOutlined: () => , SearchOutlined: () => , SoundOutlined: () => , UploadOutlined: () => , UserOutlined: () => ,
+}))
+
+vi.mock("@/store/authStore", () => ({ useAuthStore: (sel: any) => sel({ user: { id: "1" }, isAuthenticated: true }) }))
+vi.mock("@/api/voiceClone", () => ({ createVoiceClone: vi.fn().mockResolvedValue({ success: true }), deleteVoiceClone: vi.fn().mockResolvedValue({ success: true }), retryVoiceClone: vi.fn().mockResolvedValue({ success: true }) }))
+
+vi.mock("@/api/voices", () => ({
+ fetchVoices: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), fetchPresetVoices: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), getVoices: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), createVoice: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), updateVoice: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), deleteVoice: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), generateAIVoice: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }),
+}))
+
+vi.mock("@/pages/voices/VoiceLibrary.css", () => ({}))
+
+import VoiceLibrary from "@/pages/voices/VoiceLibrary"
+
+describe("VoiceLibrary Page", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.firstChild).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/editing-planner/hooks/useUndoRedo.test.ts b/apps/web/src/test/pages/editing-planner/hooks/useUndoRedo.test.ts
new file mode 100644
index 000000000..9bc0496e2
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/hooks/useUndoRedo.test.ts
@@ -0,0 +1,157 @@
+/**
+ * useUndoRedo hook 测试
+ */
+import { describe, it, expect, beforeEach } from "vitest"
+import { renderHook, act } from "@testing-library/react"
+import { useUndoRedo } from "@/pages/editing-planner/hooks/useUndoRedo"
+
+describe("useUndoRedo", () => {
+ it("应该使用初始状态初始化", () => {
+ const { result } = renderHook(() => useUndoRedo(0))
+ expect(result.current.state).toBe(0)
+ expect(result.current.canUndo).toBe(false)
+ expect(result.current.canRedo).toBe(false)
+ })
+
+ it("set 应该更新状态并启用撤销", () => {
+ const { result } = renderHook(() => useUndoRedo(0))
+
+ act(() => {
+ result.current.set(1)
+ })
+
+ expect(result.current.state).toBe(1)
+ expect(result.current.canUndo).toBe(true)
+ expect(result.current.canRedo).toBe(false)
+ })
+
+ it("set 支持函数式更新", () => {
+ const { result } = renderHook(() => useUndoRedo(0))
+
+ act(() => {
+ result.current.set((prev) => prev + 1)
+ })
+
+ expect(result.current.state).toBe(1)
+ })
+
+ it("undo 应该回退到上一个状态", () => {
+ const { result } = renderHook(() => useUndoRedo(0))
+
+ act(() => {
+ result.current.set(1)
+ })
+ act(() => {
+ result.current.set(2)
+ })
+
+ expect(result.current.state).toBe(2)
+
+ act(() => {
+ result.current.undo()
+ })
+
+ expect(result.current.state).toBe(1)
+ expect(result.current.canUndo).toBe(true)
+ expect(result.current.canRedo).toBe(true)
+ })
+
+ it("redo 应该重做已撤销的操作", () => {
+ const { result } = renderHook(() => useUndoRedo(0))
+
+ act(() => result.current.set(1))
+ act(() => result.current.set(2))
+ act(() => result.current.undo())
+
+ expect(result.current.state).toBe(1)
+
+ act(() => {
+ result.current.redo()
+ })
+
+ expect(result.current.state).toBe(2)
+ expect(result.current.canUndo).toBe(true)
+ })
+
+ it("没有历史时 undo 不改变状态", () => {
+ const { result } = renderHook(() => useUndoRedo(0))
+
+ act(() => {
+ result.current.undo()
+ })
+
+ expect(result.current.state).toBe(0)
+ expect(result.current.canUndo).toBe(false)
+ })
+
+ it("没有未来时 redo 不改变状态", () => {
+ const { result } = renderHook(() => useUndoRedo(0))
+
+ act(() => result.current.set(1))
+
+ act(() => {
+ result.current.redo()
+ })
+
+ expect(result.current.state).toBe(1)
+ expect(result.current.canRedo).toBe(false)
+ })
+
+ it("新操作应该清空 redo 历史", () => {
+ const { result } = renderHook(() => useUndoRedo(0))
+
+ act(() => result.current.set(1))
+ act(() => result.current.set(2))
+ act(() => result.current.undo()) // state = 1, canRedo = true
+ expect(result.current.canRedo).toBe(true)
+
+ act(() => result.current.set(3)) // 新操作清空 redo
+
+ expect(result.current.state).toBe(3)
+ expect(result.current.canRedo).toBe(false)
+ })
+
+ it("reset 应该重置状态和历史", () => {
+ const { result } = renderHook(() => useUndoRedo(0))
+
+ act(() => result.current.set(1))
+ act(() => result.current.set(2))
+
+ act(() => {
+ result.current.reset(100)
+ })
+
+ expect(result.current.state).toBe(100)
+ expect(result.current.canUndo).toBe(false)
+ expect(result.current.canRedo).toBe(false)
+ })
+
+ it("多次 undo 后可以一直 undo 到初始状态", () => {
+ const { result } = renderHook(() => useUndoRedo(0))
+
+ act(() => result.current.set(1))
+ act(() => result.current.set(2))
+ act(() => result.current.set(3))
+
+ act(() => result.current.undo())
+ expect(result.current.state).toBe(2)
+
+ act(() => result.current.undo())
+ expect(result.current.state).toBe(1)
+
+ act(() => result.current.undo())
+ expect(result.current.state).toBe(0)
+ expect(result.current.canUndo).toBe(false)
+ })
+
+ it("支持对象类型状态", () => {
+ const { result } = renderHook(() => useUndoRedo({ count: 0, name: "test" }))
+
+ act(() => result.current.set({ count: 1, name: "test" }))
+ expect(result.current.state.count).toBe(1)
+
+ act(() => result.current.undo())
+ expect(result.current.state.count).toBe(0)
+ expect(result.current.state.name).toBe("test")
+ })
+})