,
+}))
+
+describe("MyTemplates", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container).toBeTruthy()
+ })
+})
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..19a4c3752
--- /dev/null
+++ b/apps/web/src/test/pages/MyVoices.test.tsx
@@ -0,0 +1,94 @@
+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/PlanClipsManager.test.tsx b/apps/web/src/test/pages/PlanClipsManager.test.tsx
new file mode 100644
index 000000000..e01e23ef4
--- /dev/null
+++ b/apps/web/src/test/pages/PlanClipsManager.test.tsx
@@ -0,0 +1,60 @@
+import React from "react"
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+import PlanClipsManager from "@/pages/edit-plans/PlanClipsManager"
+
+vi.mock("react-router-dom", async () => {
+ const actual = await vi.importActual("react-router-dom")
+ return {
+ ...actual,
+ useNavigate: () => vi.fn(),
+ useParams: () => ({ templateId: "test-123" }),
+ }
+})
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: { clips: [], name: "Test Plan" },
+ 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("@ant-design/icons", () => ({
+ ArrowLeftOutlined: () => ArrowLeftOutlined,
+ PlusOutlined: () => PlusOutlined,
+ DeleteOutlined: () => DeleteOutlined,
+ EditOutlined: () => EditOutlined,
+ UploadOutlined: () => UploadOutlined,
+ OrderedListOutlined: () => OrderedListOutlined,
+ SaveOutlined: () => SaveOutlined,
+}))
+
+vi.mock("@/api/editPlans", () => ({
+ getPlanClips: vi.fn().mockResolvedValue({ clips: [], name: "" }),
+ updatePlanClipsOrder: vi.fn(),
+ deletePlanClip: vi.fn(),
+ createPlanClip: vi.fn(),
+}))
+
+describe("PlanClipsManager", () => {
+ it("should render without crashing", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container).toBeTruthy()
+ })
+})
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..f2c8629e2
--- /dev/null
+++ b/apps/web/src/test/pages/ProductLibrary.test.tsx
@@ -0,0 +1,134 @@
+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..0a2c15a74
--- /dev/null
+++ b/apps/web/src/test/pages/TaskCenter.test.tsx
@@ -0,0 +1,130 @@
+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..9ea5d984c
--- /dev/null
+++ b/apps/web/src/test/pages/TemplateLibrary.test.tsx
@@ -0,0 +1,128 @@
+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..29319a0b3
--- /dev/null
+++ b/apps/web/src/test/pages/TitleLibrary.test.tsx
@@ -0,0 +1,78 @@
+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..ac43bce7c
--- /dev/null
+++ b/apps/web/src/test/pages/VoiceClone.test.tsx
@@ -0,0 +1,95 @@
+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..efc09a302
--- /dev/null
+++ b/apps/web/src/test/pages/VoiceLibrary.test.tsx
@@ -0,0 +1,141 @@
+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/components/BgmSelector.test.tsx b/apps/web/src/test/pages/editing-planner/components/BgmSelector.test.tsx
new file mode 100644
index 000000000..93e07a066
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/components/BgmSelector.test.tsx
@@ -0,0 +1,30 @@
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import BgmSelector from "@/pages/editing-planner/components/BgmSelector"
+
+vi.mock("@/api/bgm", () => ({
+ getBgmPresets: vi.fn().mockResolvedValue([]),
+}))
+
+const defaultProps = {
+ open: true,
+ onClose: vi.fn(),
+ config: {
+ enabled: true,
+ music_id: "",
+ volume: 50,
+ } as any,
+ onChange: vi.fn(),
+}
+
+describe("BgmSelector", () => {
+ it("should render without crashing", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+
+ it("should render when closed", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/editing-planner/components/ClipPropertiesPanel.test.tsx b/apps/web/src/test/pages/editing-planner/components/ClipPropertiesPanel.test.tsx
new file mode 100644
index 000000000..c6bdfd50e
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/components/ClipPropertiesPanel.test.tsx
@@ -0,0 +1,63 @@
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+import ClipPropertiesPanel from "@/pages/editing-planner/components/ClipPropertiesPanel"
+
+vi.mock("react-router-dom", async () => {
+ const actual = await vi.importActual("react-router-dom")
+ return {
+ ...actual,
+ useNavigate: () => vi.fn(),
+ }
+})
+
+const mockClip = {
+ id: "clip-1",
+ type: "voice",
+ duration: 10,
+ startOffset: 0,
+} as any
+
+const defaultProps = {
+ selectedClip: mockClip,
+ titleSettings: { enabled: true, text: "Test Title" } as any,
+ subtitleSettings: { enabled: true } as any,
+ bgmSettings: { enabled: false } as any,
+ clipsCount: 3,
+ totalDuration: 60,
+ currentMode: "template" as const,
+ onTitleSettingsChange: vi.fn(),
+ onSubtitleSettingsChange: vi.fn(),
+ onBgmSettingsChange: vi.fn(),
+ onClipUpdate: vi.fn(),
+ onOpenBgmDrawer: vi.fn(),
+ onOpenSubtitleDrawer: vi.fn(),
+ voiceMaterials: [],
+ voiceMaterialsLoading: false,
+ onRefreshVoiceMaterials: vi.fn(),
+ onClipVoiceSelect: vi.fn(),
+ onOpenTransitionDrawer: vi.fn(),
+ onOpenSpeedDrawer: vi.fn(),
+ onOpenTtsDrawer: vi.fn(),
+ onOpenWatermarkDrawer: vi.fn(),
+}
+
+describe("ClipPropertiesPanel", () => {
+ it("should render with selected clip", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container).toBeTruthy()
+ })
+
+ it("should render without selected clip", () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/editing-planner/components/CoverSelector.test.tsx b/apps/web/src/test/pages/editing-planner/components/CoverSelector.test.tsx
new file mode 100644
index 000000000..0a2305619
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/components/CoverSelector.test.tsx
@@ -0,0 +1,24 @@
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import CoverSelector from "@/pages/editing-planner/components/CoverSelector"
+import { DEFAULT_COVER_CONFIG } from "@/pages/editing-planner/types"
+
+const defaultProps = {
+ open: true,
+ onClose: vi.fn(),
+ config: DEFAULT_COVER_CONFIG,
+ onChange: vi.fn(),
+ totalDuration: 60,
+}
+
+describe("CoverSelector", () => {
+ it("should render without crashing", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+
+ it("should render when closed", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/editing-planner/components/GenerationProgressModal.test.tsx b/apps/web/src/test/pages/editing-planner/components/GenerationProgressModal.test.tsx
new file mode 100644
index 000000000..65b088729
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/components/GenerationProgressModal.test.tsx
@@ -0,0 +1,102 @@
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import GenerationProgressModal from "@/pages/editing-planner/components/GenerationProgressModal"
+
+const baseProps = {
+ open: true,
+ voiceoverDuration: null,
+ estimatedDuration: 60,
+ onDurationChange: vi.fn(),
+ onGenerate: vi.fn(),
+ task: null,
+ submitting: false,
+ onCancel: vi.fn(),
+ onRetry: vi.fn(),
+ onClose: vi.fn(),
+}
+
+describe("GenerationProgressModal", () => {
+ it("should render setup phase", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+
+ it("should render progress phase without task", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+
+ it("should render progress phase with task data", () => {
+ const { container } = render(
+ ,
+ )
+ expect(container).toBeTruthy()
+ })
+
+ it("should render completed phase", () => {
+ const { container } = render(
+ ,
+ )
+ expect(container).toBeTruthy()
+ })
+
+ it("should render failed phase with retry", () => {
+ const { container } = render(
+ ,
+ )
+ expect(container).toBeTruthy()
+ })
+
+ it("should render failed phase without retry", () => {
+ const { container } = render(
+ ,
+ )
+ expect(container).toBeTruthy()
+ })
+
+ it("should not render when closed", () => {
+ const { container } = render(
+ ,
+ )
+ expect(container).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/editing-planner/components/IntroOutroPanel.test.tsx b/apps/web/src/test/pages/editing-planner/components/IntroOutroPanel.test.tsx
new file mode 100644
index 000000000..5aa5d4a7a
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/components/IntroOutroPanel.test.tsx
@@ -0,0 +1,23 @@
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import IntroOutroPanel from "@/pages/editing-planner/components/IntroOutroPanel"
+import { DEFAULT_INTRO_OUTRO } from "@/pages/editing-planner/types"
+
+const defaultProps = {
+ open: true,
+ onClose: vi.fn(),
+ config: DEFAULT_INTRO_OUTRO,
+ onChange: vi.fn(),
+}
+
+describe("IntroOutroPanel", () => {
+ it("should render without crashing", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+
+ it("should render when closed", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/editing-planner/components/PipConfigPanel.test.tsx b/apps/web/src/test/pages/editing-planner/components/PipConfigPanel.test.tsx
new file mode 100644
index 000000000..345b872d3
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/components/PipConfigPanel.test.tsx
@@ -0,0 +1,24 @@
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import PipConfigPanel from "@/pages/editing-planner/components/PipConfigPanel"
+import { DEFAULT_PIP_CONFIG } from "@/pages/editing-planner/types"
+
+const defaultProps = {
+ open: true,
+ onClose: vi.fn(),
+ config: DEFAULT_PIP_CONFIG,
+ onChange: vi.fn(),
+ totalDuration: 60,
+}
+
+describe("PipConfigPanel", () => {
+ it("should render without crashing", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+
+ it("should render when closed", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/editing-planner/components/StickerPanel.test.tsx b/apps/web/src/test/pages/editing-planner/components/StickerPanel.test.tsx
new file mode 100644
index 000000000..c480e4269
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/components/StickerPanel.test.tsx
@@ -0,0 +1,24 @@
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import StickerPanel from "@/pages/editing-planner/components/StickerPanel"
+import { DEFAULT_STICKER_CONFIG } from "@/pages/editing-planner/types"
+
+const defaultProps = {
+ open: true,
+ onClose: vi.fn(),
+ config: DEFAULT_STICKER_CONFIG,
+ onChange: vi.fn(),
+ totalDuration: 60,
+}
+
+describe("StickerPanel", () => {
+ it("should render without crashing", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+
+ it("should render when closed", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/editing-planner/components/TimelinePanel.test.tsx b/apps/web/src/test/pages/editing-planner/components/TimelinePanel.test.tsx
new file mode 100644
index 000000000..2fe180298
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/components/TimelinePanel.test.tsx
@@ -0,0 +1,45 @@
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import TimelinePanel from "@/pages/editing-planner/components/TimelinePanel"
+
+const mockClips = [
+ { id: "clip-1", type: "voice", duration: 10, startOffset: 0 } as any,
+ { id: "clip-2", type: "pip", duration: 5, startOffset: 0 } as any,
+ { id: "clip-3", type: "voice", duration: 15, startOffset: 0 } as any,
+]
+
+const defaultProps = {
+ clips: mockClips,
+ selectedClipId: "clip-1",
+ currentMode: "template",
+ onClipSelect: vi.fn(),
+ onClipReorder: vi.fn(),
+ onClipRemove: vi.fn(),
+ onAddClip: vi.fn(),
+ onClipTrim: vi.fn(),
+ onClipSplit: vi.fn(),
+ onClipResetTrim: vi.fn(),
+ currentTime: 0,
+ pixelsPerSecond: 30,
+ onZoomChange: vi.fn(),
+ onSeek: vi.fn(),
+}
+
+describe("TimelinePanel", () => {
+ it("should render with clips", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+
+ it("should render with empty clips", () => {
+ const { container } = render(
+ ,
+ )
+ expect(container).toBeTruthy()
+ })
+
+ it("should render without selected clip", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/editing-planner/components/TtsPanel.test.tsx b/apps/web/src/test/pages/editing-planner/components/TtsPanel.test.tsx
new file mode 100644
index 000000000..ba2aeaf68
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/components/TtsPanel.test.tsx
@@ -0,0 +1,28 @@
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import TtsPanel from "@/pages/editing-planner/components/TtsPanel"
+import { DEFAULT_TTS_CONFIG } from "@/pages/editing-planner/types"
+
+vi.mock("@/api/tts", () => ({
+ getTtsVoices: vi.fn().mockResolvedValue([]),
+ previewTts: vi.fn().mockResolvedValue({ url: "" }),
+}))
+
+const defaultProps = {
+ open: true,
+ onClose: vi.fn(),
+ config: DEFAULT_TTS_CONFIG,
+ onChange: vi.fn(),
+}
+
+describe("TtsPanel", () => {
+ it("should render without crashing", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+
+ it("should render when closed", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+})
diff --git a/apps/web/src/test/pages/editing-planner/components/WatermarkPanel.test.tsx b/apps/web/src/test/pages/editing-planner/components/WatermarkPanel.test.tsx
new file mode 100644
index 000000000..37fcb4c97
--- /dev/null
+++ b/apps/web/src/test/pages/editing-planner/components/WatermarkPanel.test.tsx
@@ -0,0 +1,23 @@
+import { describe, expect, it, vi } from "vitest"
+import { render } from "@testing-library/react"
+import WatermarkPanel from "@/pages/editing-planner/components/WatermarkPanel"
+import { DEFAULT_WATERMARK } from "@/pages/editing-planner/types"
+
+const defaultProps = {
+ open: true,
+ onClose: vi.fn(),
+ config: DEFAULT_WATERMARK,
+ onChange: vi.fn(),
+}
+
+describe("WatermarkPanel", () => {
+ it("should render without crashing", () => {
+ const { container } = render()
+ expect(container).toBeTruthy()
+ })
+
+ it("should render when closed", () => {
+ const { container } = render()
+ expect(container).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")
+ })
+})