feat: 补齐前端主流程并收紧工作台布局
统一工作台紧凑头部与形态图库间距,修复主体身份布局,并接入正式严格视频质量流水线。
This commit is contained in:
@@ -162,7 +162,7 @@ describe('全站平面主题', () => {
|
||||
)
|
||||
expect(css).toMatch(/\.asset-impact-links-scroll\.n-scrollbar\s*\{[^}]*height:\s*auto;[^}]*max-width:\s*100%;/)
|
||||
expect(readFileSync('src/features/subject-images/SubjectImagesPage.vue', 'utf8')).toMatch(
|
||||
/<WorkspacePage\b[^>]*>\s*<template #default>/
|
||||
/<WorkspacePage\b[^>]*>\s*<template #header>/
|
||||
)
|
||||
})
|
||||
|
||||
@@ -184,7 +184,7 @@ describe('全站平面主题', () => {
|
||||
// DOM 环境不计算几何;保护容器宽度断点和居中契约,真实坐标仍需浏览器验收。
|
||||
const css = readAllStyles()
|
||||
expect(css).toMatch(
|
||||
/\.form-image-filter-region\s*\{[^}]*padding(?:-block)?:\s*14px;[^}]*background:\s*var\(--app-subtle\);/
|
||||
/\.form-image-filter-region\s*\{[^}]*padding(?:-block)?:\s*5px;[^}]*background:\s*var\(--app-subtle\);/
|
||||
)
|
||||
expect(css).toMatch(/\.form-image-toolbar\s*\{[^}]*align-items:\s*center;[^}]*gap:\s*12px 16px;/)
|
||||
expect(css).toMatch(
|
||||
|
||||
@@ -99,12 +99,19 @@ describe('镜头生产数据契约', () => {
|
||||
})
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await productionApi.videoReadiness('project/1', true)
|
||||
await productionApi.generateVideos('project/1', { concurrency: 3, force: true })
|
||||
await productionApi.generateQualityVideos('project/1', {
|
||||
concurrency: 3,
|
||||
force: true,
|
||||
limit: 2,
|
||||
maxRepairAttempts: 2
|
||||
})
|
||||
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(fetcher.mock.calls[1]?.[0]).toBe('/api/projects/project%2F1/videos/generate-quality')
|
||||
expect(JSON.parse(String(fetcher.mock.calls[1]?.[1]?.body))).toEqual({
|
||||
concurrency: 3,
|
||||
force: true
|
||||
force: true,
|
||||
limit: 2,
|
||||
maxRepairAttempts: 2
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ describe('过期首帧到具体素材定位', () => {
|
||||
|
||||
it('切换镜头时筛选、关联说明和图片共用稳定纵向容器,长关联列表单独横向滚动', async () => {
|
||||
const { keyframes, videos, references, fetcher } = await gallery('?sourceShotId=shot-target')
|
||||
expect(wrapper!.find('.workspace-heading-scroll').exists()).toBe(false)
|
||||
expect(wrapper!.find('.workspace-heading-scroll').exists()).toBe(true)
|
||||
const scroll = wrapper!.get<HTMLElement>('.workspace-scroll > .n-scrollbar-container').element
|
||||
const content = wrapper!.get('.workspace-scroll-content').element
|
||||
const sticky = wrapper!.get('.gallery-sticky-controls').element
|
||||
|
||||
@@ -1111,7 +1111,7 @@ describe('工作台页面交互', () => {
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await mountStoryboard()
|
||||
await flushPromises()
|
||||
await confirmGeneration('生成本集导演设计')
|
||||
await confirmGeneration('补齐项目导演设计')
|
||||
expect(button('补齐项目导演设计').disabled).toBe(true)
|
||||
await drawerPanel().get('[aria-label="关闭操作面板"]').trigger('click')
|
||||
await flushPromises()
|
||||
@@ -1123,61 +1123,69 @@ describe('工作台页面交互', () => {
|
||||
wrapper = undefined
|
||||
finish(
|
||||
new Response(
|
||||
'{"data":{"episodeNo":1,"direction":{"episodeNo":1,"beatDirections":[]},"validation":{"valid":true,"issues":[]}}}'
|
||||
`{"data":{"projectId":"${fixture.id}","episodeCount":2,"completedEpisodes":1,"skippedEpisodes":1,"failedEpisodes":0,"shotCount":4,"directionCount":4,"coverage":1,"episodes":[]}}`
|
||||
)
|
||||
)
|
||||
await flushPromises()
|
||||
expect(getStoryboardSession(fixture.id).receipt).toMatchObject({
|
||||
kind: 'direction',
|
||||
episodeNo: 1,
|
||||
title: '第 1 集导演设计'
|
||||
kind: 'batch',
|
||||
title: '项目导演设计'
|
||||
})
|
||||
expect(getOperation(fixture.id).pending).toBe(false)
|
||||
const posts = fetcher.mock.calls.filter(([, init]) => init?.method === 'POST')
|
||||
expect(posts).toHaveLength(1)
|
||||
expect(JSON.parse(posts[0]![1]!.body as string)).toEqual({ episodeNo: 1, persist: true })
|
||||
expect(JSON.parse(posts[0]![1]!.body as string)).toEqual({ concurrency: 2, force: false })
|
||||
})
|
||||
|
||||
it('分镜必须先补齐导演设计,校验失败显示未保存,修复次数 0 不被默认值覆盖', async () => {
|
||||
let complete = false
|
||||
it('分镜只提供正式项目生成,逐集失败回执保留零次修复参数', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async (url, init) => {
|
||||
if (init?.method === 'POST')
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
episodeNo: 1,
|
||||
visualState: {},
|
||||
validation: {
|
||||
valid: false,
|
||||
issues: [{ episodeNo: 1, beatNo: 1, shotNo: 1, message: '缺少主体状态' }]
|
||||
},
|
||||
repaired: false,
|
||||
repairAttempts: 0,
|
||||
remainingIssues: [],
|
||||
persisted: false
|
||||
projectId: fixture.id,
|
||||
episodeCount: 1,
|
||||
completedEpisodes: 0,
|
||||
skippedEpisodes: 0,
|
||||
failedEpisodes: 1,
|
||||
shotCount: 2,
|
||||
visualStateCount: 0,
|
||||
coverage: 0,
|
||||
episodes: [
|
||||
{
|
||||
episodeNo: 1,
|
||||
status: 'failed',
|
||||
shotCount: 2,
|
||||
visualStateCount: 0,
|
||||
repairAttempts: 0,
|
||||
error: '缺少主体状态'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
)
|
||||
const result = String(url).includes('storyboard-directions')
|
||||
? directionsResult(fixture.id, 1, complete)
|
||||
? directionsResult(fixture.id, 1, true)
|
||||
: visualStatesResult(fixture.id)
|
||||
return new Response(JSON.stringify({ data: result }))
|
||||
})
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await mountStoryboard()
|
||||
await flushPromises()
|
||||
expect(button('生成本集导演设计').disabled).toBe(false)
|
||||
expect(button('生成本集即时状态').disabled).toBe(true)
|
||||
expect(wrapper!.text()).not.toContain('生成本集导演设计')
|
||||
expect(wrapper!.text()).not.toContain('生成本集即时状态')
|
||||
expect(button('补齐项目导演设计').disabled).toBe(false)
|
||||
expect(button('补齐项目即时状态').disabled).toBe(false)
|
||||
expect(fetcher.mock.calls.every(([, init]) => init?.method !== 'POST')).toBe(true)
|
||||
complete = true
|
||||
button('刷新分镜').click()
|
||||
await flushPromises()
|
||||
expect(button('生成本集即时状态').disabled).toBe(false)
|
||||
await drawerPanel().get('#storyboard-repairs').setValue('0')
|
||||
await confirmGeneration('生成本集即时状态')
|
||||
await confirmGeneration('补齐项目即时状态')
|
||||
const post = fetcher.mock.calls.find(([, init]) => init?.method === 'POST')!
|
||||
expect(JSON.parse(post[1]!.body as string)).toEqual({ episodeNo: 1, persist: true, maxRepairAttempts: 0 })
|
||||
expect(drawerPanel().text()).toContain('校验失败,未保存本次生成结果')
|
||||
expect(JSON.parse(post[1]!.body as string)).toEqual({
|
||||
concurrency: 2,
|
||||
force: false,
|
||||
maxRepairAttempts: 0
|
||||
})
|
||||
expect(drawerPanel().text()).toContain('本次存在失败')
|
||||
expect(drawerPanel().text()).toContain('缺少主体状态')
|
||||
})
|
||||
|
||||
@@ -1291,11 +1299,11 @@ describe('工作台页面交互', () => {
|
||||
await flushPromises()
|
||||
expect(wrapper!.text()).toContain('shot-db-1-1')
|
||||
expect(wrapper!.text()).toContain('数据库暂不可用')
|
||||
expect(button('生成本集导演设计').disabled).toBe(true)
|
||||
expect(button('补齐项目导演设计').disabled).toBe(true)
|
||||
fail = false
|
||||
button('重试查询').click()
|
||||
await flushPromises()
|
||||
expect(button('生成本集导演设计').disabled).toBe(false)
|
||||
expect(button('补齐项目导演设计').disabled).toBe(false)
|
||||
})
|
||||
|
||||
it('镜头工具按需 GET,切换镜头会废弃旧参考图,未完成设计不能编译规格', async () => {
|
||||
|
||||
+11
-15
@@ -43,21 +43,13 @@ describe('后端 API 契约', () => {
|
||||
})
|
||||
expect(fetcher.mock.calls[4]![1]!.body).toBeUndefined()
|
||||
})
|
||||
it('分镜单集显式持久化,批量保持 force 和零次修复参数', async () => {
|
||||
it('分镜只调用正式项目接口,并保持 force 和零次修复参数', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async () => new Response('{"data":{}}'))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await storyboardApi.generateDirection('a/b', 2)
|
||||
await storyboardApi.generateDirections('p', { concurrency: 2, force: false })
|
||||
await storyboardApi.generateVisualState('p', 2, 0)
|
||||
await storyboardApi.generateDirections('a/b', { concurrency: 2, force: false })
|
||||
await storyboardApi.generateVisualStates('p', { concurrency: 3, force: true, maxRepairAttempts: 0 })
|
||||
expect(fetcher.mock.calls.map(([url, init]) => [url, init?.method, JSON.parse(init!.body as string)])).toEqual([
|
||||
['/api/projects/a%2Fb/storyboard-directions/generate-test', 'POST', { episodeNo: 2, persist: true }],
|
||||
['/api/projects/p/storyboard-directions/generate', 'POST', { concurrency: 2, force: false }],
|
||||
[
|
||||
'/api/projects/p/storyboard-visual-states/generate-test',
|
||||
'POST',
|
||||
{ episodeNo: 2, persist: true, maxRepairAttempts: 0 }
|
||||
],
|
||||
['/api/projects/a%2Fb/storyboard-directions/generate', 'POST', { concurrency: 2, force: false }],
|
||||
[
|
||||
'/api/projects/p/storyboard-visual-states/generate',
|
||||
'POST',
|
||||
@@ -86,7 +78,7 @@ describe('后端 API 契约', () => {
|
||||
expect(JSON.parse(fetcher.mock.calls[4]![1]!.body as string)).toEqual({ force: false })
|
||||
})
|
||||
|
||||
it('分镜生成保留 200 中的校验失败和持久化标志,长请求不自动超时或重试', async () => {
|
||||
it('分镜正式批量生成保留部分失败回执,长请求不自动超时或重试', async () => {
|
||||
vi.useFakeTimers()
|
||||
let finish!: (response: Response) => void
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(
|
||||
@@ -96,12 +88,16 @@ describe('后端 API 契约', () => {
|
||||
})
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
const pending = storyboardApi.generateVisualState('p', 1, 2)
|
||||
const pending = storyboardApi.generateVisualStates('p', {
|
||||
concurrency: 1,
|
||||
force: false,
|
||||
maxRepairAttempts: 2
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(120_000)
|
||||
expect(fetcher).toHaveBeenCalledOnce()
|
||||
expect(fetcher.mock.calls[0]?.[1]?.signal?.aborted).toBe(false)
|
||||
finish(new Response('{"data":{"validation":{"valid":false,"issues":[]},"persisted":false}}'))
|
||||
await expect(pending).resolves.toMatchObject({ validation: { valid: false }, persisted: false })
|
||||
finish(new Response('{"data":{"failedEpisodes":1,"episodes":[{"episodeNo":1,"status":"failed"}]}}'))
|
||||
await expect(pending).resolves.toMatchObject({ failedEpisodes: 1, episodes: [{ status: 'failed' }] })
|
||||
})
|
||||
it('解包 data,同时保留创建项目的顶层 202 结构', async () => {
|
||||
const fetcher = vi
|
||||
|
||||
Reference in New Issue
Block a user