import { generateScriptStream, applyScriptToShot, getProjectScript } from '../../../api/video_flow'; import { ScriptEditUseCase } from '../usecase/ScriptEditUseCase'; import { ScriptSlice, ScriptSliceType } from '../domain/valueObject'; // Mock API模块 jest.mock('@/api/video_flow', () => ({ generateScriptStream: jest.fn(), applyScriptToShot: jest.fn(), getProjectScript: jest.fn(), })); // Mock UseCase模块 jest.mock('../usecase/ScriptEditUseCase'); describe('ScriptService 业务逻辑测试', () => { let mockScriptEditUseCase: jest.Mocked; // 测试数据 const mockScriptSlices: ScriptSlice[] = [ { id: 'slice1', type: ScriptSliceType.role, text: '你好,我是主角', metaData: { speaker: '主角', emotion: '友好' } }, { id: 'slice2', type: ScriptSliceType.scene, text: '场景:阳光明媚的公园', metaData: { location: '公园', weather: '晴天' } }, { id: 'slice3', type: ScriptSliceType.text, text: '这是一个温馨的故事', metaData: { style: '温馨' } } ]; const mockStreamData = [ { id: 'slice1', type: 'role', text: '你好,我是主角', metaData: { speaker: '主角' } }, { id: 'slice2', type: 'scene', text: '场景:阳光明媚的公园', metaData: { location: '公园' } }, { id: 'slice3', type: 'text', text: '这是一个温馨的故事', metaData: { style: '温馨' } } ]; beforeEach(() => { jest.clearAllMocks(); // 设置Mock UseCase实例 mockScriptEditUseCase = { generateScript: jest.fn(), applyScript: jest.fn(), updateScriptSlice: jest.fn(), getScriptSlices: jest.fn(), toString: jest.fn(), } as any; // 设置Mock构造函数 (ScriptEditUseCase as jest.MockedClass).mockImplementation(() => mockScriptEditUseCase); // 设置默认的Mock返回值 mockScriptEditUseCase.getScriptSlices.mockReturnValue(mockScriptSlices); mockScriptEditUseCase.toString.mockReturnValue('你好,我是主角\n场景:阳光明媚的公园\n这是一个温馨的故事'); mockScriptEditUseCase.updateScriptSlice.mockReturnValue(true); mockScriptEditUseCase.generateScript.mockResolvedValue(); mockScriptEditUseCase.applyScript.mockResolvedValue(); // Mock流式数据 (generateScriptStream as jest.Mock).mockResolvedValue(mockStreamData); (applyScriptToShot as jest.Mock).mockResolvedValue({ successful: true, data: { success: true }, }); (getProjectScript as jest.Mock).mockResolvedValue({ successful: true, data: { prompt: '创建一个关于友谊的温馨故事', scriptText: '你好,我是主角\n场景:阳光明媚的公园\n这是一个温馨的故事' } }); }); describe('剧本业务流程测试', () => { it('应该完成完整的剧本编辑流程:根据提示词生成剧本→修改提示词重新生成→重置提示词重新生成→修改剧本→应用剧本', async () => { // 1. 用户操作:根据提示词生成新剧本 const initialPrompt = '创建一个关于友谊的温馨故事'; // 模拟AI生成剧本的流式数据 const mockStream = (async function* () { for (const data of mockStreamData) { yield data; } })(); (generateScriptStream as jest.Mock).mockResolvedValue(mockStream); mockScriptEditUseCase.generateScript.mockImplementation(async (prompt) => { // 模拟流式处理 for await (const chunk of mockStream) { // 这里可以模拟逐步添加剧本片段 } }); // 创建剧本编辑用例并生成剧本 const scriptEditUseCase = new ScriptEditUseCase(''); await scriptEditUseCase.generateScript(initialPrompt); expect(ScriptEditUseCase).toHaveBeenCalledWith(''); expect(mockScriptEditUseCase.generateScript).toHaveBeenCalledWith(initialPrompt); // 验证生成的剧本内容 const generatedScript = scriptEditUseCase.toString(); expect(generatedScript).toContain('你好,我是主角'); expect(generatedScript).toContain('场景:阳光明媚的公园'); // 验证剧本片段 const scriptSlices = scriptEditUseCase.getScriptSlices(); expect(scriptSlices).toHaveLength(3); expect(scriptSlices[0].type).toBe(ScriptSliceType.role); expect(scriptSlices[1].type).toBe(ScriptSliceType.scene); // 2. 用户操作:修改提示词重新生成剧本 const modifiedPrompt = '创建一个关于冒险的刺激故事'; // 模拟修改后的剧本数据 const modifiedScriptSlices: ScriptSlice[] = [ { id: 'slice1', type: ScriptSliceType.role, text: '我们出发吧!', metaData: { speaker: '冒险者', emotion: '兴奋' } }, { id: 'slice2', type: ScriptSliceType.scene, text: '场景:神秘的森林', metaData: { location: '森林', atmosphere: '神秘' } }, { id: 'slice3', type: ScriptSliceType.text, text: '这是一个充满挑战的冒险', metaData: { style: '冒险' } } ]; mockScriptEditUseCase.getScriptSlices.mockReturnValue(modifiedScriptSlices); mockScriptEditUseCase.toString.mockReturnValue('我们出发吧!\n场景:神秘的森林\n这是一个充满挑战的冒险'); // 重新生成剧本 await scriptEditUseCase.generateScript(modifiedPrompt); expect(mockScriptEditUseCase.generateScript).toHaveBeenCalledWith(modifiedPrompt); // 验证修改后的剧本内容 const modifiedScript = scriptEditUseCase.toString(); expect(modifiedScript).toContain('我们出发吧!'); expect(modifiedScript).toContain('神秘的森林'); expect(modifiedScript).toContain('充满挑战的冒险'); // 3. 用户操作:重置提示词重新生成剧本 const resetPrompt = '创建一个关于友谊的温馨故事'; // 回到初始提示词 // 模拟重置后的剧本数据(回到初始状态) mockScriptEditUseCase.getScriptSlices.mockReturnValue(mockScriptSlices); mockScriptEditUseCase.toString.mockReturnValue('你好,我是主角\n场景:阳光明媚的公园\n这是一个温馨的故事'); // 重置并重新生成剧本 await scriptEditUseCase.generateScript(resetPrompt); expect(mockScriptEditUseCase.generateScript).toHaveBeenCalledWith(resetPrompt); // 验证重置后的剧本内容 const resetScript = scriptEditUseCase.toString(); expect(resetScript).toContain('你好,我是主角'); expect(resetScript).toContain('阳光明媚的公园'); expect(resetScript).toContain('温馨的故事'); // 4. 用户操作:修改剧本片段 const updatedText = '你好,我是勇敢的主角'; const updatedMetaData = { speaker: '主角', emotion: '勇敢' }; // 修改第一个剧本片段 const updateSuccess = scriptEditUseCase.updateScriptSlice('slice1', updatedText, updatedMetaData); expect(updateSuccess).toBe(true); expect(mockScriptEditUseCase.updateScriptSlice).toHaveBeenCalledWith('slice1', updatedText, updatedMetaData); // 验证修改后的片段 const updatedSlices = scriptEditUseCase.getScriptSlices(); // 注意:mock返回的数据不会因为updateScriptSlice调用而改变 // 这里只是验证getScriptSlices被正确调用 expect(updatedSlices).toEqual(mockScriptSlices); // 5. 用户操作:应用剧本到分镜 // 应用剧本 await scriptEditUseCase.applyScript(); expect(mockScriptEditUseCase.applyScript).toHaveBeenCalled(); // 验证API调用 expect(applyScriptToShot).toHaveBeenCalledWith({ scriptSlices: updatedSlices }); }); it('应该验证剧本生成的不同提示词效果', async () => { const scriptEditUseCase = new ScriptEditUseCase(''); // 测试不同类型的提示词 const testPrompts = [ '创建一个浪漫的爱情故事', '创建一个悬疑的侦探故事', '创建一个科幻的未来故事' ]; for (const prompt of testPrompts) { await scriptEditUseCase.generateScript(prompt); expect(mockScriptEditUseCase.generateScript).toHaveBeenCalledWith(prompt); } // 验证每个提示词都被正确处理 expect(mockScriptEditUseCase.generateScript).toHaveBeenCalledTimes(testPrompts.length); }); it('应该验证剧本片段的修改和更新', async () => { const scriptEditUseCase = new ScriptEditUseCase(''); await scriptEditUseCase.generateScript('测试剧本'); // 测试修改不同类型的剧本片段 const testUpdates = [ { id: 'slice1', text: '修改后的对话', metaData: { speaker: '新角色', emotion: '愤怒' } }, { id: 'slice2', text: '修改后的场景描述', metaData: { location: '新地点', weather: '雨天' } }, { id: 'slice3', text: '修改后的文本内容', metaData: { style: '新风格', tone: '严肃' } } ]; for (const update of testUpdates) { const success = scriptEditUseCase.updateScriptSlice(update.id, update.text, update.metaData); expect(success).toBe(true); expect(mockScriptEditUseCase.updateScriptSlice).toHaveBeenCalledWith( update.id, update.text, update.metaData ); } // 验证所有修改都被正确处理 expect(mockScriptEditUseCase.updateScriptSlice).toHaveBeenCalledTimes(testUpdates.length); }); it('应该处理剧本编辑流程中的错误情况', async () => { const scriptEditUseCase = new ScriptEditUseCase(''); // 模拟生成剧本失败 mockScriptEditUseCase.generateScript.mockRejectedValue(new Error('AI生成失败')); await expect(scriptEditUseCase.generateScript('测试提示词')).rejects.toThrow('AI生成失败'); // 模拟更新剧本片段失败 mockScriptEditUseCase.updateScriptSlice.mockReturnValue(false); const updateResult = scriptEditUseCase.updateScriptSlice('nonexistent', '新文本'); expect(updateResult).toBe(false); // 模拟应用剧本失败 mockScriptEditUseCase.applyScript.mockRejectedValue(new Error('应用剧本失败')); await expect(scriptEditUseCase.applyScript()).rejects.toThrow('应用剧本失败'); }); }); describe('剧本流式生成测试', () => { it('应该正确处理流式剧本生成', async () => { const scriptEditUseCase = new ScriptEditUseCase(''); // 模拟流式数据生成 const streamChunks = [ { id: 'slice1', type: 'role', text: '第一段对话', metaData: { speaker: '角色1' } }, { id: 'slice2', type: 'scene', text: '场景描述', metaData: { location: '地点1' } }, { id: 'slice3', type: 'text', text: '叙述文本', metaData: { style: '风格1' } } ]; let chunkIndex = 0; const mockStream = (async function* () { for (const chunk of streamChunks) { yield chunk; chunkIndex++; } })(); (generateScriptStream as jest.Mock).mockResolvedValue(mockStream); // 模拟流式处理 mockScriptEditUseCase.generateScript.mockImplementation(async (prompt) => { for await (const chunk of mockStream) { // 模拟逐步处理每个片段 // 注意:这里不需要验证chunk,因为mockStream已经包含了正确的数据 } }); await scriptEditUseCase.generateScript('流式生成测试'); expect(mockScriptEditUseCase.generateScript).toHaveBeenCalledWith('流式生成测试'); expect(chunkIndex).toBe(streamChunks.length); }); it('应该处理流式生成中的错误', async () => { const scriptEditUseCase = new ScriptEditUseCase(''); // 模拟流式数据生成失败 const mockErrorStream = (async function* () { throw new Error('流式生成错误'); })(); (generateScriptStream as jest.Mock).mockResolvedValue(mockErrorStream); mockScriptEditUseCase.generateScript.mockImplementation(async () => { for await (const chunk of mockErrorStream) { // 这里不会执行,因为流会抛出错误 } }); await expect(scriptEditUseCase.generateScript('错误测试')).rejects.toThrow('流式生成错误'); }); }); describe('剧本应用测试', () => { it('应该成功应用剧本到分镜', async () => { const scriptEditUseCase = new ScriptEditUseCase(''); // 先生成剧本 await scriptEditUseCase.generateScript('应用测试'); // 应用剧本 await scriptEditUseCase.applyScript(); expect(mockScriptEditUseCase.applyScript).toHaveBeenCalled(); }); it('应该处理应用剧本失败的情况', async () => { const scriptEditUseCase = new ScriptEditUseCase(''); // 模拟应用失败 (applyScriptToShot as jest.Mock).mockResolvedValue({ successful: false, message: '应用剧本失败', }); mockScriptEditUseCase.applyScript.mockRejectedValue(new Error('应用失败')); await expect(scriptEditUseCase.applyScript()).rejects.toThrow('应用失败'); }); }); describe('剧本状态管理测试', () => { it('应该正确管理剧本的生成状态', async () => { const scriptEditUseCase = new ScriptEditUseCase(''); // 设置初始状态为空数组 mockScriptEditUseCase.getScriptSlices.mockReturnValue([]); // 初始状态应该是空的 const initialSlices = scriptEditUseCase.getScriptSlices(); expect(initialSlices).toEqual([]); // 生成剧本后应该有内容 await scriptEditUseCase.generateScript('状态测试'); // 重新设置mock返回值为有内容的数组 mockScriptEditUseCase.getScriptSlices.mockReturnValue(mockScriptSlices); const generatedSlices = scriptEditUseCase.getScriptSlices(); expect(generatedSlices).toEqual(mockScriptSlices); // 修改后状态应该更新 scriptEditUseCase.updateScriptSlice('slice1', '新文本', undefined); expect(mockScriptEditUseCase.updateScriptSlice).toHaveBeenCalledWith('slice1', '新文本', undefined); }); it('应该验证剧本文本的完整性', async () => { const scriptEditUseCase = new ScriptEditUseCase(''); await scriptEditUseCase.generateScript('完整性测试'); const scriptText = scriptEditUseCase.toString(); const scriptSlices = scriptEditUseCase.getScriptSlices(); // 验证文本包含所有片段的内容 for (const slice of scriptSlices) { expect(scriptText).toContain(slice.text); } // 验证片段数量与文本行数匹配 const textLines = scriptText.split('\n').filter(line => line.trim()); expect(textLines.length).toBeGreaterThan(0); }); it('应该根据项目ID获取已存在的剧本数据', async () => { const projectId = 'test-project-123'; // 模拟API返回项目剧本数据(只包含提示词和剧本文本) const mockProjectScriptData = { prompt: '创建一个关于冒险的故事', scriptText: '我们出发吧!\n场景:神秘的森林\n这是一个充满挑战的冒险' }; (getProjectScript as jest.Mock).mockResolvedValue({ successful: true, data: mockProjectScriptData }); // 创建剧本编辑用例并模拟解析逻辑 const scriptEditUseCase = new ScriptEditUseCase(mockProjectScriptData.scriptText); // 模拟解析后的剧本片段(这是UseCase内部逻辑的结果) const mockParsedSlices = [ { id: 'slice1', type: ScriptSliceType.role, text: '我们出发吧!', metaData: { speaker: '冒险者', emotion: '兴奋' } }, { id: 'slice2', type: ScriptSliceType.scene, text: '场景:神秘的森林', metaData: { location: '森林', atmosphere: '神秘' } }, { id: 'slice3', type: ScriptSliceType.text, text: '这是一个充满挑战的冒险', metaData: { style: '冒险' } } ]; // 模拟从项目获取剧本数据 const response = await getProjectScript({ projectId }); expect(response.successful).toBe(true); expect(response.data.prompt).toBe('创建一个关于冒险的故事'); expect(response.data.scriptText).toContain('我们出发吧!'); // 验证API只返回提示词和剧本文本,不包含解析后的片段 expect(response.data).not.toHaveProperty('scriptSlices'); // 验证UseCase能够正确解析剧本文本 const parsedSlices = scriptEditUseCase.getScriptSlices(); // 注意:由于ScriptValueObject.parseFromString目前是TODO,这里只是验证调用不会出错 expect(Array.isArray(parsedSlices)).toBe(true); // 验证API调用参数 expect(getProjectScript).toHaveBeenCalledWith({ projectId }); }); it('应该处理获取项目剧本失败的情况', async () => { const projectId = 'invalid-project'; // 模拟API失败 (getProjectScript as jest.Mock).mockResolvedValue({ successful: false, message: '项目不存在' }); await expect(getProjectScript({ projectId })).resolves.toEqual({ successful: false, message: '项目不存在' }); expect(getProjectScript).toHaveBeenCalledWith({ projectId }); }); }); });