展示已有主图、候选图和失败记录,支持单图及批量生成、主图选择。 补充正式 ID 校验、费用确认、图片加载占位和回归测试。 依赖后端 eac8dc3 的项目形态图库只读接口。
173 lines
8.0 KiB
TypeScript
173 lines
8.0 KiB
TypeScript
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { AssetImage } from '../../components/ui'
|
|
import { getOperation } from '../workflows/operations'
|
|
import GenerateImageDialog from './components/GenerateImageDialog.vue'
|
|
import ImageGalleryDialog from './components/ImageGalleryDialog.vue'
|
|
import { coverImage, hasRunningImages, primaryImage, validImageSize } from './model'
|
|
import { formFixture, imageFixture } from './testing/fixtures'
|
|
|
|
let wrapper: VueWrapper | undefined
|
|
/** Reka 将弹窗挂载到 body,需要从实际弹窗找到按钮。 */
|
|
function button(label: string): HTMLButtonElement {
|
|
const element = [...document.querySelectorAll('button')].find(item => item.textContent?.trim() === label)
|
|
if (!element) throw new Error('找不到按钮:' + label)
|
|
return element
|
|
}
|
|
afterEach(() => {
|
|
wrapper?.unmount()
|
|
wrapper = undefined
|
|
document.body.innerHTML = ''
|
|
vi.unstubAllGlobals()
|
|
Object.assign(getOperation('gallery-test'), { pending: false, label: '', error: '', notice: '' })
|
|
})
|
|
|
|
describe('形态图片选择与操作', () => {
|
|
it('已有主图优先于新候选图和失败记录,无主图才回退到成功候选图', () => {
|
|
const form = formFixture()
|
|
form.images.unshift(
|
|
imageFixture({ id: 'failed', isPrimary: false, status: 'failed', imageUrl: null }),
|
|
imageFixture({ id: 'candidate', isPrimary: false })
|
|
)
|
|
expect(coverImage(form)?.id).toBe('image-db-1')
|
|
form.images.pop()
|
|
expect(primaryImage(form.images)).toBeUndefined()
|
|
expect(coverImage(form)?.id).toBe('candidate')
|
|
expect(hasRunningImages(form.images)).toBe(false)
|
|
form.images.push(imageFixture({ status: 'generating', isPrimary: false }))
|
|
expect(hasRunningImages(form.images)).toBe(true)
|
|
})
|
|
|
|
it('尺寸可同时留空,但不接受单边、零、负数或非整数', () => {
|
|
expect(validImageSize('', '')).toBe(true)
|
|
expect(validImageSize(2048, 2048)).toBe(true)
|
|
for (const [width, height] of [
|
|
[1024, ''],
|
|
['', 1024],
|
|
[0, 1024],
|
|
[-1, 1024],
|
|
[10.5, 1024]
|
|
] as const)
|
|
expect(validImageSize(width, height)).toBe(false)
|
|
})
|
|
|
|
it('生图确认发送 Seedream 与正式形态 ID,默认不替换已有主图', async () => {
|
|
wrapper = mount(GenerateImageDialog, {
|
|
attachTo: document.body,
|
|
props: { open: true, form: formFixture(), disabled: false }
|
|
})
|
|
await flushPromises()
|
|
expect(button('确认生成图片').disabled).toBe(true)
|
|
document.querySelector<HTMLInputElement>('#confirm-image-cost')!.click()
|
|
await flushPromises()
|
|
button('确认生成图片').click()
|
|
await flushPromises()
|
|
expect(wrapper.emitted('generate')).toEqual([['form-db-1', { provider: 'seedream', setPrimary: false }]])
|
|
})
|
|
|
|
it('新形态默认设主图,填写一侧尺寸时不能提交,切换形态清空自定义提示词', async () => {
|
|
const form = formFixture()
|
|
form.images = []
|
|
wrapper = mount(GenerateImageDialog, { attachTo: document.body, props: { open: true, form, disabled: false } })
|
|
await flushPromises()
|
|
const width = document.querySelector<HTMLInputElement>('#image-width')!
|
|
width.value = '2048'
|
|
width.dispatchEvent(new Event('input', { bubbles: true }))
|
|
document.querySelector<HTMLInputElement>('#confirm-image-cost')!.click()
|
|
await flushPromises()
|
|
expect(button('确认生成图片').disabled).toBe(true)
|
|
await wrapper.setProps({ form: { ...form, id: 'form-db-2' } })
|
|
await flushPromises()
|
|
expect(width.value).toBe('')
|
|
expect(document.querySelector<HTMLInputElement>('#confirm-image-cost')!.checked).toBe(false)
|
|
document.querySelector<HTMLInputElement>('#confirm-image-cost')!.click()
|
|
await flushPromises()
|
|
button('确认生成图片').click()
|
|
await flushPromises()
|
|
expect(wrapper.emitted('generate')).toEqual([['form-db-2', { provider: 'seedream', setPrimary: true }]])
|
|
})
|
|
|
|
it('图库不自动生图,主图切换经确认后 PUT,失败图片不能设主图', async () => {
|
|
let primary = false
|
|
const fetcher = vi.fn<typeof fetch>().mockImplementation(async (_url, init) => {
|
|
if (init?.method === 'PUT') {
|
|
primary = true
|
|
return new Response(JSON.stringify({ data: imageFixture() }))
|
|
}
|
|
return new Response(
|
|
JSON.stringify({
|
|
data: [
|
|
imageFixture({ isPrimary: primary }),
|
|
imageFixture({
|
|
id: 'failed',
|
|
isPrimary: false,
|
|
status: 'failed',
|
|
imageUrl: null,
|
|
error: '供应商拒绝请求'
|
|
})
|
|
]
|
|
})
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetcher)
|
|
wrapper = mount(ImageGalleryDialog, {
|
|
attachTo: document.body,
|
|
props: { projectId: 'gallery-test', form: formFixture('gallery-test'), open: true, disabled: false }
|
|
})
|
|
await flushPromises()
|
|
expect(fetcher.mock.calls).toHaveLength(1)
|
|
expect(document.body.textContent).toContain('<script>模型提示词</script>')
|
|
expect(document.querySelector('[role="dialog"] script')).toBeNull()
|
|
button('设为主参考图').click()
|
|
await flushPromises()
|
|
expect(fetcher.mock.calls).toHaveLength(1)
|
|
button('确认切换主图').click()
|
|
await flushPromises()
|
|
expect(fetcher.mock.calls.find(([, init]) => init?.method === 'PUT')?.[0]).toBe(
|
|
'/api/subject-forms/form-db-1/images/image-db-1/primary'
|
|
)
|
|
expect(wrapper.emitted('changed')).toHaveLength(1)
|
|
document.querySelector<HTMLButtonElement>('[aria-label="查看图片 failed"]')!.click()
|
|
await flushPromises()
|
|
expect(button('设为主参考图').disabled).toBe(true)
|
|
expect(document.body.textContent).toContain('供应商拒绝请求')
|
|
})
|
|
|
|
it('关闭图片弹窗取消查询,迟到的旧形态响应不会污染再次打开的形态', async () => {
|
|
let finish!: (response: Response) => void
|
|
const fetcher = vi
|
|
.fn<typeof fetch>()
|
|
.mockImplementationOnce(
|
|
() =>
|
|
new Promise(resolve => {
|
|
finish = resolve
|
|
})
|
|
)
|
|
.mockImplementation(async () => new Response('{"data":[]}'))
|
|
vi.stubGlobal('fetch', fetcher)
|
|
wrapper = mount(ImageGalleryDialog, {
|
|
attachTo: document.body,
|
|
props: { projectId: 'gallery-test', form: formFixture('gallery-test'), open: true, disabled: false }
|
|
})
|
|
await flushPromises()
|
|
await wrapper.setProps({ open: false })
|
|
expect(fetcher.mock.calls[0]?.[1]?.signal?.aborted).toBe(true)
|
|
await wrapper.setProps({ open: true, form: { ...formFixture('gallery-test'), id: 'form-db-2' } })
|
|
await flushPromises()
|
|
finish(new Response(JSON.stringify({ data: [imageFixture()] })))
|
|
await flushPromises()
|
|
expect(document.body.textContent).not.toContain('Image ID · image-db-1')
|
|
expect(document.body.textContent).toContain('此形态尚无图片记录')
|
|
})
|
|
|
|
it('图片加载失败有占位,地址变化可恢复,危险协议不会写入 img', async () => {
|
|
wrapper = mount(AssetImage, { props: { src: '/storage/a.png', alt: '形态主图' } })
|
|
await wrapper.get('img').trigger('error')
|
|
expect(wrapper.text()).toContain('图片无法加载')
|
|
await wrapper.setProps({ src: '/storage/b.png' })
|
|
expect(wrapper.get('img').attributes('src')).toContain('/storage/b.png')
|
|
await wrapper.setProps({ src: 'javascript:alert(1)' })
|
|
expect(wrapper.find('img').exists()).toBe(false)
|
|
})
|
|
})
|