feat: 收口组件样式并调整移动端侧栏
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mediaAssetUrl } from '@/lib/assets'
|
||||
import KeyframeDialog from '@/features/production/components/KeyframeDialog.vue'
|
||||
import { productionApi } from '@/features/production/api'
|
||||
import {
|
||||
isActiveVideo,
|
||||
issueLabel,
|
||||
primaryKeyframe,
|
||||
primaryVideo,
|
||||
productionStatusLabel,
|
||||
validOptionalSize
|
||||
} from '@/features/production/model'
|
||||
import { keyframeFixture, videoFixture } from '@/features/production/testing/fixtures'
|
||||
|
||||
let wrapper: VueWrapper | undefined
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
wrapper = undefined
|
||||
document.body.innerHTML = ''
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** 从 Naive 弹窗中查找精确按钮。 */
|
||||
function button(label: string): HTMLButtonElement {
|
||||
const item = [...document.querySelectorAll('button')].find(element => element.textContent?.trim() === label)
|
||||
if (!item) throw new Error(`缺少按钮 ${label}`)
|
||||
return item
|
||||
}
|
||||
|
||||
describe('镜头生产数据契约', () => {
|
||||
it('可选尺寸必须成对留空或填写正整数', () => {
|
||||
expect(validOptionalSize('', '')).toBe(true)
|
||||
expect(validOptionalSize(1920, 1080)).toBe(true)
|
||||
expect(validOptionalSize(1920, '')).toBe(false)
|
||||
expect(validOptionalSize('', 1080)).toBe(false)
|
||||
expect(validOptionalSize(0, 1080)).toBe(false)
|
||||
expect(validOptionalSize(10.5, 1080)).toBe(false)
|
||||
})
|
||||
|
||||
it('主资产只接受已完成且具有地址的记录,活动视频覆盖三种状态', () => {
|
||||
expect(primaryKeyframe([keyframeFixture()])?.id).toBe('keyframe-1')
|
||||
expect(primaryKeyframe([keyframeFixture({ status: 'failed' })])).toBeUndefined()
|
||||
expect(primaryVideo([videoFixture()])?.id).toBe('video-1')
|
||||
expect(primaryVideo([videoFixture({ videoUrl: null })])).toBeUndefined()
|
||||
for (const status of ['pending', 'queued', 'running'] as const)
|
||||
expect(isActiveVideo(videoFixture({ status }))).toBe(true)
|
||||
expect(isActiveVideo(videoFixture())).toBe(false)
|
||||
})
|
||||
|
||||
it('就绪问题和异步任务状态提供中文标签', () => {
|
||||
expect(issueLabel('missing_keyframe')).toBe('缺少主首帧')
|
||||
expect(issueLabel('invalid_generation_spec')).toBe('生成规格不完整')
|
||||
expect(issueLabel('missing_identity_anchor')).toBe('缺少演员母版')
|
||||
expect(issueLabel('identity_unlocked')).toBe('演员身份未锁定')
|
||||
expect(productionStatusLabel('in_progress')).toBe('任务进行中')
|
||||
expect(productionStatusLabel('unknown')).toBe('unknown')
|
||||
})
|
||||
|
||||
it('视频地址与图片使用相同的安全协议限制', () => {
|
||||
expect(mediaAssetUrl('/storage/videos/a.mp4')).toContain('/storage/videos/a.mp4')
|
||||
expect(mediaAssetUrl('https://cdn.example.com/a.mp4')).toBe('https://cdn.example.com/a.mp4')
|
||||
expect(mediaAssetUrl('javascript:alert(1)')).toBeNull()
|
||||
expect(mediaAssetUrl('/storage/../admin')).toBeNull()
|
||||
})
|
||||
|
||||
it('项目接口传递 force、Provider 与并发,视频创建不冒充同步完成', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async url => {
|
||||
const path = String(url)
|
||||
if (path.includes('/readiness'))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
total: 1,
|
||||
ready: 1,
|
||||
skipped: 0,
|
||||
inProgress: 0,
|
||||
blocked: 0,
|
||||
missingPrompt: 0,
|
||||
missingKeyframe: 0,
|
||||
missingReference: 0,
|
||||
items: []
|
||||
}
|
||||
})
|
||||
)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
total: 1,
|
||||
targetCount: 1,
|
||||
created: 1,
|
||||
skipped: 0,
|
||||
readiness: {},
|
||||
failed: 0,
|
||||
failures: []
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await productionApi.videoReadiness('project/1', true)
|
||||
await productionApi.generateVideos('project/1', { concurrency: 3, force: true })
|
||||
expect(fetcher.mock.calls[0]?.[0]).toBe('/api/projects/project%2F1/videos/readiness?force=true')
|
||||
expect(fetcher.mock.calls[1]?.[0]).toBe('/api/projects/project%2F1/videos/generate')
|
||||
expect(JSON.parse(String(fetcher.mock.calls[1]?.[1]?.body))).toEqual({
|
||||
concurrency: 3,
|
||||
force: true
|
||||
})
|
||||
})
|
||||
|
||||
it('首个首帧默认设主图,已有主图时默认只新增候选,并要求费用确认', async () => {
|
||||
wrapper = mount(KeyframeDialog, {
|
||||
attachTo: document.body,
|
||||
props: { open: true, shotId: 'shot-1', shotTitle: '开场', keyframes: [], disabled: false }
|
||||
})
|
||||
await flushPromises()
|
||||
expect(button('确认生成首帧').disabled).toBe(true)
|
||||
document.querySelector<HTMLInputElement>('#confirm-keyframe-cost')!.click()
|
||||
await flushPromises()
|
||||
button('确认生成首帧').click()
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('generate')).toEqual([[{ setPrimary: true }]])
|
||||
|
||||
await wrapper.setProps({ open: false })
|
||||
await flushPromises()
|
||||
await wrapper.setProps({ open: true, shotId: 'shot-2', keyframes: [keyframeFixture({ shotId: 'shot-2' })] })
|
||||
await flushPromises()
|
||||
document.querySelector<HTMLInputElement>('#confirm-keyframe-cost')!.click()
|
||||
await flushPromises()
|
||||
button('确认生成首帧').click()
|
||||
expect(wrapper.emitted('generate')?.at(-1)).toEqual([{ setPrimary: false }])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user