188 lines
8.7 KiB
TypeScript
188 lines
8.7 KiB
TypeScript
import { defineComponent } from 'vue'
|
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { projectContextKey } from '@/features/projects/context'
|
|
import { testProjectContext } from '@/testing/project-context'
|
|
import { getOperation } from '@/features/workflows/operations'
|
|
import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures'
|
|
import { currentIdentityAnchorId, getImageSession, isPrimaryIdentityStale } from '@/features/subject-images/model'
|
|
import { useSubjectImages } from '@/features/subject-images/useSubjectImages'
|
|
import FormPromptDialog from '@/features/subject-images/components/FormPromptDialog.vue'
|
|
import GenerateImageDialog from '@/features/subject-images/components/GenerateImageDialog.vue'
|
|
|
|
let wrapper: VueWrapper | undefined
|
|
const projectId = 'capability-test'
|
|
/** 从实际挂载的确认弹窗查找操作按钮。 */
|
|
function find(label: string) {
|
|
return [...document.querySelectorAll('button')].find(item => item.textContent?.trim() === label)!
|
|
}
|
|
afterEach(() => {
|
|
wrapper?.unmount()
|
|
wrapper = undefined
|
|
document.body.innerHTML = ''
|
|
vi.unstubAllGlobals()
|
|
Object.assign(getOperation(projectId), { pending: false, error: '', notice: '', label: '' })
|
|
Object.assign(getImageSession(projectId), { receipt: null, promptReceipt: null })
|
|
})
|
|
|
|
async function setup() {
|
|
let service!: ReturnType<typeof useSubjectImages>
|
|
const context = testProjectContext()
|
|
const form = formFixture(projectId)
|
|
const fetcher = vi.fn<typeof fetch>().mockImplementation(async (url, init) => {
|
|
const path = String(url)
|
|
let data: unknown = [form]
|
|
if (init?.method === 'POST') {
|
|
if (path.endsWith('/generation-prompts'))
|
|
data = {
|
|
total: 2,
|
|
targetCount: 2,
|
|
generated: 1,
|
|
skipped: 0,
|
|
failed: 1,
|
|
failures: [{ subjectFormId: 'form-failed', error: '模型拒绝' }]
|
|
}
|
|
else if (path.endsWith('/generation-prompt')) {
|
|
form.generationPrompt = '正式提示词'
|
|
data = { id: form.id, subjectId: form.subjectId, generationPrompt: form.generationPrompt }
|
|
} else if (path.includes('/subject-forms/')) data = imageFixture()
|
|
else
|
|
data = {
|
|
total: 3,
|
|
targetCount: 1,
|
|
generated: 1,
|
|
skipped: 0,
|
|
failed: 0,
|
|
failures: [],
|
|
eligibleCount: 3,
|
|
remaining: 2
|
|
}
|
|
}
|
|
return new Response(JSON.stringify({ data }))
|
|
})
|
|
vi.stubGlobal('fetch', fetcher)
|
|
wrapper = mount(
|
|
defineComponent({
|
|
setup() {
|
|
service = useSubjectImages()
|
|
return () => null
|
|
}
|
|
}),
|
|
{ global: { provide: { [projectContextKey as symbol]: context } } }
|
|
)
|
|
await flushPromises()
|
|
return {
|
|
service,
|
|
context,
|
|
form,
|
|
fetcher,
|
|
posts: () => fetcher.mock.calls.filter(([, init]) => init?.method === 'POST')
|
|
}
|
|
}
|
|
|
|
describe('形态正式提示词与批量配置', () => {
|
|
it('单个使用正式形态 ID 和 force,生成提示词不会调用图片接口', async () => {
|
|
const { service, posts } = await setup()
|
|
await service.generatePrompt('unknown-form', false)
|
|
expect(posts()).toHaveLength(0)
|
|
await service.generatePrompt('form-db-1', false)
|
|
expect(posts()).toHaveLength(1)
|
|
expect(posts()[0]?.[0]).toBe('/api/subject-forms/form-db-1/generation-prompt')
|
|
expect(JSON.parse(String(posts()[0]?.[1]?.body))).toEqual({ force: false })
|
|
expect(service.forms.value[0]?.generationPrompt).toBe('正式提示词')
|
|
await service.generatePrompt('form-db-1', true)
|
|
expect(JSON.parse(String(posts()[1]?.[1]?.body))).toEqual({ force: true })
|
|
})
|
|
it('提示词接口返回空正文时显示失败,不误报保存成功', async () => {
|
|
const { service, fetcher } = await setup()
|
|
fetcher.mockResolvedValueOnce(
|
|
new Response(JSON.stringify({ data: { id: 'form-db-1', subjectId: 'subject-db-1', generationPrompt: '' } }))
|
|
)
|
|
await service.generatePrompt('form-db-1', false)
|
|
expect(getOperation(projectId).error).toContain('未确认正式提示词已保存')
|
|
})
|
|
it('批量提示词保留部分失败回执,不覆盖图片回执且不传图片上限', async () => {
|
|
const { service, posts } = await setup()
|
|
service.limit.value = 1
|
|
service.promptConcurrency.value = 4
|
|
service.promptForce.value = true
|
|
await service.generatePrompts()
|
|
expect(posts()[0]?.[0]).toBe('/api/projects/capability-test/subject-forms/generation-prompts')
|
|
expect(JSON.parse(String(posts()[0]?.[1]?.body))).toEqual({ concurrency: 4, force: true })
|
|
expect(service.session.value.promptReceipt?.result.failures[0]?.error).toBe('模型拒绝')
|
|
expect(service.session.value.receipt).toBeNull()
|
|
})
|
|
it('图片数量上限可选,非法上限和并发阻止提交', async () => {
|
|
const { service, posts } = await setup()
|
|
for (const limit of [0, -1, 1.5]) {
|
|
service.limit.value = limit
|
|
await service.generateProject()
|
|
}
|
|
expect(posts()).toHaveLength(0)
|
|
service.limit.value = 1
|
|
await service.generateProject()
|
|
expect(JSON.parse(String(posts()[0]?.[1]?.body))).toEqual({
|
|
concurrency: 2,
|
|
force: false,
|
|
limit: 1
|
|
})
|
|
expect(service.session.value.receipt?.result.remaining).toBe(2)
|
|
service.limit.value = ''
|
|
await service.generateProject()
|
|
expect(JSON.parse(String(posts()[1]?.[1]?.body))).not.toHaveProperty('limit')
|
|
service.promptConcurrency.value = 0
|
|
await service.generatePrompts()
|
|
expect(posts()).toHaveLength(2)
|
|
})
|
|
it.each(['draft', 'generating', 'need_review', 'failed'] as const)('%s 剧本不能生成提示词和图片', async status => {
|
|
const { service, context, posts } = await setup()
|
|
context.data.value!.project.status = status
|
|
await service.generatePrompt('form-db-1', true)
|
|
await service.generatePrompts()
|
|
await service.generateProject()
|
|
expect(posts()).toHaveLength(0)
|
|
})
|
|
it.each(['character', 'scene', 'prop'])('%s 已锁定母版才参与过期判断,刷新只新增候选', async module => {
|
|
const { service, form, posts } = await setup()
|
|
form.subject.module = module
|
|
form.subject.identity = { id: 'identity', isLocked: false, images: [{ id: 'anchor-new' }] }
|
|
expect(currentIdentityAnchorId(form)).toBeUndefined()
|
|
expect(isPrimaryIdentityStale(form)).toBe(false)
|
|
form.subject.identity.isLocked = true
|
|
expect(isPrimaryIdentityStale(form)).toBe(true)
|
|
await service.query.refresh()
|
|
await service.generateStale()
|
|
expect(JSON.parse(String(posts()[0]?.[1]?.body))).toEqual({ setPrimary: false })
|
|
})
|
|
it('正式提示词确认必须勾选,未确认不发送事件', async () => {
|
|
const form = formFixture()
|
|
wrapper = mount(FormPromptDialog, { attachTo: document.body, props: { open: true, disabled: false, form } })
|
|
await flushPromises()
|
|
find('生成正式提示词').click()
|
|
await flushPromises()
|
|
expect(find('确认生成正式提示词').disabled).toBe(true)
|
|
expect(wrapper.emitted('generate')).toBeUndefined()
|
|
document.querySelector<HTMLElement>('.app-dialog [role="checkbox"]')!.click()
|
|
await flushPromises()
|
|
find('确认生成正式提示词').click()
|
|
await flushPromises()
|
|
expect(wrapper.emitted('generate')).toEqual([['form-db-1', false]])
|
|
})
|
|
it('缺少正式和原始提示词仍可确认生图,由后端补齐而非前端编造', async () => {
|
|
const form = formFixture()
|
|
form.appearancePrompt = null
|
|
form.generationPrompt = null
|
|
wrapper = mount(GenerateImageDialog, { attachTo: document.body, props: { open: true, disabled: false, form } })
|
|
await flushPromises()
|
|
document.querySelector<HTMLElement>('#confirm-image-cost')!.click()
|
|
await flushPromises()
|
|
const submit = [...document.querySelectorAll('button')].find(
|
|
item => item.textContent?.trim() === '确认生成图片'
|
|
)!
|
|
expect(submit.disabled).toBe(false)
|
|
submit.click()
|
|
await flushPromises()
|
|
expect(wrapper.emitted('generate')).toEqual([['form-db-1', { setPrimary: false }]])
|
|
})
|
|
})
|