import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' import VideoPanelCardBody from '@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/video/panel-card/VideoPanelCardBody' import type { VideoPanelRuntime } from '@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/video/panel-card/hooks/useVideoPanelActions' vi.mock('@/components/task/TaskStatusInline', () => ({ default: () => React.createElement('span', null, 'task-status'), })) vi.mock('@/components/ui/config-modals/ModelCapabilityDropdown', () => ({ ModelCapabilityDropdown: () => React.createElement('div', null, 'model-dropdown'), })) vi.mock('@/components/ui/icons', () => ({ AppIcon: ({ name }: { name: string }) => React.createElement('span', null, name), })) function createRuntime(overrides: Partial = {}): VideoPanelRuntime { const translate = (key: string, values?: Record) => { if (key === 'firstLastFrame.asLastFrameFor') { return `作为镜头 ${String(values?.number ?? '')} 的尾帧` } if (key === 'firstLastFrame.asFirstFrameFor') { return `作为镜头 ${String(values?.number ?? '')} 的首帧` } if (key === 'firstLastFrame.generate') return '生成首尾帧视频' if (key === 'firstLastFrame.generated') return '首尾帧视频已生成' if (key === 'promptModal.promptLabel') return '视频提示词' if (key === 'promptModal.placeholder') return '输入首尾帧视频提示词...' if (key === 'panelCard.clickToEditPrompt') return '点击编辑提示词...' if (key === 'panelCard.selectModel') return '选择模型' if (key === 'panelCard.generateVideo') return '生成视频' if (key === 'panelCard.unknownShotType') return '未知镜头' if (key === 'stage.hasSynced') return '已生成' if (key === 'promptModal.duration') return '秒' return key } const runtime = { t: translate, tCommon: (key: string) => key, panel: { storyboardId: 'sb-1', panelIndex: 2, panelId: 'panel-2', imageUrl: 'https://example.com/frame-2.jpg', videoUrl: null, videoGenerationMode: null, lipSyncVideoUrl: null, textPanel: { shot_type: '平视中景', description: '谢俞站在宴席中央', duration: 3, }, }, panelIndex: 2, panelKey: 'sb-1-2', media: { showLipSyncVideo: true, onToggleLipSyncVideo: () => undefined, onPreviewImage: () => undefined, baseVideoUrl: undefined, currentVideoUrl: undefined, }, taskStatus: { isVideoTaskRunning: false, isLipSyncTaskRunning: false, taskRunningVideoLabel: '生成中', lipSyncInlineState: null, }, videoModel: { selectedModel: 'veo-3.1', setSelectedModel: () => undefined, capabilityFields: [], generationOptions: {}, setCapabilityValue: () => undefined, missingCapabilityFields: [], videoModelOptions: [], }, player: { isPlaying: false, }, promptEditor: { isEditing: false, editingPrompt: '', setEditingPrompt: () => undefined, handleStartEdit: () => undefined, handleSave: () => undefined, handleCancelEdit: () => undefined, isSavingPrompt: false, localPrompt: '人物从席间回身,接到下一镜头', }, voiceManager: { hasMatchedAudio: false, hasMatchedVoiceLines: false, audioGenerateError: null, localVoiceLines: [], isVoiceLineTaskRunning: () => false, handlePlayVoiceLine: () => undefined, handleGenerateAudio: async () => undefined, playingVoiceLineId: null, }, lipSync: { handleStartLipSync: () => undefined, executingLipSync: false, }, layout: { isLinked: true, isLastFrame: true, nextPanel: { storyboardId: 'sb-1', panelIndex: 3, imageUrl: 'https://example.com/frame-3.jpg', }, prevPanel: { storyboardId: 'sb-1', panelIndex: 1, imageUrl: 'https://example.com/frame-1.jpg', }, hasNext: true, flModel: 'veo-3.1', flModelOptions: [], flGenerationOptions: {}, flCapabilityFields: [], flMissingCapabilityFields: [], flCustomPrompt: '', defaultFlPrompt: '', videoRatio: '9:16', }, actions: { onGenerateVideo: () => undefined, onUpdatePanelVideoModel: () => undefined, onToggleLink: () => undefined, onFlModelChange: () => undefined, onFlCapabilityChange: () => undefined, onFlCustomPromptChange: () => undefined, onResetFlPrompt: () => undefined, onGenerateFirstLastFrame: () => undefined, }, computed: { showLipSyncSection: false, canLipSync: false, hasVisibleBaseVideo: false, }, } return { ...runtime, ...overrides, } as unknown as VideoPanelRuntime } describe('VideoPanelCardBody', () => { it('renders incoming and outgoing first-last-frame UI for chained panel', () => { const markup = renderToStaticMarkup( React.createElement(VideoPanelCardBody, { runtime: createRuntime(), }), ) expect(markup).toContain('作为镜头 2 的尾帧') expect(markup).toContain('作为镜头 4 的首帧') expect(markup).toContain('视频提示词') expect(markup).toContain('生成首尾帧视频') }) })