169 lines
7.1 KiB
TypeScript
169 lines
7.1 KiB
TypeScript
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import QualityResult from '@/features/production/components/QualityResult.vue'
|
|
import { expandSections } from '@/testing/naive'
|
|
import ShotProductionAssets from '@/features/production/components/ShotProductionAssets.vue'
|
|
import { primaryKeyframe } from '@/features/production/model'
|
|
import { qualityKey, qualitySession, sessionVideoValidation } from '@/features/production/quality'
|
|
import { keyframeFixture, videoFixture } from '@/features/production/testing/fixtures'
|
|
import { storyboardApi } from '@/features/storyboard/api'
|
|
import { hasRunningWorkflow } from '@/features/workflows/selectors'
|
|
import type { DesignedShot } from '@/features/storyboard/types'
|
|
import type { VideoValidation } from '@/features/production/quality.types'
|
|
import type { Checkpoint } from '@/features/workflows/types'
|
|
|
|
const shot: DesignedShot = {
|
|
shotId: 'shot-1',
|
|
shotNo: 1,
|
|
beatNo: 1,
|
|
title: '测试镜头',
|
|
description: '',
|
|
visualFocus: '',
|
|
direction: null,
|
|
visualState: null
|
|
}
|
|
const validation: VideoValidation = {
|
|
shotId: 'shot-1',
|
|
videoId: 'video-1',
|
|
videoUrl: '/storage/videos/video-1.mp4',
|
|
isPrimary: false,
|
|
passed: true,
|
|
summary: '通过',
|
|
subjects: [],
|
|
subjectCountConsistent: true,
|
|
unauthorizedText: { detected: false, texts: [] },
|
|
issues: [],
|
|
sampleFrames: [],
|
|
allowedTexts: [],
|
|
validatedAt: '2026-09-10T00:00:00Z'
|
|
}
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
/** 构造带完整执行字段的后端 checkpoint。 */
|
|
const record = (workflowName: string, day: number, status: 'running' | 'completed'): Checkpoint => ({
|
|
checkpointId: `${workflowName}-${day}`,
|
|
workflowName,
|
|
createdAt: `2026-09-0${day}T00:00:00Z`,
|
|
state: {
|
|
workflowExecution: {
|
|
status,
|
|
executionId: `${workflowName}-${day}`,
|
|
startedAt: `2026-09-0${day}T00:00:00Z`
|
|
}
|
|
}
|
|
})
|
|
|
|
describe('精简公开资产回归', () => {
|
|
it('无内部字段的视频可播放,未知质量不得直接晋升;真实校验结果按项目隔离', async () => {
|
|
const projectId = 'public-video-contract'
|
|
const video = videoFixture({ isPrimary: false })
|
|
expect(video).not.toHaveProperty('rawJson')
|
|
const fetcher = vi.fn<typeof fetch>().mockImplementation(
|
|
async url =>
|
|
new Response(
|
|
JSON.stringify({
|
|
data: String(url).endsWith('/videos') ? [video] : []
|
|
})
|
|
)
|
|
)
|
|
vi.stubGlobal('fetch', fetcher)
|
|
const wrapper = mount(ShotProductionAssets, { props: { projectId, shot, disabled: false } })
|
|
try {
|
|
await flushPromises()
|
|
expect(wrapper.find('video').exists()).toBe(true)
|
|
expect(wrapper.text()).not.toContain('自动校验中')
|
|
expect(wrapper.findAll('button').some(button => button.text() === '设为主视频')).toBe(false)
|
|
const session = qualitySession(
|
|
qualityKey(projectId, { kind: 'video', shotId: shot.shotId, assetId: video.id, title: '' })
|
|
)
|
|
session.videoValidation = validation
|
|
await flushPromises()
|
|
expect(wrapper.findAll('button').some(button => button.text() === '设为主视频')).toBe(true)
|
|
expect(sessionVideoValidation('other-project', shot.shotId, video.id)).toBeNull()
|
|
session.videoValidation = { ...validation, passed: false }
|
|
await flushPromises()
|
|
expect(wrapper.findAll('button').some(button => button.text() === '设为主视频')).toBe(false)
|
|
expect(fetcher.mock.calls.every(([, options]) => options?.method === 'GET')).toBe(true)
|
|
} finally {
|
|
wrapper.unmount()
|
|
}
|
|
})
|
|
|
|
it('模型输入变体可以预览,但不能设为业务主首帧', async () => {
|
|
const variant = keyframeFixture({ source: 'provider_variant', isPrimary: false })
|
|
expect(primaryKeyframe([{ ...variant, isPrimary: true }])).toBeUndefined()
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn<typeof fetch>().mockImplementation(
|
|
async url =>
|
|
new Response(
|
|
JSON.stringify({
|
|
data: String(url).endsWith('/keyframes') ? [variant] : []
|
|
})
|
|
)
|
|
)
|
|
)
|
|
const wrapper = mount(ShotProductionAssets, {
|
|
props: { projectId: 'public-variant-contract', shot, disabled: false }
|
|
})
|
|
try {
|
|
await flushPromises()
|
|
expect(wrapper.text()).toContain('模型输入变体')
|
|
expect(wrapper.findAll('button').some(button => button.text() === '设为主首帧')).toBe(false)
|
|
} finally {
|
|
wrapper.unmount()
|
|
}
|
|
})
|
|
|
|
it('提示词返回使用 shotId,保留正文、状态和更新时间', async () => {
|
|
const result = {
|
|
shotId: 'shot/db',
|
|
videoPrompt: '镜头正文',
|
|
negativePrompt: null,
|
|
status: 'prompt_ready',
|
|
updatedAt: '2026-09-10T00:00:00Z'
|
|
}
|
|
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({ data: result })))
|
|
vi.stubGlobal('fetch', fetcher)
|
|
expect(await storyboardApi.generatePrompt('shot/db', false)).toEqual(result)
|
|
expect(fetcher.mock.calls[0]?.[0]).toBe('/api/storyboard-shots/shot%2Fdb/video-prompt')
|
|
expect(JSON.parse(String(fetcher.mock.calls[0]?.[1]?.body))).toEqual({ force: false })
|
|
})
|
|
|
|
it('视频专项检查区分通过、失败和未执行,并保留后端摘要', async () => {
|
|
const wrapper = mount(QualityResult, {
|
|
attachTo: document.body,
|
|
props: {
|
|
receipt: {
|
|
kind: 'video',
|
|
promoted: false,
|
|
result: {
|
|
...validation,
|
|
passed: false,
|
|
motionRealism: { enabled: true, passed: false, summary: '运动发生跳变' },
|
|
spatialInteraction: { enabled: true, passed: true, summary: '空间关系一致' },
|
|
physicalStructure: { enabled: false, reason: 'missing_prop_subject' }
|
|
}
|
|
}
|
|
}
|
|
})
|
|
try {
|
|
await expandSections()
|
|
expect(wrapper.text()).toContain('运动真实性:未通过')
|
|
expect(wrapper.text()).toContain('运动发生跳变')
|
|
expect(wrapper.text()).toContain('空间交互:通过')
|
|
expect(wrapper.text()).toContain('物理结构:未执行')
|
|
} finally {
|
|
wrapper.unmount()
|
|
}
|
|
})
|
|
|
|
it('项目不再提供任务数组时,从每条工作流最新 checkpoint 判断运行状态', () => {
|
|
expect(hasRunningWorkflow([])).toBe(false)
|
|
expect(hasRunningWorkflow([record('breakdown', 1, 'running'), record('breakdown', 2, 'completed')])).toBe(false)
|
|
expect(hasRunningWorkflow([record('breakdown', 1, 'running'), record('other', 2, 'completed')])).toBe(true)
|
|
})
|
|
})
|