516 lines
23 KiB
TypeScript
516 lines
23 KiB
TypeScript
import { defineComponent, reactive, ref } from 'vue'
|
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { NCheckbox, NInputNumber, NRadioGroup } from 'naive-ui'
|
|
import { testProjectContext } from '@/testing/project-context'
|
|
import { productionApi } from '@/features/production/api'
|
|
import { qualityApi } from '@/features/production/quality-api'
|
|
import {
|
|
allowedTextLines,
|
|
qualityKey,
|
|
qualitySession,
|
|
qualityTargets,
|
|
savedVideoValidation,
|
|
videoRepairInfo,
|
|
validQualityInput
|
|
} from '@/features/production/quality'
|
|
import type { KeyframeReadiness } from '@/features/production/types'
|
|
import type { QualityInput, QualityTarget } from '@/features/production/quality.types'
|
|
import { useQuality } from '@/features/production/useQuality'
|
|
import { keyframeFixture, videoFixture } from '@/features/production/testing/fixtures'
|
|
import QualityDialog from '@/features/production/components/QualityDialog.vue'
|
|
import QualityResult from '@/features/production/components/QualityResult.vue'
|
|
import ModelCapabilities from '@/features/production/components/ModelCapabilities.vue'
|
|
|
|
let wrapper: VueWrapper | undefined
|
|
let sequence = 0
|
|
afterEach(() => {
|
|
wrapper?.unmount()
|
|
wrapper = undefined
|
|
document.body.innerHTML = ''
|
|
vi.restoreAllMocks()
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
/** 模拟统一配置后的真实契约,测试绝不调用真实图片或视觉模型。 */
|
|
function server() {
|
|
const projectId = `quality-test-${sequence++}`
|
|
const project = testProjectContext(projectId).project.value!
|
|
const validation = {
|
|
passed: true,
|
|
summary: '身份与造型一致',
|
|
subjects: [],
|
|
subjectCountConsistent: true,
|
|
unauthorizedText: { detected: false, texts: [] },
|
|
issues: []
|
|
}
|
|
const keyframe = keyframeFixture()
|
|
const video = videoFixture()
|
|
const readiness = {
|
|
total: 1,
|
|
ready: 1,
|
|
skipped: 0,
|
|
blocked: 0,
|
|
stalePrimaryKeyframe: 0,
|
|
missingVisualStyle: 0,
|
|
missingIdentity: 0,
|
|
missingIdentityAnchor: 0,
|
|
identityUnlocked: 0,
|
|
invalidGenerationSpec: 0,
|
|
missingReference: 0,
|
|
items: [{ shotId: 'shot-1', shotNo: 1, beatNo: 1, episodeNo: 1, status: 'ready', issues: [] }]
|
|
} as KeyframeReadiness
|
|
const capability = {
|
|
provider: 'qwen-image',
|
|
referenceCount: 3,
|
|
maxReferenceImages: 3,
|
|
valid: true,
|
|
message: '参考图超限'
|
|
}
|
|
const keyframeResult = {
|
|
...validation,
|
|
shotId: 'shot-1',
|
|
keyframeId: 'keyframe-1',
|
|
validationTaskId: 'validation-1',
|
|
isPrimary: true,
|
|
imageUrl: keyframe.imageUrl!
|
|
}
|
|
const attempt = {
|
|
attempt: 0,
|
|
keyframeId: 'keyframe-1',
|
|
validationTaskId: 'validation-1',
|
|
passed: true,
|
|
issues: [],
|
|
validationDurationMs: 10
|
|
}
|
|
const repair = {
|
|
shotId: 'shot-1',
|
|
initialKeyframeId: 'keyframe-1',
|
|
finalKeyframeId: 'keyframe-1',
|
|
passed: true,
|
|
repaired: false,
|
|
primaryChanged: false,
|
|
repairAttempts: 0,
|
|
maxRepairAttempts: 1,
|
|
attempts: [attempt]
|
|
}
|
|
const batch = {
|
|
total: 1,
|
|
ready: 1,
|
|
selected: 1,
|
|
targetCount: 1,
|
|
passed: 1,
|
|
repairFailed: 0,
|
|
failed: 0,
|
|
skipped: 0,
|
|
blocked: 0,
|
|
stalePrimaryKeyframe: 0,
|
|
missingReference: 0,
|
|
results: [{ shotId: 'shot-1', success: true, status: 'passed' as const }]
|
|
}
|
|
const fetcher = vi.fn<typeof fetch>().mockImplementation(async (url, options) => {
|
|
const path = String(url)
|
|
let data: unknown
|
|
if (options?.method === 'POST') {
|
|
if (path.endsWith('/generate-quality')) data = batch
|
|
else if (path.includes('/videos/') && path.endsWith('/repair'))
|
|
data = {
|
|
shotId: 'shot-1',
|
|
sourceVideoId: 'video-1',
|
|
repairAttempt: 1,
|
|
maxRepairAttempts: 2,
|
|
repairInstructions: ['修复人物漂移'],
|
|
allowedTexts: [],
|
|
candidate: videoFixture({
|
|
id: 'video-repair-1',
|
|
status: 'queued',
|
|
isPrimary: false,
|
|
videoUrl: null,
|
|
rawJson: JSON.stringify({ repair: { sourceVideoId: 'video-1', attempt: 1 } })
|
|
})
|
|
}
|
|
else if (path.endsWith('/repair')) data = repair
|
|
else if (path.includes('/videos/'))
|
|
data = {
|
|
...validation,
|
|
shotId: 'shot-1',
|
|
videoId: 'video-1',
|
|
validatedAt: '2026-09-03',
|
|
isPrimary: validation.passed && !!videoRepairInfo(video.rawJson),
|
|
videoUrl: video.videoUrl,
|
|
sampleFrames: [{ label: '中间', timeSeconds: 2 }],
|
|
allowedTexts: []
|
|
}
|
|
else data = keyframeResult
|
|
} else if (path.endsWith(`/projects/${projectId}`)) data = project
|
|
else if (path.includes('/readiness')) data = readiness
|
|
else if (path.endsWith('/keyframes')) data = [keyframe]
|
|
else if (path.endsWith('/videos')) data = [video]
|
|
else if (path.endsWith('/keyframe-provider-capability')) data = capability
|
|
else if (path.endsWith('/image-providers/capabilities'))
|
|
data = [
|
|
{
|
|
provider: 'qwen-image',
|
|
active: true,
|
|
capabilities: { references: { supported: true, maxReferenceImages: 3 } }
|
|
}
|
|
]
|
|
else throw new Error('意外接口:' + path)
|
|
return new Response(JSON.stringify({ data }))
|
|
})
|
|
vi.stubGlobal('fetch', fetcher)
|
|
return {
|
|
projectId,
|
|
project,
|
|
validation,
|
|
keyframe,
|
|
video,
|
|
readiness,
|
|
capability,
|
|
keyframeResult,
|
|
repair,
|
|
batch,
|
|
fetcher,
|
|
posts: () => fetcher.mock.calls.filter(([, options]) => options?.method === 'POST')
|
|
}
|
|
}
|
|
|
|
/** 统一默认小批次,零修复和自定义允许文字均可单独覆写。 */
|
|
function input(patch: Partial<QualityInput> = {}): QualityInput {
|
|
return { concurrency: 1, limit: 1, episodeNo: 1, maxRepairAttempts: 1, force: false, allowedTexts: [], ...patch }
|
|
}
|
|
|
|
/** 通过实际挂载按钮检验费用确认,避免绕过禁用状态。 */
|
|
function validationButton() {
|
|
return [...document.querySelectorAll<HTMLButtonElement>('button')].find(
|
|
item => item.textContent === '开始视觉校验'
|
|
)!
|
|
}
|
|
|
|
function setup(target: QualityTarget = { kind: 'keyframe', shotId: 'shot-1', assetId: 'keyframe-1', title: '镜头一' }) {
|
|
const data = server()
|
|
const props = reactive({ projectId: data.projectId, target: target as QualityTarget | null, disabled: false })
|
|
const visible = ref(true)
|
|
const changed = vi.fn<() => void>()
|
|
let service!: ReturnType<typeof useQuality>
|
|
wrapper = mount(
|
|
defineComponent({
|
|
setup() {
|
|
service = useQuality(props, () => visible.value, changed)
|
|
return () => null
|
|
}
|
|
})
|
|
)
|
|
return { ...data, props, visible, changed, service }
|
|
}
|
|
|
|
describe('视觉质量契约与付费边界', () => {
|
|
it('打开面板不发模型请求,确认后仅校验指定首帧', async () => {
|
|
const data = setup()
|
|
expect(data.fetcher).not.toHaveBeenCalled()
|
|
await data.service.run('validate', input({ allowedTexts: ['记忆当铺'] }))
|
|
expect(data.posts()).toHaveLength(1)
|
|
expect(data.posts()[0]?.[0]).toBe('/api/storyboard-shots/shot-1/keyframes/keyframe-1/validate')
|
|
expect(JSON.parse(String(data.posts()[0]?.[1]?.body))).toEqual({ allowedTexts: ['记忆当铺'] })
|
|
expect(data.service.session.value.receipt).toMatchObject({ kind: 'keyframe', result: { passed: true } })
|
|
})
|
|
|
|
it.each(['generating', 'failed', 'need_review'] as const)('后端项目状态为 %s 时不发付费请求', async status => {
|
|
const data = setup()
|
|
data.project.status = status
|
|
await data.service.run('validate', input())
|
|
expect(data.posts()).toHaveLength(0)
|
|
expect(data.service.session.value.error).toContain('剧本未完成')
|
|
})
|
|
|
|
it('目标不属于项目时不查询目标素材,不生成', async () => {
|
|
const data = setup({ kind: 'keyframe', shotId: 'foreign-shot', assetId: 'foreign-image', title: '外部' })
|
|
await data.service.run('validate', input())
|
|
expect(data.posts()).toHaveLength(0)
|
|
expect(data.fetcher.mock.calls.some(([url]) => String(url).includes('foreign-shot'))).toBe(false)
|
|
})
|
|
|
|
it('畸形视觉结果不能当作校验通过,也不会自动重试', async () => {
|
|
const data = setup()
|
|
vi.spyOn(qualityApi, 'validateKeyframe').mockResolvedValueOnce({
|
|
...data.keyframeResult,
|
|
passed: undefined
|
|
} as never)
|
|
await data.service.run('validate', input())
|
|
expect(data.service.session.value.receipt).toBeNull()
|
|
expect(data.service.session.value.error).toContain('不完整')
|
|
expect(qualityApi.validateKeyframe).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('0 次修复保留语义;不会额外调用普通生图或切换主图接口', async () => {
|
|
const data = setup()
|
|
await data.service.run('repair', input({ maxRepairAttempts: 0 }))
|
|
expect(data.posts()).toHaveLength(1)
|
|
expect(data.posts()[0]?.[0]).toContain('/repair')
|
|
expect(JSON.parse(String(data.posts()[0]?.[1]?.body))).toEqual({ maxRepairAttempts: 0, allowedTexts: [] })
|
|
expect(data.fetcher.mock.calls.some(([, options]) => options?.method === 'PUT')).toBe(false)
|
|
expect(data.service.session.value.receipt).toMatchObject({ kind: 'repair', result: { primaryChanged: false } })
|
|
})
|
|
|
|
it('模型参考图能力不足时阻止修复与质量批次', async () => {
|
|
const data = setup()
|
|
data.capability.valid = false
|
|
await data.service.run('repair', input())
|
|
expect(data.posts()).toHaveLength(0)
|
|
expect(data.service.session.value.error).toContain('参考图超限')
|
|
data.props.target = { kind: 'batch', title: '第一集', episodeNo: 1 }
|
|
await data.service.run('batch', input())
|
|
expect(data.posts()).toHaveLength(0)
|
|
})
|
|
|
|
it('视频需完成并有有效时长,校验不自动切换主视频', async () => {
|
|
const data = setup({ kind: 'video', shotId: 'shot-1', assetId: 'video-1', title: '视频' })
|
|
data.video.durationSeconds = null
|
|
await data.service.run('validate', input())
|
|
expect(data.posts()).toHaveLength(0)
|
|
data.video.durationSeconds = 5
|
|
await data.service.run('validate', input())
|
|
expect(data.posts()).toHaveLength(1)
|
|
expect(data.posts()[0]?.[0]).toBe('/api/storyboard-shots/shot-1/videos/video-1/validate')
|
|
expect(data.service.session.value.receipt).toMatchObject({
|
|
kind: 'video',
|
|
result: { sampleFrames: [{ timeSeconds: 2 }] }
|
|
})
|
|
expect(data.fetcher.mock.calls.some(([, options]) => options?.method === 'PUT')).toBe(false)
|
|
})
|
|
|
|
it('批量范围、上限、尺寸原样传递,不发送 Provider;部分失败仍显示待处理', async () => {
|
|
const data = setup({ kind: 'batch', episodeNo: 1, title: '第一集' })
|
|
Object.assign(data.batch, {
|
|
passed: 0,
|
|
repairFailed: 1,
|
|
results: [{ shotId: 'shot-1', success: false, status: 'repair_failed', error: '人物不一致' }]
|
|
})
|
|
await data.service.run('batch', input({ width: 1536, height: 1024 }))
|
|
expect(data.posts()).toHaveLength(1)
|
|
expect(JSON.parse(String(data.posts()[0]?.[1]?.body))).toEqual(input({ width: 1536, height: 1024 }))
|
|
const receipt = data.service.session.value.receipt!
|
|
wrapper!.unmount()
|
|
wrapper = mount(QualityResult, { props: { receipt } })
|
|
expect(wrapper.text()).toContain('仍需处理')
|
|
expect(wrapper.text()).toContain('人物不一致')
|
|
await wrapper
|
|
.findAll('button')
|
|
.find(button => button.text() === '定位镜头')!
|
|
.trigger('click')
|
|
expect(wrapper.emitted('locate')).toEqual([['shot-1']])
|
|
})
|
|
|
|
it('过期优先级与后端一致,当前集无过期项时不误补其他镜头', async () => {
|
|
const data = setup({ kind: 'batch', episodeNo: 1, title: '第一集' })
|
|
data.readiness.stalePrimaryKeyframe = 1
|
|
expect(qualityTargets(data.readiness, input())).toHaveLength(0)
|
|
await data.service.run('batch', input())
|
|
expect(data.posts()).toHaveLength(0)
|
|
expect(data.service.session.value.error).toContain('切换剧集')
|
|
expect(qualityTargets(data.readiness, input({ force: true }))).toHaveLength(1)
|
|
})
|
|
|
|
it('错误输入与未完成素材不提交,已禁用操作也不能绕过', async () => {
|
|
const data = setup()
|
|
for (const values of [{ limit: 0 }, { concurrency: 1.5 }, { maxRepairAttempts: -1 }, { width: 100 }])
|
|
await data.service.run('repair', input(values))
|
|
expect(data.fetcher).not.toHaveBeenCalled()
|
|
data.keyframe.status = 'generating'
|
|
await data.service.run('repair', input())
|
|
expect(data.posts()).toHaveLength(0)
|
|
data.props.disabled = true
|
|
const count = data.fetcher.mock.calls.length
|
|
await data.service.run('validate', input())
|
|
expect(data.fetcher).toHaveBeenCalledTimes(count)
|
|
})
|
|
|
|
it('预检中关闭或换项目不发 POST;已提交的迟到回执只写回原目标', async () => {
|
|
const data = setup()
|
|
let finishCheck!: (value: KeyframeReadiness) => void
|
|
vi.spyOn(productionApi, 'keyframeReadiness').mockImplementationOnce(
|
|
() =>
|
|
new Promise(resolve => {
|
|
finishCheck = resolve
|
|
})
|
|
)
|
|
const pendingCheck = data.service.run('validate', input())
|
|
await flushPromises()
|
|
data.visible.value = false
|
|
finishCheck(data.readiness)
|
|
await pendingCheck
|
|
expect(data.posts()).toHaveLength(0)
|
|
data.visible.value = true
|
|
let finish!: (value: typeof data.keyframeResult) => void
|
|
vi.spyOn(qualityApi, 'validateKeyframe').mockImplementationOnce(
|
|
() =>
|
|
new Promise(resolve => {
|
|
finish = resolve
|
|
})
|
|
)
|
|
const originalKey = data.service.key.value
|
|
const pending = data.service.run('validate', input())
|
|
await flushPromises()
|
|
await data.service.run('validate', input())
|
|
expect(qualityApi.validateKeyframe).toHaveBeenCalledTimes(1)
|
|
data.props.projectId = 'different-project'
|
|
finish(data.keyframeResult)
|
|
await pending
|
|
expect(qualitySession(originalKey).receipt).toMatchObject({ kind: 'keyframe' })
|
|
expect(data.service.session.value.receipt).toBeNull()
|
|
})
|
|
|
|
it('API 对正式 ID 编码,视觉与质量长请求只发一次', async () => {
|
|
const data = server()
|
|
await qualityApi.validateKeyframe('shot/a', 'asset/b', [])
|
|
await qualityApi.repair('shot/a', 'asset/b', { maxRepairAttempts: 1 })
|
|
await qualityApi.validateVideo('shot/a', 'video/b', [])
|
|
expect(data.posts().map(([url]) => url)).toEqual([
|
|
'/api/storyboard-shots/shot%2Fa/keyframes/asset%2Fb/validate',
|
|
'/api/storyboard-shots/shot%2Fa/keyframes/asset%2Fb/repair',
|
|
'/api/storyboard-shots/shot%2Fa/videos/video%2Fb/validate'
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('质量面板渐进展示', () => {
|
|
it('默认仅校验,未确认时不可提交,打开或更改参数不产生费用', async () => {
|
|
const data = server()
|
|
wrapper = mount(QualityDialog, {
|
|
attachTo: document.body,
|
|
props: {
|
|
projectId: data.projectId,
|
|
target: { kind: 'keyframe', shotId: 'shot-1', assetId: 'keyframe-1', title: '测试镜头' },
|
|
open: true,
|
|
disabled: false
|
|
}
|
|
})
|
|
await flushPromises()
|
|
expect(validationButton().disabled).toBe(true)
|
|
expect(document.body.textContent).toContain('更多参数与模型限制')
|
|
expect(document.querySelector('[aria-label="额外允许的画面文字"]')).toBeNull()
|
|
expect(data.fetcher).not.toHaveBeenCalled()
|
|
wrapper.findAllComponents(NCheckbox).at(-1)!.vm.$emit('update:checked', true)
|
|
await flushPromises()
|
|
expect(validationButton().disabled).toBe(false)
|
|
validationButton().click()
|
|
await flushPromises()
|
|
expect(data.posts()).toHaveLength(1)
|
|
expect(document.body.textContent).toContain('视觉校验通过')
|
|
})
|
|
|
|
it('批量默认当前集一镜,调整次数撤销费用确认', async () => {
|
|
const data = server()
|
|
wrapper = mount(QualityDialog, {
|
|
attachTo: document.body,
|
|
props: {
|
|
projectId: data.projectId,
|
|
target: { kind: 'batch', episodeNo: 2, title: '第二集' },
|
|
open: true,
|
|
disabled: false
|
|
}
|
|
})
|
|
await flushPromises()
|
|
expect(document.body.textContent).toContain('仅第 2 集,最多 1 镜')
|
|
expect(document.body.textContent).toContain('最多调用 2 次生图、2 次视觉校验')
|
|
wrapper.findAllComponents(NCheckbox).at(-1)!.vm.$emit('update:checked', true)
|
|
await flushPromises()
|
|
wrapper.findAllComponents(NInputNumber)[0]!.vm.$emit('update:value', 2)
|
|
await flushPromises()
|
|
const submit = [...document.querySelectorAll<HTMLButtonElement>('button')].find(
|
|
item => item.textContent === '开始质量生成'
|
|
)!
|
|
expect(submit.disabled).toBe(true)
|
|
expect(data.posts()).toHaveLength(0)
|
|
})
|
|
|
|
it('模型能力只读按需查询,明确当前启用模型及参考图上限', async () => {
|
|
const data = server()
|
|
wrapper = mount(ModelCapabilities, { props: { shotId: 'shot-1' } })
|
|
expect(data.fetcher).not.toHaveBeenCalled()
|
|
await wrapper.get('button').trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper.text()).toContain('qwen-image · 当前启用')
|
|
expect(wrapper.text()).toContain('最多 3 张参考图')
|
|
expect(data.posts()).toHaveLength(0)
|
|
})
|
|
|
|
it('视频历史读取不触发视觉模型,畸形历史不会伪造通过', () => {
|
|
const data = server()
|
|
expect(savedVideoValidation(JSON.stringify({ videoValidation: data.validation }))?.passed).toBe(true)
|
|
expect(savedVideoValidation('{broken')).toBeNull()
|
|
expect(savedVideoValidation('{"videoValidation":{"passed":true}}')).toBeNull()
|
|
expect(allowedTextLines(' 招牌\n\n招牌\n编号 ')).toEqual(['招牌', '编号'])
|
|
expect(validQualityInput(input({ maxRepairAttempts: 0 }))).toBe(true)
|
|
expect(qualityKey('a', { kind: 'batch', episodeNo: 1, title: '' })).not.toBe(
|
|
qualityKey('b', { kind: 'batch', episodeNo: 1, title: '' })
|
|
)
|
|
expect(data.fetcher).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('视频修复候选与复检晋升', () => {
|
|
it('只允许校验未通过的视频创建一个候选,不自动再次校验', async () => {
|
|
const data = setup({ kind: 'video', shotId: 'shot-1', assetId: 'video-1', title: '视频' })
|
|
await data.service.run('repair', input({ maxRepairAttempts: 2 }))
|
|
expect(data.posts()).toHaveLength(0)
|
|
data.video.rawJson = JSON.stringify({ videoValidation: { ...data.validation, passed: false } })
|
|
await data.service.run('repair', input({ maxRepairAttempts: 2 }))
|
|
expect(data.posts()).toHaveLength(1)
|
|
expect(data.posts()[0]?.[0]).toBe('/api/storyboard-shots/shot-1/videos/video-1/repair')
|
|
expect(JSON.parse(String(data.posts()[0]?.[1]?.body))).toEqual({ maxRepairAttempts: 2 })
|
|
expect(data.service.session.value.receipt).toMatchObject({
|
|
kind: 'video-repair',
|
|
result: { candidate: { status: 'queued', isPrimary: false } }
|
|
})
|
|
})
|
|
|
|
it('链路次数上限不能作为零次修复,已达上限不创建任务', async () => {
|
|
const data = setup({ kind: 'video', shotId: 'shot-1', assetId: 'video-1', title: '视频' })
|
|
data.video.rawJson = JSON.stringify({
|
|
videoValidation: { ...data.validation, passed: false },
|
|
repair: { attempt: 2 }
|
|
})
|
|
await data.service.run('repair', input({ maxRepairAttempts: 0 }))
|
|
await data.service.run('repair', input({ maxRepairAttempts: 2 }))
|
|
expect(data.posts()).toHaveLength(0)
|
|
expect(data.service.session.value.error).toContain('次数上限')
|
|
})
|
|
|
|
it('复检通过的修复候选准确显示自动晋升,前端不再另发主视频 PUT', async () => {
|
|
const data = setup({ kind: 'video', shotId: 'shot-1', assetId: 'video-1', title: '修复候选' })
|
|
data.video.rawJson = JSON.stringify({ repair: { attempt: 1, sourceVideoId: 'source' } })
|
|
data.video.isPrimary = false
|
|
await data.service.run('validate', input())
|
|
const receipt = data.service.session.value.receipt!
|
|
expect(receipt).toMatchObject({ kind: 'video', promoted: true, result: { isPrimary: true } })
|
|
expect(data.posts()).toHaveLength(1)
|
|
expect(data.fetcher.mock.calls.some(([, options]) => options?.method === 'PUT')).toBe(false)
|
|
wrapper!.unmount()
|
|
wrapper = mount(QualityResult, { props: { receipt } })
|
|
expect(wrapper.text()).toContain('后端已将其设为主视频')
|
|
})
|
|
|
|
it('视频修复面板明确一次一任务及复检后替换,不展示生图次数', async () => {
|
|
const data = server()
|
|
wrapper = mount(QualityDialog, {
|
|
attachTo: document.body,
|
|
props: {
|
|
projectId: data.projectId,
|
|
target: { kind: 'video', shotId: 'shot-1', assetId: 'video-1', title: '视频' },
|
|
open: true,
|
|
disabled: false,
|
|
savedRawJson: JSON.stringify({ videoValidation: { ...data.validation, passed: false } })
|
|
}
|
|
})
|
|
await flushPromises()
|
|
expect(document.body.textContent).toContain('修复候选复检通过后会自动设为主视频')
|
|
wrapper.getComponent(NRadioGroup).vm.$emit('update:value', 'repair')
|
|
await flushPromises()
|
|
expect(document.body.textContent).toContain('本次最多提交 1 个视频生成任务')
|
|
expect(document.body.textContent).not.toContain('次生图')
|
|
expect(document.body.textContent).toContain('修复链次数上限')
|
|
expect(data.posts()).toHaveLength(0)
|
|
})
|
|
})
|