feat: 实现剧本创作与拆解前端工作台
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import CreateProjectDialog from '../projects/components/CreateProjectDialog.vue'
|
||||
import BreakdownPage from '../breakdown/BreakdownPage.vue'
|
||||
import CreateDramaPage from '../create-drama/CreateDramaPage.vue'
|
||||
import ProjectsPage from '../projects/ProjectsPage.vue'
|
||||
import { projectContextKey, type useProjectData } from '../projects/context'
|
||||
import type { ProjectDetail } from '../projects/types'
|
||||
import type { Checkpoint } from './types'
|
||||
|
||||
/** 模拟 API 响应仅用于测试,不会打包进生产页面。 */
|
||||
const fixture: ProjectDetail = {
|
||||
id: 'page-test-project',
|
||||
title: '雨夜来信',
|
||||
topic: '一封信改变了两个人的命运',
|
||||
style: '都市悬疑',
|
||||
status: 'completed',
|
||||
createdAt: '2026-08-27T00:00:00Z',
|
||||
updatedAt: '2026-08-27T00:00:00Z',
|
||||
episodes: [{ episode: 1, title: '来信', content: '<script>不要执行模型内容</script>\n第一场:旧书店。' }],
|
||||
characters: [{ id: 'character', name: '林知夏' }],
|
||||
world: { era: '当代' },
|
||||
reviews: [],
|
||||
tasks: []
|
||||
}
|
||||
let wrapper: VueWrapper | undefined
|
||||
|
||||
/** 使用真实上下文形状,不绕过页面内的异步操作与按钮守卫。 */
|
||||
function context(): ReturnType<typeof useProjectData> {
|
||||
const data = ref({ project: fixture, checkpoints: [] as Checkpoint[] })
|
||||
return {
|
||||
data,
|
||||
project: computed(() => data.value.project),
|
||||
checkpoints: computed(() => data.value.checkpoints),
|
||||
loading: ref(false),
|
||||
error: ref(''),
|
||||
updatedAt: ref(''),
|
||||
refresh: vi.fn<() => Promise<void>>().mockResolvedValue()
|
||||
}
|
||||
}
|
||||
|
||||
/** 找到挂载到 body 的 Reka 弹窗按钮。 */
|
||||
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()
|
||||
})
|
||||
|
||||
describe('工作台页面交互', () => {
|
||||
it('项目筛选无结果时可清除条件并返回真实列表', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn<typeof fetch>().mockImplementation(async () => new Response(JSON.stringify({ data: [fixture] })))
|
||||
)
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [{ path: '/', component: ProjectsPage }]
|
||||
})
|
||||
await router.push('/')
|
||||
wrapper = mount(ProjectsPage, { attachTo: document.body, global: { plugins: [router] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('雨夜来信')
|
||||
await wrapper.get('input[aria-label="搜索项目"]').setValue('不存在的关键词')
|
||||
expect(wrapper.text()).toContain('没有匹配的剧本')
|
||||
button('清除筛选').click()
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('tbody tr')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('新建弹窗提交真实参数并返回 202 的项目 ID', async () => {
|
||||
const fetcher = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValue(new Response('{"projectId":"created","status":"generating"}', { status: 202 }))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
wrapper = mount(CreateProjectDialog, { attachTo: document.body })
|
||||
button('新建剧本').click()
|
||||
await flushPromises()
|
||||
const topic = document.querySelector<HTMLTextAreaElement>('#topic')!
|
||||
topic.value = ' 雨夜来信 '
|
||||
topic.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
const count = document.querySelector<HTMLInputElement>('#episode-count')!
|
||||
count.value = '6'
|
||||
count.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
document.querySelector('form')!.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('created')).toEqual([['created']])
|
||||
expect(JSON.parse(fetcher.mock.calls[0]![1]!.body as string)).toEqual({
|
||||
topic: '雨夜来信',
|
||||
style: '爽文反转',
|
||||
episodeCount: 6
|
||||
})
|
||||
})
|
||||
|
||||
it('修改每组集数会废弃预览,重新预览并确认后才能启动', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async url => {
|
||||
if (String(url).includes('breakdown-preview'))
|
||||
return new Response(
|
||||
'{"data":{"episodeCount":1,"groupCount":1,"estimatedTaskCount":3,"groups":[],"tasks":[],"modules":["character","scene","prop"],"groupSize":2}}'
|
||||
)
|
||||
return new Response('{"data":{"workflowExecution":{"status":"completed"}}}')
|
||||
})
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
const provided = context()
|
||||
wrapper = mount(BreakdownPage, {
|
||||
attachTo: document.body,
|
||||
global: { provide: { [projectContextKey as symbol]: provided } }
|
||||
})
|
||||
button('预览分组').click()
|
||||
await flushPromises()
|
||||
expect(button('开始拆解').disabled).toBe(false)
|
||||
await wrapper.get('#group-size').setValue('2')
|
||||
expect(document.body.textContent).not.toContain('开始拆解')
|
||||
button('预览分组').click()
|
||||
await flushPromises()
|
||||
button('开始拆解').click()
|
||||
await flushPromises()
|
||||
button('确认开始拆解').click()
|
||||
await flushPromises()
|
||||
const post = fetcher.mock.calls.find(call => call[1]?.method === 'POST')
|
||||
expect(post?.[0]).toBe('/api/projects/page-test-project/breakdown/start')
|
||||
expect(JSON.parse(post![1]!.body as string)).toEqual({ groupSize: 2, modules: ['character', 'scene', 'prop'] })
|
||||
expect(provided.refresh).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('剧本文本按纯文本显示,不执行模型输出的 HTML', () => {
|
||||
wrapper = mount(CreateDramaPage, {
|
||||
attachTo: document.body,
|
||||
global: { provide: { [projectContextKey as symbol]: context() }, stubs: { RouterLink: true } }
|
||||
})
|
||||
expect(wrapper.find('.script-body').text()).toContain('<script>不要执行模型内容</script>')
|
||||
expect(wrapper.find('.script-body script').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user