d7b36135d6
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 25s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m32s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m33s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m37s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m52s
AI Code Review / AI Code Review (pull_request) Successful in 5m10s
PR #666 将 API 路径迁移到 /templates/{templateId}/editor 架构,但只改了 import 路径, 没有适配函数签名变化(参数从 planId 改为 templateId),也没有替换已废弃的 createEditPlan(后端无 /templates/drafts 接口)。 修复内容: - GeneratePage: createEditPlan → getEditPlan 自动创建草稿 + updateEditPlan 更新内容 - GeneratePage: 所有 plan.id 调用改为 selectedTemplate(templateId) - E2E: 等待 POST /editor/generate 替代已废弃的创建接口 - 单元测试: 更新 mock 返回值适配新 API 签名 根因:POST /template-editors → 404 → POST /templates/drafts → 405
288 lines
10 KiB
TypeScript
Executable File
288 lines
10 KiB
TypeScript
Executable File
import React from "react"
|
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
|
|
import { render, act } from "@testing-library/react"
|
|
import { MemoryRouter } from "react-router-dom"
|
|
|
|
// Hoisted mock icons factory
|
|
const hoistedIcons = vi.hoisted(() => {
|
|
const iconNames = [
|
|
"AudioOutlined",
|
|
"ThunderboltOutlined",
|
|
"CheckCircleFilled",
|
|
"CheckCircleOutlined",
|
|
"CloseCircleOutlined",
|
|
"LoadingOutlined",
|
|
"PlayCircleOutlined",
|
|
"PauseCircleOutlined",
|
|
"DownloadOutlined",
|
|
"ShareAltOutlined",
|
|
"SaveOutlined",
|
|
"PlusOutlined",
|
|
"MinusOutlined",
|
|
"CloseOutlined",
|
|
"SearchOutlined",
|
|
"EditOutlined",
|
|
"DeleteOutlined",
|
|
"UploadOutlined",
|
|
"FolderOutlined",
|
|
"FolderAddOutlined",
|
|
"MoreOutlined",
|
|
"ExperimentOutlined",
|
|
"ExclamationCircleOutlined",
|
|
"InboxOutlined",
|
|
"VideoCameraOutlined",
|
|
"PictureOutlined",
|
|
"SoundOutlined",
|
|
"UserOutlined",
|
|
"ManOutlined",
|
|
"WomanOutlined",
|
|
"TagsOutlined",
|
|
"MutedOutlined",
|
|
"RobotOutlined",
|
|
"UnorderedListOutlined",
|
|
"AppstoreOutlined",
|
|
"UndoOutlined",
|
|
"RedoOutlined",
|
|
"SettingOutlined",
|
|
"HistoryOutlined",
|
|
"FilterOutlined",
|
|
"FontColorsOutlined",
|
|
"BgColorsOutlined",
|
|
"MusicOutlined",
|
|
"ScissorOutlined",
|
|
"BulbOutlined",
|
|
"FundOutlined",
|
|
"LayoutOutlined",
|
|
"ColumnHeightOutlined",
|
|
"SwapOutlined",
|
|
"LeftOutlined",
|
|
"RightOutlined",
|
|
"UpOutlined",
|
|
"DownOutlined",
|
|
"CopyOutlined",
|
|
]
|
|
const icons: Record<string, React.FC> = {}
|
|
iconNames.forEach((name) => {
|
|
icons[name] = () => React.createElement("span", null, name.charAt(0))
|
|
})
|
|
return icons
|
|
})
|
|
|
|
// === React Query mock ===
|
|
vi.mock("@tanstack/react-query", () => ({
|
|
useQuery: vi.fn(() => ({
|
|
data: undefined,
|
|
isLoading: false,
|
|
isError: false,
|
|
refetch: vi.fn(),
|
|
})),
|
|
useMutation: vi.fn(() => ({
|
|
mutate: vi.fn(),
|
|
mutateAsync: vi.fn(),
|
|
isLoading: false,
|
|
})),
|
|
useQueryClient: vi.fn(() => ({
|
|
invalidateQueries: vi.fn(),
|
|
setQueryData: vi.fn(),
|
|
getQueryData: vi.fn(),
|
|
})),
|
|
useInfiniteQuery: vi.fn(() => ({
|
|
data: { pages: [] },
|
|
isLoading: false,
|
|
fetchNextPage: vi.fn(),
|
|
hasNextPage: false,
|
|
})),
|
|
}))
|
|
|
|
// === Ant Design Icons mock ===
|
|
vi.mock("@ant-design/icons", () => hoistedIcons)
|
|
|
|
// === Ant Design mock ===
|
|
vi.mock("antd", () => {
|
|
const Typography = {
|
|
Text: ({ children }: any) => React.createElement("span", null, children),
|
|
Title: ({ children }: any) => React.createElement("h1", null, children),
|
|
Paragraph: ({ children }: any) => React.createElement("p", null, children),
|
|
}
|
|
return {
|
|
Typography,
|
|
message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
|
|
Modal: ({ open, children, title }: any) =>
|
|
open ? React.createElement("div", { role: "dialog", "data-title": title }, children) : null,
|
|
Progress: () => React.createElement("div"),
|
|
Popover: ({ children }: any) => React.createElement("span", null, children),
|
|
Popconfirm: ({ children }: any) => React.createElement("span", null, children),
|
|
Tooltip: ({ children }: any) => React.createElement("span", null, children),
|
|
Tabs: () => React.createElement("div"),
|
|
Drawer: ({ open, children }: any) =>
|
|
open ? React.createElement("div", { role: "dialog" }, children) : null,
|
|
Upload: ({ children }: any) => React.createElement("div", null, children),
|
|
Slider: () => React.createElement("div"),
|
|
Switch: () => React.createElement("input", { type: "checkbox" }),
|
|
Segmented: () => React.createElement("div"),
|
|
Spin: () => React.createElement("div", null, "Loading"),
|
|
Empty: () => React.createElement("div", null, "Empty"),
|
|
Divider: () => React.createElement("hr"),
|
|
Space: ({ children }: any) => React.createElement("div", null, children),
|
|
Dropdown: ({ children }: any) => React.createElement("span", null, children),
|
|
Menu: () => React.createElement("div"),
|
|
Badge: ({ children }: any) => React.createElement("span", null, children),
|
|
Radio: ({ children }: any) => React.createElement("span", null, children),
|
|
RadioGroup: ({ children }: any) => React.createElement("div", null, children),
|
|
Checkbox: ({ children }: any) => React.createElement("span", null, children),
|
|
InputNumber: () => React.createElement("input", { type: "number" }),
|
|
Form: ({ children }: any) => React.createElement("form", null, children),
|
|
FormItem: ({ children }: any) => React.createElement("div", null, children),
|
|
Result: ({ status, title }: any) =>
|
|
React.createElement("div", { "data-status": status }, title),
|
|
List: () => React.createElement("div"),
|
|
Table: () => React.createElement("div"),
|
|
Pagination: () => React.createElement("div"),
|
|
Card: ({ children }: any) => React.createElement("div", null, children),
|
|
Avatar: ({ children }: any) => React.createElement("span", null, children),
|
|
Collapse: ({ children }: any) => React.createElement("div", null, children),
|
|
CollapsePanel: ({ children }: any) => React.createElement("div", null, children),
|
|
Tag: ({ children }: any) => React.createElement("span", null, children),
|
|
Button: ({ children, onClick }: any) => React.createElement("button", { onClick }, children),
|
|
Input: ({ placeholder }: any) => React.createElement("input", { placeholder }),
|
|
Select: ({ children }: any) => React.createElement("select", null, children),
|
|
Option: ({ children }: any) => React.createElement("option", null, children),
|
|
ConfigProvider: ({ children }: any) => React.createElement(React.Fragment, null, children),
|
|
TextArea: ({ placeholder }: any) => React.createElement("textarea", { placeholder }),
|
|
Steps: () => React.createElement("div"),
|
|
Step: () => React.createElement("div"),
|
|
}
|
|
})
|
|
|
|
// === UI Components mock ===
|
|
vi.mock("@/components/ui", () => ({
|
|
Button: ({ children, onClick }: any) => React.createElement("button", { onClick }, children),
|
|
Input: ({ placeholder }: any) => React.createElement("input", { placeholder }),
|
|
Modal: ({ open, children }: any) =>
|
|
open ? React.createElement("div", { role: "dialog" }, children) : null,
|
|
Drawer: ({ open, children }: any) =>
|
|
open ? React.createElement("div", { role: "dialog" }, children) : null,
|
|
Empty: () => React.createElement("div", null, "Empty"),
|
|
Card: ({ children }: any) => React.createElement("div", null, children),
|
|
Tag: ({ children }: any) => React.createElement("span", null, children),
|
|
Tooltip: ({ children }: any) => React.createElement("span", null, children),
|
|
Select: ({ children }: any) => React.createElement("select", null, children),
|
|
Progress: () => React.createElement("div"),
|
|
Upload: ({ children }: any) => React.createElement("div", null, children),
|
|
Slider: () => React.createElement("div"),
|
|
Switch: () => React.createElement("input", { type: "checkbox" }),
|
|
}))
|
|
|
|
// === API mocks ===
|
|
vi.mock("@/api/assets", () => ({
|
|
getAssetLibraries: vi.fn().mockResolvedValue([]),
|
|
createAssetLibrary: vi.fn().mockResolvedValue({}),
|
|
getAssets: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
|
getAssetsByKind: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
|
createAsset: vi.fn().mockResolvedValue({}),
|
|
updateAsset: vi.fn().mockResolvedValue({}),
|
|
deleteAsset: vi.fn().mockResolvedValue({}),
|
|
uploadAssetDirect: vi.fn().mockResolvedValue({}),
|
|
batchDeleteAssets: vi.fn().mockResolvedValue({}),
|
|
AssetType: { VIDEO: "video", IMAGE: "image", AUDIO: "audio", VOICE: "voice" },
|
|
}))
|
|
|
|
vi.mock("@/api/tags", () => ({
|
|
getTags: vi.fn().mockResolvedValue({ items: [] }),
|
|
createTag: vi.fn().mockResolvedValue({}),
|
|
tagAsset: vi.fn().mockResolvedValue({}),
|
|
untagAsset: vi.fn().mockResolvedValue({}),
|
|
}))
|
|
|
|
vi.mock("@/api/tts", () => ({
|
|
synthesizeSpeech: vi.fn().mockResolvedValue({ job_id: "test-job" }),
|
|
getTTSJobStatus: vi.fn().mockResolvedValue({ status: "completed", audio_url: "" }),
|
|
saveTtsToLibrary: vi.fn().mockResolvedValue({}),
|
|
}))
|
|
|
|
vi.mock("@/api/voices", () => ({
|
|
fetchPresetVoices: vi.fn().mockResolvedValue({ items: [] }),
|
|
}))
|
|
|
|
vi.mock("@/api/editingPlanner", () => ({
|
|
getEditingTemplates: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
|
MODE_LABELS: { pip: "画中画" },
|
|
}))
|
|
|
|
vi.mock("@/api/titles", () => ({
|
|
getTitles: vi.fn().mockResolvedValue({ items: [] }),
|
|
}))
|
|
|
|
vi.mock("@/api/templateEditor", () => ({
|
|
generateEditPlan: vi.fn().mockResolvedValue({ plan_id: "test-plan", generation_task_id: "test-task", plan_status: "processing", clip_count: 5 }),
|
|
updateEditPlan: vi.fn().mockResolvedValue({ plan_id: "test-plan", template_id: "test-template" }),
|
|
getEditPlan: vi.fn().mockResolvedValue({ plan_id: "test-plan", template_id: "test-template", name: "", config: {}, status: "draft" }),
|
|
getGenerationStatus: vi.fn().mockResolvedValue({ plan_status: "completed", generation_task_id: "test-task", clips: [] }),
|
|
getGenerationTaskResults: vi.fn().mockResolvedValue({ items: [] }),
|
|
}))
|
|
|
|
vi.mock("@/api/voiceClone", () => ({
|
|
formatDuration: vi.fn((s: number) => `${s}s`),
|
|
}))
|
|
|
|
vi.mock("@/api/client", () => ({
|
|
default: {
|
|
interceptors: { request: { handlers: [] }, response: { handlers: [] } },
|
|
get: vi.fn().mockResolvedValue({ data: {} }),
|
|
post: vi.fn().mockResolvedValue({ data: {} }),
|
|
put: vi.fn().mockResolvedValue({ data: {} }),
|
|
delete: vi.fn().mockResolvedValue({ data: {} }),
|
|
},
|
|
}))
|
|
|
|
// === Hooks mock ===
|
|
vi.mock("@/hooks/useCloneProgress", () => ({
|
|
useCloneProgress: vi.fn(() => ({
|
|
progress: 0,
|
|
status: "idle",
|
|
start: vi.fn(),
|
|
reset: vi.fn(),
|
|
})),
|
|
}))
|
|
|
|
// === Components mock ===
|
|
vi.mock("@/components/voice/CloneModal", () => ({
|
|
default: () => React.createElement("div", { "data-testid": "CloneModal" }),
|
|
}))
|
|
|
|
// === PageHead mock ===
|
|
vi.mock("@/components/layout/PageHead", () => ({
|
|
default: ({ title }: { title: string }) =>
|
|
React.createElement("div", { "data-testid": "page-head" }, title),
|
|
}))
|
|
|
|
// CSS mock
|
|
vi.mock("@/pages/generate/generate.css", () => ({}))
|
|
|
|
import GeneratePage from "@/pages/generate/GeneratePage"
|
|
|
|
describe("GeneratePage", () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it("renders without crashing", () => {
|
|
const { container } = render(
|
|
React.createElement(MemoryRouter, null, React.createElement(GeneratePage)),
|
|
)
|
|
expect(container).toBeTruthy()
|
|
})
|
|
|
|
it("advances timers without errors", () => {
|
|
render(React.createElement(MemoryRouter, null, React.createElement(GeneratePage)))
|
|
act(() => {
|
|
vi.advanceTimersByTime(30000)
|
|
})
|
|
expect(true).toBe(true)
|
|
})
|
|
})
|