feat: 接入形态图库与 Seedream 生图操作
展示已有主图、候选图和失败记录,支持单图及批量生成、主图选择。 补充正式 ID 校验、费用确认、图片加载占位和回归测试。 依赖后端 eac8dc3 的项目形态图库只读接口。
This commit is contained in:
@@ -19,6 +19,9 @@ import {
|
||||
} from '../storyboard/testing/fixtures'
|
||||
import { getStoryboardSession } from '../storyboard/model'
|
||||
import { getOperation } from './operations'
|
||||
import SubjectImagesPage from '../subject-images/SubjectImagesPage.vue'
|
||||
import { formFixture, imageFixture } from '../subject-images/testing/fixtures'
|
||||
import { getImageSession } from '../subject-images/model'
|
||||
|
||||
/** 模拟 API 响应仅用于测试,不会打包进生产页面。 */
|
||||
const fixture: ProjectDetail = {
|
||||
@@ -81,16 +84,133 @@ function mountStoryboard() {
|
||||
return provided
|
||||
}
|
||||
|
||||
/** 形态图库使用真实路由解析来自主体列表的筛选参数。 */
|
||||
async function mountSubjectImages() {
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [{ path: '/projects/:projectId/:workspace', component: SubjectImagesPage }]
|
||||
})
|
||||
await router.push(`/projects/${fixture.id}/subject-images`)
|
||||
wrapper = mount(SubjectImagesPage, {
|
||||
attachTo: document.body,
|
||||
global: { plugins: [router], provide: { [projectContextKey as symbol]: context() } }
|
||||
})
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
wrapper = undefined
|
||||
document.body.innerHTML = ''
|
||||
vi.unstubAllGlobals()
|
||||
Object.assign(getStoryboardSession(fixture.id), { receipt: null, prompts: {} })
|
||||
getImageSession(fixture.id).receipt = null
|
||||
Object.assign(getOperation(fixture.id), { pending: false, label: '', error: '', notice: '' })
|
||||
})
|
||||
|
||||
describe('工作台页面交互', () => {
|
||||
it('形态图库自动显示后端已有图片,不需要再次生图;筛选只改变显示', async () => {
|
||||
const fetcher = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockImplementation(async () => new Response(JSON.stringify({ data: [formFixture()] })))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await mountSubjectImages()
|
||||
expect(wrapper!.get('.form-image-card img').attributes('src')).toContain(
|
||||
'/storage/subjects/project/form/image.png'
|
||||
)
|
||||
expect(wrapper!.text()).toContain('林知夏')
|
||||
expect(wrapper!.text()).toContain('主参考图')
|
||||
expect(fetcher.mock.calls.map(([url, init]) => [url, init?.method])).toEqual([
|
||||
['/api/projects/page-test-project/subject-forms', 'GET']
|
||||
])
|
||||
await wrapper!.get('[aria-label="搜索形态图片"]').setValue('不存在的形态')
|
||||
expect(wrapper!.text()).toContain('没有匹配的形态')
|
||||
button('清除筛选').click()
|
||||
await flushPromises()
|
||||
expect(wrapper!.findAll('.form-image-card')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('形态单图确认后发送正式 ID 并刷新候选图,不会调用 production/start', async () => {
|
||||
const form = formFixture()
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async (_url, init) => {
|
||||
if (init?.method === 'POST') {
|
||||
const candidate = imageFixture({ id: 'new-candidate', isPrimary: false, imageUrl: '/storage/new.png' })
|
||||
form.images.unshift(candidate)
|
||||
return new Response(JSON.stringify({ data: candidate }))
|
||||
}
|
||||
return new Response(JSON.stringify({ data: [form] }))
|
||||
})
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await mountSubjectImages()
|
||||
button('再生成一张').click()
|
||||
await flushPromises()
|
||||
document.querySelector<HTMLInputElement>('#confirm-image-cost')!.click()
|
||||
await flushPromises()
|
||||
document.querySelector('form')!.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
||||
await flushPromises()
|
||||
const post = fetcher.mock.calls.find(([, init]) => init?.method === 'POST')!
|
||||
expect(post[0]).toBe('/api/subject-forms/form-db-1/images')
|
||||
expect(JSON.parse(post[1]!.body as string)).toEqual({ provider: 'seedream', setPrimary: false })
|
||||
expect(wrapper!.text()).toContain('2 条记录')
|
||||
expect(wrapper!.get('.form-image-card img').attributes('src')).not.toContain('/storage/new.png')
|
||||
expect(fetcher.mock.calls.some(([url]) => String(url).includes('production/start'))).toBe(false)
|
||||
})
|
||||
|
||||
it('批量生图保留部分失败回执,默认跳过有主图项,筛选不会成为批量参数', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(
|
||||
async (_url, init) =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
data:
|
||||
init?.method === 'POST'
|
||||
? {
|
||||
total: 2,
|
||||
targetCount: 1,
|
||||
generated: 0,
|
||||
skipped: 1,
|
||||
failed: 1,
|
||||
failures: [{ subjectFormId: 'form-db-2', error: '服务暂不可用' }]
|
||||
}
|
||||
: [formFixture()]
|
||||
})
|
||||
)
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await mountSubjectImages()
|
||||
await wrapper!.get('[aria-label="搜索形态图片"]').setValue('@CH0001')
|
||||
await confirmGeneration('补齐项目主参考图')
|
||||
expect(wrapper!.get('[aria-label="生图批量回执"]').text()).toContain('部分形态生图失败')
|
||||
expect(wrapper!.text()).toContain('服务暂不可用')
|
||||
const post = fetcher.mock.calls.find(([, init]) => init?.method === 'POST')!
|
||||
expect(JSON.parse(post[1]!.body as string)).toEqual({ provider: 'seedream', concurrency: 2, force: false })
|
||||
await wrapper!.get('.panel details input[type="checkbox"]').setValue(true)
|
||||
await confirmGeneration('为全项目新增候选图')
|
||||
const lastPost = fetcher.mock.calls.filter(([, init]) => init?.method === 'POST').at(-1)!
|
||||
expect(JSON.parse(lastPost[1]!.body as string).force).toBe(true)
|
||||
})
|
||||
|
||||
it('后端持久化生成中或查询失败时暂停生图,旧图库仍可查看', async () => {
|
||||
let fail = false
|
||||
const form = formFixture()
|
||||
form.images.unshift(imageFixture({ id: 'running', isPrimary: false, status: 'generating', imageUrl: null }))
|
||||
const fetcher = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockImplementation(async () =>
|
||||
fail
|
||||
? new Response('{"message":"数据库异常"}', { status: 500 })
|
||||
: new Response(JSON.stringify({ data: [form] }))
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await mountSubjectImages()
|
||||
expect(button('再生成一张').disabled).toBe(true)
|
||||
expect(wrapper!.text()).toContain('数据库中仍有排队或生成中的图片')
|
||||
fail = true
|
||||
button('刷新图库').click()
|
||||
await flushPromises()
|
||||
expect(wrapper!.text()).toContain('数据库异常')
|
||||
expect(wrapper!.find('.form-image-card img').exists()).toBe(true)
|
||||
expect(button('补齐项目主参考图').disabled).toBe(true)
|
||||
})
|
||||
it('参考图拦截危险地址,生成规格只通过 GET 读取正式形态和状态', async () => {
|
||||
const shot = designedShot()
|
||||
const fetcher = vi
|
||||
|
||||
Reference in New Issue
Block a user