505 lines
26 KiB
TypeScript
505 lines
26 KiB
TypeScript
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { NSelect } from 'naive-ui'
|
|
import { testProjectContext } from '@/testing/project-context'
|
|
import { projectContextKey } from '@/features/projects/context'
|
|
import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures'
|
|
import { referenceLocation, referenceTargets } from '@/features/production/asset-links'
|
|
import StaleKeyframeNotice from '@/features/production/components/StaleKeyframeNotice.vue'
|
|
import SubjectImagesPage from '@/features/subject-images/SubjectImagesPage.vue'
|
|
import ProductionPage from '@/features/production/ProductionPage.vue'
|
|
import { directionsResult, storyboardCheckpoint } from '@/features/storyboard/testing/fixtures'
|
|
import type { ProductionIssue } from '@/features/production/types'
|
|
import type { ShotReferences } from '@/features/storyboard/types'
|
|
|
|
let wrapper: VueWrapper | undefined
|
|
afterEach(() => {
|
|
wrapper?.unmount()
|
|
wrapper = undefined
|
|
document.body.innerHTML = ''
|
|
vi.useRealTimers()
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
/** 镜头引用非默认形态,用于防止退化成仅按主体名搜索。 */
|
|
function fixture() {
|
|
const first = formFixture('capability-test')
|
|
const second = {
|
|
...first,
|
|
id: 'form-special',
|
|
name: '雨夜形态',
|
|
isDefault: false,
|
|
images: [imageFixture({ subjectFormId: 'form-special' })]
|
|
}
|
|
const other = {
|
|
...formFixture('capability-test'),
|
|
id: 'form-other',
|
|
subjectId: 'subject-other',
|
|
subject: { ...first.subject, id: 'subject-other', name: '路人', ref: '@CH0005' },
|
|
images: [imageFixture({ subjectFormId: 'form-other' })]
|
|
}
|
|
const references: ShotReferences = {
|
|
shotId: 'shot-target',
|
|
references: [
|
|
{
|
|
shotSubjectId: 'binding-1',
|
|
subjectId: first.subjectId,
|
|
subjectRef: first.subject.ref,
|
|
subjectName: first.subject.name,
|
|
module: 'character',
|
|
subjectFormId: second.id,
|
|
subjectFormName: second.name,
|
|
imageId: 'image-db-1',
|
|
imageUrl: '/storage/current.png'
|
|
},
|
|
{
|
|
shotSubjectId: 'binding-2',
|
|
subjectId: other.subjectId,
|
|
subjectRef: other.subject.ref,
|
|
subjectName: other.subject.name,
|
|
module: 'character',
|
|
subjectFormId: other.id,
|
|
subjectFormName: other.name,
|
|
imageId: 'other-image',
|
|
imageUrl: '/storage/other.png'
|
|
}
|
|
],
|
|
missing: []
|
|
}
|
|
const issues: ProductionIssue[] = [
|
|
{ code: 'stale_keyframe', reason: '参考资产已变化: @CH0001, @CH0005', missingSubjects: ['@CH0001', '@CH0005'] }
|
|
]
|
|
const keyframes = {
|
|
total: 1,
|
|
ready: 1,
|
|
skipped: 0,
|
|
blocked: 0,
|
|
stalePrimaryKeyframe: 1,
|
|
items: [
|
|
{
|
|
shotId: 'shot-target',
|
|
shotNo: 1,
|
|
episodeNo: 2,
|
|
beatNo: 2,
|
|
status: 'ready',
|
|
primaryKeyframeStale: true,
|
|
primaryKeyframeId: 'keyframe-old',
|
|
issues: []
|
|
}
|
|
]
|
|
}
|
|
const videos = {
|
|
total: 1,
|
|
ready: 0,
|
|
skipped: 0,
|
|
blocked: 1,
|
|
items: [{ shotId: 'shot-target', shotNo: 1, status: 'blocked', issues }]
|
|
}
|
|
const fetcher = vi.fn<typeof fetch>().mockImplementation(async url => {
|
|
const path = String(url)
|
|
const data = path.endsWith('/subject-forms')
|
|
? [first, second, other]
|
|
: path.includes('/keyframes/readiness')
|
|
? keyframes
|
|
: path.includes('/videos/readiness')
|
|
? videos
|
|
: path.endsWith('/references')
|
|
? references
|
|
: []
|
|
return new Response(JSON.stringify({ data }))
|
|
})
|
|
vi.stubGlobal('fetch', fetcher)
|
|
return { first, second, other, references, issues, keyframes, videos, fetcher }
|
|
}
|
|
|
|
/** 在真实内存路由中验证跨页 URL,不替换 RouterLink 行为。 */
|
|
async function gallery(query = '') {
|
|
const data = fixture()
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [
|
|
{ path: '/projects/:projectId/subject-images', component: SubjectImagesPage },
|
|
{ path: '/projects/:projectId/production', component: { template: '<div />' } }
|
|
]
|
|
})
|
|
await router.push('/projects/capability-test/subject-images' + query)
|
|
wrapper = mount(SubjectImagesPage, {
|
|
attachTo: document.body,
|
|
global: { plugins: [router], provide: { [projectContextKey as symbol]: testProjectContext() } }
|
|
})
|
|
await flushPromises()
|
|
return { ...data, router }
|
|
}
|
|
|
|
describe('过期首帧到具体素材定位', () => {
|
|
it('网格与瀑布流切换保留卡片、滚动容器和筛选,不发起额外请求', async () => {
|
|
const { fetcher } = await gallery()
|
|
const requestCount = fetcher.mock.calls.length
|
|
const scroll = wrapper!.get<HTMLElement>('.workspace-scroll > .n-scrollbar-container').element
|
|
const grid = wrapper!.get('.form-image-grid').element
|
|
const cards = wrapper!.findAll('.form-image-card').map(card => card.element)
|
|
scroll.scrollTop = 240
|
|
expect(wrapper!.get('[aria-label="网格布局"]').attributes('aria-pressed')).toBe('true')
|
|
await wrapper!.get('[aria-label="瀑布流布局"]').trigger('click')
|
|
expect(wrapper!.get('.form-image-masonry').element).toBe(grid)
|
|
expect(wrapper!.get('[aria-label="瀑布流布局"]').attributes('aria-pressed')).toBe('true')
|
|
expect(wrapper!.get('[aria-label="网格布局"]').attributes('aria-pressed')).toBe('false')
|
|
expect(wrapper!.findAll('.form-image-card').map(card => card.element)).toEqual(cards)
|
|
expect(scroll.scrollTop).toBe(240)
|
|
expect(
|
|
wrapper!.get<HTMLElement>('.form-image-card').element.style.getPropertyValue('--form-image-aspect')
|
|
).toBe('')
|
|
await wrapper!.get('input[aria-label="搜索形态图片"]').setValue('雨夜')
|
|
await wrapper!.get('[aria-label="网格布局"]').trigger('click')
|
|
expect(wrapper!.find('.form-image-masonry').exists()).toBe(false)
|
|
expect(wrapper!.get('.form-image-grid').element).toBe(grid)
|
|
expect(wrapper!.findAll('.form-image-card')).toHaveLength(1)
|
|
expect(wrapper!.get('.form-image-card').text()).toContain('雨夜形态')
|
|
expect(wrapper!.get('.form-image-card').text()).toContain('查看图片与记录')
|
|
expect(wrapper!.get('.workspace-scroll > .n-scrollbar-container').element).toBe(scroll)
|
|
expect(fetcher).toHaveBeenCalledTimes(requestCount)
|
|
})
|
|
|
|
it('图库与关联检查不定时刷新,手动刷新仍能更新数据', async () => {
|
|
vi.useFakeTimers()
|
|
const { fetcher } = await gallery('?sourceShotId=shot-target')
|
|
const count = fetcher.mock.calls.length
|
|
const grid = wrapper!.get('.form-image-grid').element
|
|
await vi.advanceTimersByTimeAsync(30_000)
|
|
await flushPromises()
|
|
expect(fetcher).toHaveBeenCalledTimes(count)
|
|
expect(wrapper!.get('.form-image-grid').element).toBe(grid)
|
|
const refresh = wrapper!.findAll('button').find(button => button.text().includes('刷新图库'))!
|
|
await refresh.trigger('click')
|
|
await flushPromises()
|
|
expect(fetcher.mock.calls.length).toBeGreaterThan(count)
|
|
const refreshed = fetcher.mock.calls.length
|
|
await vi.advanceTimersByTimeAsync(30_000)
|
|
expect(fetcher).toHaveBeenCalledTimes(refreshed)
|
|
})
|
|
|
|
it('切换镜头时筛选、关联说明和图片共用稳定纵向容器,长关联列表单独横向滚动', async () => {
|
|
const { keyframes, videos, references, fetcher } = await gallery('?sourceShotId=shot-target')
|
|
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
|
|
const toolbar = wrapper!.get('.form-image-filter-region').element
|
|
expect(sticky.parentElement).toBe(content)
|
|
expect(toolbar.parentElement).toBe(sticky)
|
|
expect(wrapper!.get('.asset-impact-context').element.parentElement).toBe(sticky)
|
|
expect(wrapper!.get('.form-image-grid').element.parentElement).toBe(content)
|
|
scroll.scrollTop = 480
|
|
keyframes.items.push({ ...keyframes.items[0]!, shotId: 'shot-next', shotNo: 2 })
|
|
videos.items.push({ ...videos.items[0]!, shotId: 'shot-next', shotNo: 2 })
|
|
keyframes.total = videos.total = 2
|
|
let finish!: (response: Response) => void
|
|
const original = fetcher.getMockImplementation()!
|
|
fetcher.mockImplementation((url, init) =>
|
|
String(url).endsWith('/shot-next/references')
|
|
? new Promise(resolve => {
|
|
finish = resolve
|
|
})
|
|
: original(url, init)
|
|
)
|
|
await wrapper!
|
|
.findAll('button')
|
|
.find(button => button.text() === '刷新图库')!
|
|
.trigger('click')
|
|
await flushPromises()
|
|
wrapper!
|
|
.findAllComponents(NSelect)
|
|
.find(item => item.attributes('aria-label') === '选择关联镜头')!
|
|
.vm.$emit('update:value', 'shot-next')
|
|
await flushPromises()
|
|
expect(wrapper!.text()).toContain('正在读取镜头关联素材')
|
|
finish(
|
|
new Response(
|
|
JSON.stringify({
|
|
data: {
|
|
...references,
|
|
shotId: 'shot-next',
|
|
references: Array.from({ length: 24 }, (_, index) => ({
|
|
...references.references[0]!,
|
|
subjectFormId: `long-form-${index}`,
|
|
subjectFormName: `很长的关联素材名称${index}`
|
|
}))
|
|
}
|
|
})
|
|
)
|
|
)
|
|
await flushPromises()
|
|
expect(wrapper!.get('.workspace-scroll > .n-scrollbar-container').element).toBe(scroll)
|
|
expect(wrapper!.get('.form-image-filter-region').element).toBe(toolbar)
|
|
expect(scroll.scrollTop).toBe(480)
|
|
expect(wrapper!.get('.gallery-sticky-controls').element).toBe(sticky)
|
|
expect(wrapper!.get('.asset-impact-context').element.parentElement).toBe(sticky)
|
|
expect(wrapper!.get('.form-image-grid').element.parentElement).toBe(content)
|
|
const links = wrapper!.get('.asset-impact-links-scroll')
|
|
expect(links.findAll('a')).toHaveLength(24)
|
|
expect(links.text()).toContain('很长的关联素材名称23')
|
|
expect(links.classes()).toContain('n-scrollbar')
|
|
expect(links.find('.n-scrollbar-container > .asset-impact-links').exists()).toBe(true)
|
|
expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true)
|
|
})
|
|
|
|
it('切换镜头取消旧参考图查询,迟到响应不会产生旧素材链接', async () => {
|
|
const { references } = fixture()
|
|
let finish!: (response: Response) => void
|
|
const nextReferences = {
|
|
...references,
|
|
shotId: 'shot-new',
|
|
references: [{ ...references.references[0]!, subjectFormId: 'form-new', subjectFormName: '新镜头形态' }]
|
|
}
|
|
const fetcher = vi.fn<typeof fetch>().mockImplementation(url =>
|
|
String(url).includes('shot-target')
|
|
? new Promise(resolve => {
|
|
finish = resolve
|
|
})
|
|
: Promise.resolve(new Response(JSON.stringify({ data: nextReferences })))
|
|
)
|
|
vi.stubGlobal('fetch', fetcher)
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [{ path: '/:pathMatch(.*)*', component: { template: '<div />' } }]
|
|
})
|
|
await router.push('/')
|
|
wrapper = mount(StaleKeyframeNotice, {
|
|
global: { plugins: [router] },
|
|
props: { projectId: 'capability-test', shotId: 'shot-target', issues: [] }
|
|
})
|
|
await flushPromises()
|
|
const signal = fetcher.mock.calls[0]?.[1]?.signal
|
|
await wrapper.setProps({ shotId: 'shot-new' })
|
|
await flushPromises()
|
|
expect(signal?.aborted).toBe(true)
|
|
finish(new Response(JSON.stringify({ data: references })))
|
|
await flushPromises()
|
|
expect(wrapper.text()).toContain('新镜头形态')
|
|
expect(wrapper.findAll('a').some(link => link.attributes('href')?.includes('form-special'))).toBe(false)
|
|
})
|
|
it('参考素材查询失败时保留引用级定位和重试,不静默选择默认形态', async () => {
|
|
const { issues } = fixture()
|
|
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockResolvedValue(new Response('{}', { status: 500 })))
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [{ path: '/:pathMatch(.*)*', component: { template: '<div />' } }]
|
|
})
|
|
await router.push('/')
|
|
wrapper = mount(StaleKeyframeNotice, {
|
|
global: { plugins: [router] },
|
|
props: { projectId: 'capability-test', shotId: 'shot-target', issues, inconsistent: true }
|
|
})
|
|
await flushPromises()
|
|
expect(wrapper.text()).toContain('状态待核对')
|
|
expect(wrapper.text()).toContain('重试定位')
|
|
expect(wrapper.findAll('a').every(link => !link.attributes('href')?.includes('subjectFormId'))).toBe(true)
|
|
})
|
|
it('结构化变更引用映射到实际使用的非默认形态,并保留来源镜头', () => {
|
|
const { references, issues } = fixture()
|
|
const targets = referenceTargets(references, issues)
|
|
expect(targets.map(item => item.formId)).toEqual(['form-special', 'form-other'])
|
|
expect(referenceLocation('project/1', 'shot-target', targets[0]!)).toEqual({
|
|
path: '/projects/project%2F1/subject-images',
|
|
query: { sourceShotId: 'shot-target', subjectRef: '@CH0001', subjectFormId: 'form-special' }
|
|
})
|
|
})
|
|
it('没有当前关联时只回退到引用,不猜默认形态;没有变更列表时展示关联素材', () => {
|
|
const { references } = fixture()
|
|
const targets = referenceTargets(references, [
|
|
{ code: 'stale_keyframe', reason: '历史主体被移除', missingSubjects: ['@CH0999'] }
|
|
])
|
|
expect(targets[0]?.formId).toBeUndefined()
|
|
expect(targets[0]?.label).toContain('当前形态待核对')
|
|
expect(referenceTargets(references, [])).toHaveLength(2)
|
|
})
|
|
it('生产警告的引用可点击定位,点击不提交生图', async () => {
|
|
const { issues, fetcher } = fixture()
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [{ path: '/:pathMatch(.*)*', component: { template: '<div />' } }]
|
|
})
|
|
await router.push('/projects/capability-test/production')
|
|
wrapper = mount(StaleKeyframeNotice, {
|
|
global: { plugins: [router] },
|
|
props: { projectId: 'capability-test', shotId: 'shot-target', issues }
|
|
})
|
|
await flushPromises()
|
|
const links = wrapper.findAll('a')
|
|
expect(links).toHaveLength(2)
|
|
expect(links[0]?.text()).toContain('雨夜形态')
|
|
await links[0]!.trigger('click')
|
|
await flushPromises()
|
|
expect(router.currentRoute.value.query.subjectFormId).toBe('form-special')
|
|
expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true)
|
|
})
|
|
it('图库直接访问即显示下游警告,但不把有效素材误标成身份过期', async () => {
|
|
const { fetcher } = await gallery()
|
|
expect(wrapper!.get('[data-keyframe-impact]').text()).toContain('1 个镜头')
|
|
expect(wrapper!.text()).toContain('不表示形态图片本身失效')
|
|
expect(wrapper!.findAll('.form-image-card')).toHaveLength(3)
|
|
expect(wrapper!.text()).not.toContain('个形态主图与当前已锁定身份母版不一致')
|
|
expect(fetcher.mock.calls.some(([url]) => String(url).endsWith('/references'))).toBe(false)
|
|
// 镜头选择和图库筛选共享上方工具栏;未选择镜头时不留下空的关联说明面板。
|
|
const toolbar = wrapper!.get('.form-image-toolbar')
|
|
const picker = toolbar.get('.asset-impact-picker').element
|
|
expect(toolbar.classes()).toContain('has-impact-picker')
|
|
expect(picker.nextElementSibling).toBe(toolbar.get('.form-image-filters').element)
|
|
expect(toolbar.find('input[aria-label="搜索形态图片"]').exists()).toBe(true)
|
|
expect(toolbar.findAll('.filter-toggles [role="checkbox"]')).toHaveLength(2)
|
|
expect(wrapper!.find('.asset-impact-context').exists()).toBe(false)
|
|
const select = wrapper!
|
|
.findAllComponents(NSelect)
|
|
.find(item => item.attributes('aria-label') === '选择关联镜头')!
|
|
select.vm.$emit('update:value', 'shot-target')
|
|
await flushPromises()
|
|
expect(wrapper!.get('.asset-impact-context').element.previousElementSibling).toBe(
|
|
wrapper!.get('.form-image-filter-region').element
|
|
)
|
|
expect(wrapper!.text()).toContain('待处理首帧关联此素材')
|
|
expect(fetcher.mock.calls.filter(([url]) => String(url).endsWith('/references'))).toHaveLength(1)
|
|
})
|
|
it('镜头导航与筛选共行,无关联说明时不留横条,查看全部素材清除定位和筛选', async () => {
|
|
const { references, router, fetcher } = await gallery()
|
|
references.references = []
|
|
await router.push({ query: { sourceShotId: 'shot-target' } })
|
|
await flushPromises()
|
|
const picker = wrapper!.get('.asset-impact-picker')
|
|
const back = picker.get('a[aria-label="返回第 2 集 · 镜头 1"]')
|
|
expect(back.classes()).toContain('icon-button')
|
|
expect(new URL(back.attributes('href')!, 'https://local.test').searchParams.get('shotId')).toBe('shot-target')
|
|
expect(picker.get('[aria-label="查看全部素材"]').classes()).toContain('icon-button')
|
|
expect(wrapper!.find('.asset-impact-context').exists()).toBe(false)
|
|
await wrapper!.get('input[aria-label="搜索形态图片"]').setValue('不存在的素材')
|
|
expect(wrapper!.findAll('.form-image-card')).toHaveLength(0)
|
|
await picker.get('[aria-label="查看全部素材"]').trigger('click')
|
|
await flushPromises()
|
|
expect(router.currentRoute.value.query.sourceShotId).toBeUndefined()
|
|
expect(wrapper!.findAll('.form-image-card')).toHaveLength(3)
|
|
expect(wrapper!.find('.asset-impact-picker a').exists()).toBe(false)
|
|
expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true)
|
|
})
|
|
|
|
it('精准定位卡片、清空旧类型筛选、提供可恢复原镜头的返回链接', async () => {
|
|
const { router } = await gallery('?sourceShotId=shot-target&subjectFormId=form-special&subjectRef=%40CH0001')
|
|
expect(wrapper!.findAll('.form-image-card')).toHaveLength(1)
|
|
expect(wrapper!.get('.form-image-card').attributes('data-form-id')).toBe('form-special')
|
|
expect(wrapper!.get('.form-image-card').classes()).toContain('form-image-card-focused')
|
|
expect(wrapper!.get('[data-focused-material]').text()).toContain('雨夜形态')
|
|
const back = wrapper!.findAll('a').find(link => link.text().includes('返回第 2 集'))!
|
|
const url = new URL(back.attributes('href')!, 'https://local.test')
|
|
expect(url.searchParams.get('episodeNo')).toBe('2')
|
|
expect(url.searchParams.get('shotId')).toBe('shot-target')
|
|
const type = wrapper!.findAllComponents(NSelect).find(item => item.attributes('aria-label') === '筛选主体类型')!
|
|
type.vm.$emit('update:value', 'scene')
|
|
await flushPromises()
|
|
expect(wrapper!.findAll('.form-image-card')).toHaveLength(0)
|
|
await router.push({ query: { sourceShotId: 'shot-target', subjectFormId: 'form-other' } })
|
|
await flushPromises()
|
|
expect(wrapper!.get('.form-image-card').attributes('data-form-id')).toBe('form-other')
|
|
const clear = wrapper!.findAll('button').find(button => button.text() === '清除定位')!
|
|
await clear.trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper!.findAll('.form-image-card')).toHaveLength(3)
|
|
})
|
|
it('身份主图真的过期时显示独立顶部警告,并可从定位模式筛选所有过期素材', async () => {
|
|
const { first, router } = await gallery('?subjectFormId=form-other')
|
|
first.subject.identity = { id: 'identity-1', isLocked: true, images: [{ id: 'anchor-new' }] }
|
|
const refresh = wrapper!.findAll('button').find(button => button.text() === '刷新图库')!
|
|
await refresh.trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper!.text()).toContain('形态主图需要更新')
|
|
await wrapper!
|
|
.findAll('button')
|
|
.find(button => button.text() === '筛选身份过期形态')!
|
|
.trigger('click')
|
|
await flushPromises()
|
|
expect(router.currentRoute.value.query.subjectFormId).toBeUndefined()
|
|
expect(wrapper!.findAll('.form-image-card').map(item => item.attributes('data-form-id'))).toEqual([
|
|
'form-db-1',
|
|
'form-special'
|
|
])
|
|
})
|
|
it('仅视频接口报告过期时也显示警告,并明确两种检查不一致', async () => {
|
|
const { keyframes } = await gallery('?sourceShotId=shot-target&subjectFormId=form-special')
|
|
keyframes.items[0]!.primaryKeyframeStale = false
|
|
await wrapper!
|
|
.findAll('button')
|
|
.find(button => button.text() === '刷新图库')!
|
|
.trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper!.get('[data-keyframe-impact]').text()).toContain('检查结果不一致')
|
|
expect(wrapper!.get('.form-image-card').text()).toContain('不要据此重复生图')
|
|
})
|
|
it('镜头修复后清除过期警告,保留明确的当前状态和返回入口', async () => {
|
|
const { keyframes, videos } = await gallery('?sourceShotId=shot-target&subjectFormId=form-special')
|
|
keyframes.items[0]!.primaryKeyframeStale = false
|
|
videos.items[0]!.issues = []
|
|
await wrapper!
|
|
.findAll('button')
|
|
.find(button => button.text() === '刷新图库')!
|
|
.trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper!.find('[data-keyframe-impact]').exists()).toBe(false)
|
|
expect(wrapper!.text()).toContain('未被标记为过期')
|
|
})
|
|
it('无效镜头和已移除形态不发跨项目请求,也不误定位其他素材', async () => {
|
|
const { fetcher } = await gallery('?sourceShotId=foreign-shot&subjectFormId=deleted-form')
|
|
expect(wrapper!.text()).toContain('来源镜头不存在或不属于当前项目')
|
|
expect(wrapper!.get('[data-focused-material]').text()).toContain('指定形态不存在')
|
|
expect(wrapper!.findAll('.form-image-card')).toHaveLength(0)
|
|
expect(fetcher.mock.calls.some(([url]) => String(url).includes('foreign-shot/references'))).toBe(false)
|
|
})
|
|
it('影响查询失败不能当成没有过期首帧,仍然显示已有图库', async () => {
|
|
const { fetcher } = await gallery()
|
|
const original = fetcher.getMockImplementation()!
|
|
fetcher.mockImplementation((url, init) =>
|
|
String(url).includes('/readiness')
|
|
? Promise.resolve(new Response('{}', { status: 500 }))
|
|
: original(url, init)
|
|
)
|
|
await wrapper!
|
|
.findAll('button')
|
|
.find(button => button.text() === '刷新图库')!
|
|
.trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper!.text()).toContain('暂时无法确认有无过期首帧')
|
|
expect(wrapper!.findAll('.form-image-card')).toHaveLength(3)
|
|
})
|
|
it('素材返回生产页时按正式 Shot ID 选择,不误选同编号的另一个 Beat', async () => {
|
|
const context = testProjectContext()
|
|
context.data.value!.checkpoints = [storyboardCheckpoint()]
|
|
const data = directionsResult('capability-test', 2)
|
|
const target = data.beats[1]!.shots[0]!
|
|
const fetcher = vi.fn<typeof fetch>().mockImplementation(async url => {
|
|
const path = String(url)
|
|
return new Response(
|
|
JSON.stringify({
|
|
data: path.includes('/storyboard-directions')
|
|
? data
|
|
: path.includes('/readiness') || path.endsWith('/videos/status')
|
|
? { total: 0, items: [] }
|
|
: []
|
|
})
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetcher)
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [{ path: '/projects/:projectId/production', component: ProductionPage }]
|
|
})
|
|
await router.push({
|
|
path: '/projects/capability-test/production',
|
|
query: { episodeNo: '2', shotId: target.shotId }
|
|
})
|
|
wrapper = mount(ProductionPage, {
|
|
global: { plugins: [router], provide: { [projectContextKey as symbol]: context } }
|
|
})
|
|
await flushPromises()
|
|
expect(wrapper!.get('.production-detail').text()).toContain(target.shotId)
|
|
expect(fetcher.mock.calls.some(([url]) => String(url).includes('episodeNo=2'))).toBe(true)
|
|
expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true)
|
|
})
|
|
})
|