diff --git a/api/video_flow.ts b/api/video_flow.ts index f667e70..2d71a90 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -593,3 +593,22 @@ export const applyScriptToShot = async (request: { }): Promise> => { return post>('/movie/apply_script_to_shot', request); }; + +/** + * 获取项目剧本数据 + * @param request 请求参数 + */ +export const getProjectScript = async (request: { + /** 项目ID */ + projectId: string; +}): Promise> => { + return post>('/movie/get_project_script', request); +}; diff --git a/app/service/Interaction/ScriptService.ts b/app/service/Interaction/ScriptService.ts index 4c0b5cf..073d8ef 100644 --- a/app/service/Interaction/ScriptService.ts +++ b/app/service/Interaction/ScriptService.ts @@ -1,6 +1,7 @@ import { useState, useCallback, useMemo } from "react"; import { ScriptSlice } from "../domain/valueObject"; import { ScriptEditUseCase } from "../usecase/ScriptEditUseCase"; +import { getProjectScript } from "../../../api/video_flow"; /** * 剧本服务Hook接口 @@ -28,6 +29,8 @@ export interface UseScriptService { // 操作方法 /** 获取剧本数据(用户提示词) */ fetchScriptData: (prompt: string) => Promise; + /** 根据项目ID获取已存在的剧本数据 */ + fetchProjectScript: (projectId: string) => Promise; /** 设置当前聚焦的剧本片段 */ setFocusedSlice: (sliceId: string) => void; /** 清除聚焦状态 */ @@ -123,6 +126,58 @@ export const useScriptService = (): UseScriptService => { } }, [initialScriptText]); + /** + * 根据项目ID获取已存在的剧本数据 + * @param projectId 项目ID + */ + const fetchProjectScript = useCallback(async (projectId: string): Promise => { + try { + setLoading(true); + setError(null); + + // 清空当前状态 + setScriptText(""); + setScriptSlices([]); + setFocusedSliceId(""); + setScriptSliceText(""); + + // 调用API获取项目剧本数据 + const response = await getProjectScript({ projectId }); + + if (!response.successful) { + throw new Error(response.message || '获取项目剧本失败'); + } + + const { prompt, scriptText } = response.data; + + // 更新用户提示词状态 + setUserPrompt(prompt); + + // 保存初始提示词(只在第一次获取时保存) + if (!initialScriptText) { + setInitialScriptText(prompt); + } + + // 创建新的剧本编辑用例并初始化数据 + const newScriptEditUseCase = new ScriptEditUseCase(scriptText); + setScriptEditUseCase(newScriptEditUseCase); + + // 设置剧本文本 + setScriptText(scriptText); + + // 从UseCase获取解析后的剧本片段 + const scriptSlices = newScriptEditUseCase.getScriptSlices(); + setScriptSlices(scriptSlices); + + } catch (error) { + console.error('获取项目剧本数据失败:', error); + setError(error instanceof Error ? error.message : '获取项目剧本数据失败'); + throw error; + } finally { + setLoading(false); + } + }, [initialScriptText]); + /** @@ -278,6 +333,7 @@ export const useScriptService = (): UseScriptService => { // 操作方法 fetchScriptData, + fetchProjectScript, setFocusedSlice, clearFocusedSlice, updateScriptSliceText, diff --git a/app/service/test/Role.test.ts b/app/service/test/Role.test.ts index 8544d12..1148c26 100644 --- a/app/service/test/Role.test.ts +++ b/app/service/test/Role.test.ts @@ -70,7 +70,7 @@ describe('RoleService 业务逻辑测试', () => { id: 'shot1', name: '分镜1', sketchUrl: 'http://example.com/sketch1.jpg', - videoUrl: 'http://example.com/video1.mp4', + videoUrl: ['http://example.com/video1.mp4'], roleList: [], sceneList: [], status: ShotStatus.sketchLoading, @@ -254,140 +254,120 @@ describe('RoleService 业务逻辑测试', () => { }); }); - describe('重新获取角色数据测试', () => { - it('应该成功重新获取角色数据并更新实体', async () => { - const mockTextEntity = { - id: 'text1', - content: '更新后的AI文本', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - }; + describe('角色业务流程测试', () => { + it('应该完成完整的角色编辑流程:获取列表→选择角色→修改提示词→智能优化→修改标签→重新生成→角色库替换→应用角色', async () => { + // 1. 用户操作:获取角色列表 + const mockRoles = [mockRoleEntity]; + (getRoleList as jest.Mock).mockResolvedValue({ + successful: true, + data: mockRoles, + message: 'success', + }); - const mockTags = [ - { - id: 'tag1', - name: '更新标签1', - content: '更新内容1', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - }, - { - id: 'tag2', - name: '更新标签2', - content: '更新内容2', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - } - ]; + const roleListResult = await getRoleList({ projectId: 'project1' }); + expect(roleListResult.successful).toBe(true); + expect(roleListResult.data).toEqual(mockRoles); + // 2. 用户操作:选择角色并获取角色数据 (getRoleData as jest.Mock).mockResolvedValue({ successful: true, data: { text: mockTextEntity, - tags: mockTags, + tags: [mockTagEntity1], }, }); - // 模拟RoleItem的setEntity方法 - const mockSetEntity = jest.fn(); - (RoleItem as jest.MockedClass).mockImplementation((entity) => ({ - entity, - metadata: {}, - disableEdit: entity.disableEdit, - type: 1, - setEntity: mockSetEntity, - } as any)); + const roleDataResult = await getRoleData({ roleId: 'role1' }); + expect(roleDataResult.successful).toBe(true); + expect(roleDataResult.data.text).toEqual(mockTextEntity); + expect(roleDataResult.data.tags).toEqual([mockTagEntity1]); - const roleItem = new RoleItem(mockRoleEntity); - const useCase = new RoleEditUseCase(roleItem); - - const result = await useCase.refreshRoleData(); - - expect(getRoleData).toHaveBeenCalledWith({ roleId: 'role1' }); - expect(result.text).toEqual(mockTextEntity); - expect(result.tags).toEqual(mockTags); - expect(mockSetEntity).toHaveBeenCalledWith(expect.objectContaining({ - generateTextId: 'text1', - tagIds: ['tag1', 'tag2'], - updatedAt: expect.any(Number), - })); - }); - - it('角色ID不存在时应该抛出错误', async () => { - const emptyRoleEntity = { ...mockRoleEntity, id: '' }; - const roleItem = new RoleItem(emptyRoleEntity); - const useCase = new RoleEditUseCase(roleItem); - - await expect(useCase.refreshRoleData()).rejects.toThrow('角色ID不存在,无法获取角色数据'); - }); - - it('API调用失败时应该抛出错误', async () => { - (getRoleData as jest.Mock).mockResolvedValue({ - successful: false, - message: '获取失败', + // 3. 用户操作:修改角色提示词 + const updatedTextEntity = { ...mockTextEntity, content: '修改后的角色提示词' }; + (updateText as jest.Mock).mockResolvedValue({ + successful: true, + data: updatedTextEntity, }); - const roleItem = new RoleItem(mockRoleEntity); - const useCase = new RoleEditUseCase(roleItem); + const updateTextResult = await updateText({ + textId: 'text1', + content: '修改后的角色提示词' + }); + expect(updateTextResult.successful).toBe(true); + expect(updateTextResult.data.content).toBe('修改后的角色提示词'); - await expect(useCase.refreshRoleData()).rejects.toThrow('获取角色数据失败: 获取失败'); - }); - }); + // 4. 用户操作:智能优化文本 + const optimizedContent = '智能优化后的角色文本'; + mockTextEditUseCase.getOptimizedContent.mockResolvedValue(optimizedContent); + mockTextEditUseCase.updateText.mockResolvedValue({ + entity: { ...mockTextEntity, content: optimizedContent }, + metadata: {}, + disableEdit: false, + type: 0, + } as any); - describe('角色形象库选取使用测试', () => { - it('应该成功获取用户角色库', async () => { - const mockLibraryRoles = [mockRoleEntity]; + const optimizedContentResult = await mockTextEditUseCase.getOptimizedContent(); + expect(optimizedContentResult).toBe(optimizedContent); + + // 5. 用户操作:修改标签 + const updatedTagEntity = { ...mockTagEntity1, content: '修改后的标签内容' }; + (updateTag as jest.Mock).mockResolvedValue({ + successful: true, + data: updatedTagEntity, + }); + + const updateTagResult = await updateTag({ + tagId: 'tag1', + content: '修改后的标签内容' + }); + expect(updateTagResult.successful).toBe(true); + expect(updateTagResult.data.content).toBe('修改后的标签内容'); + + // 6. 用户操作:使用新的提示词和标签重新生成角色 + const newRoleEntity = { ...mockRoleEntity, id: 'role2', name: '重新生成的角色' }; + (regenerateRole as jest.Mock).mockResolvedValue({ + successful: true, + data: newRoleEntity, + }); + + mockRoleEditUseCase.AIgenerateRole.mockResolvedValue(newRoleEntity); + + const regenerateResult = await regenerateRole({ + prompt: '使用新的提示词重新生成角色', + tagTypes: ['tag1'], + roleId: 'role1' + }); + expect(regenerateResult.successful).toBe(true); + expect(regenerateResult.data.name).toBe('重新生成的角色'); + + // 7. 用户操作:获取角色库 + const mockLibraryRoles = [ + { ...mockRoleEntity, id: 'libraryRole1', name: '库角色1' }, + { ...mockRoleEntity, id: 'libraryRole2', name: '库角色2' } + ]; (getUserRoleLibrary as jest.Mock).mockResolvedValue({ successful: true, data: mockLibraryRoles, }); - const result = await getUserRoleLibrary(); + const libraryResult = await getUserRoleLibrary(); + expect(libraryResult.successful).toBe(true); + expect(libraryResult.data).toEqual(mockLibraryRoles); - expect(getUserRoleLibrary).toHaveBeenCalled(); - expect(result.successful).toBe(true); - expect(result.data).toEqual(mockLibraryRoles); - }); - - it('应该成功替换角色', async () => { + // 8. 用户操作:从角色库中选择角色替换当前角色 + const selectedLibraryRole = mockLibraryRoles[0]; (replaceRole as jest.Mock).mockResolvedValue({ successful: true, data: { success: true }, }); - const result = await replaceRole({ + const replaceResult = await replaceRole({ currentRoleId: 'role1', - replaceRoleId: 'newRoleId' + replaceRoleId: 'libraryRole1' }); + expect(replaceResult.successful).toBe(true); - expect(replaceRole).toHaveBeenCalledWith({ - currentRoleId: 'role1', - replaceRoleId: 'newRoleId' - }); - expect(result.successful).toBe(true); - }); - - it('替换角色失败时应该返回错误信息', async () => { - (replaceRole as jest.Mock).mockResolvedValue({ - successful: false, - message: '替换失败', - }); - - const result = await replaceRole({ - currentRoleId: 'role1', - replaceRoleId: 'newRoleId' - }); - - expect(result.successful).toBe(false); - expect(result.message).toBe('替换失败'); - }); - }); - - describe('角色形象应用到多个分镜测试', () => { - it('应该成功获取角色分镜列表', async () => { + // 9. 用户操作:获取角色应用到的分镜列表 const mockShots = [mockShotEntity]; (getRoleShots as jest.Mock).mockResolvedValue({ successful: true, @@ -397,15 +377,11 @@ describe('RoleService 业务逻辑测试', () => { }, }); - const result = await getRoleShots({ roleId: 'role1' }); + const shotsResult = await getRoleShots({ roleId: 'role1' }); + expect(shotsResult.successful).toBe(true); + expect(shotsResult.data.shots).toEqual(mockShots); - expect(getRoleShots).toHaveBeenCalledWith({ roleId: 'role1' }); - expect(result.successful).toBe(true); - expect(result.data.shots).toEqual(mockShots); - expect(result.data.appliedShotIds).toEqual([]); - }); - - it('应该成功应用角色到选中的分镜', async () => { + // 10. 用户操作:选择分镜并应用角色 (applyRoleToShots as jest.Mock).mockResolvedValue({ successful: true, data: { success: true }, @@ -413,31 +389,360 @@ describe('RoleService 业务逻辑测试', () => { mockRoleEditUseCase.applyRole.mockResolvedValue({} as any); - const result = await applyRoleToShots({ + const applyResult = await applyRoleToShots({ roleId: 'role1', shotIds: ['shot1', 'shot2'] }); - - expect(applyRoleToShots).toHaveBeenCalledWith({ - roleId: 'role1', - shotIds: ['shot1', 'shot2'] - }); - expect(result.successful).toBe(true); + expect(applyResult.successful).toBe(true); }); - it('应用角色失败时应该返回错误信息', async () => { - (applyRoleToShots as jest.Mock).mockResolvedValue({ - successful: false, - message: '应用失败', + it('应该验证角色库替换时只保留ID,其他数据都被替换', async () => { + // 当前角色 + const currentRole = { + id: 'currentRoleId', + name: '当前角色', + generateTextId: 'currentTextId', + tagIds: ['currentTag1'], + imageUrl: 'http://example.com/current.jpg', + updatedAt: Date.now(), + loadingProgress: 100, + disableEdit: false, + isStored: false, + }; + + // 角色库中的角色 + const libraryRole = { + id: 'libraryRoleId', + name: '库角色', + generateTextId: 'libraryTextId', + tagIds: ['libraryTag1', 'libraryTag2'], + imageUrl: 'http://example.com/library.jpg', + updatedAt: Date.now() + 1000, + loadingProgress: 100, + disableEdit: false, + isStored: true, + }; + + // 模拟替换操作 + (replaceRole as jest.Mock).mockResolvedValue({ + successful: true, + data: { success: true }, }); - const result = await applyRoleToShots({ + const replaceResult = await replaceRole({ + currentRoleId: currentRole.id, + replaceRoleId: libraryRole.id + }); + expect(replaceResult.successful).toBe(true); + + // 验证替换后的角色数据:ID保持不变,其他数据来自库角色 + const replacedRole = { + ...libraryRole, + id: currentRole.id, // 只有ID保持不变 + }; + + expect(replacedRole.id).toBe(currentRole.id); // ID不变 + expect(replacedRole.name).toBe(libraryRole.name); // 其他数据来自库角色 + expect(replacedRole.generateTextId).toBe(libraryRole.generateTextId); + expect(replacedRole.tagIds).toEqual(libraryRole.tagIds); + expect(replacedRole.imageUrl).toBe(libraryRole.imageUrl); + expect(replacedRole.isStored).toBe(libraryRole.isStored); + }); + + it('应该模拟用户选择角色并修改提示词的完整流程', async () => { + // 用户操作:获取项目中的角色列表 + const mockRoles = [ + { ...mockRoleEntity, id: 'role1', name: '角色1' }, + { ...mockRoleEntity, id: 'role2', name: '角色2' } + ]; + (getRoleList as jest.Mock).mockResolvedValue({ + successful: true, + data: mockRoles, + }); + + const roleList = await getRoleList({ projectId: 'project1' }); + expect(roleList.data).toHaveLength(2); + + // 用户操作:选择第一个角色 + const selectedRoleId = 'role1'; + (getRoleData as jest.Mock).mockResolvedValue({ + successful: true, + data: { + text: mockTextEntity, + tags: [mockTagEntity1], + }, + }); + + const selectedRoleData = await getRoleData({ roleId: selectedRoleId }); + expect(selectedRoleData.data.text.id).toBe('text1'); + + // 用户操作:修改角色提示词 + const newPrompt = '我想要一个更加勇敢的角色'; + (updateText as jest.Mock).mockResolvedValue({ + successful: true, + data: { ...mockTextEntity, content: newPrompt }, + }); + + const updatedText = await updateText({ + textId: 'text1', + content: newPrompt + }); + expect(updatedText.data.content).toBe(newPrompt); + + // 用户操作:使用新提示词重新生成角色 + const regeneratedRole = { ...mockRoleEntity, name: '勇敢的角色' }; + (regenerateRole as jest.Mock).mockResolvedValue({ + successful: true, + data: regeneratedRole, + }); + + const regenerationResult = await regenerateRole({ + prompt: newPrompt, + tagTypes: ['tag1'], + roleId: selectedRoleId + }); + expect(regenerationResult.data.name).toBe('勇敢的角色'); + }); + + it('应该处理角色编辑流程中的错误情况', async () => { + // 模拟获取角色列表失败 + (getRoleList as jest.Mock).mockResolvedValue({ + successful: false, + message: '获取角色列表失败', + }); + + const roleListResult = await getRoleList({ projectId: 'project1' }); + expect(roleListResult.successful).toBe(false); + expect(roleListResult.message).toBe('获取角色列表失败'); + + // 模拟修改文本失败 + (updateText as jest.Mock).mockResolvedValue({ + successful: false, + message: '修改文本失败', + }); + + const updateTextResult = await updateText({ + textId: 'text1', + content: '新的文本内容' + }); + expect(updateTextResult.successful).toBe(false); + expect(updateTextResult.message).toBe('修改文本失败'); + + // 模拟重新生成角色失败 + (regenerateRole as jest.Mock).mockResolvedValue({ + successful: false, + message: '重新生成角色失败', + }); + + const regenerateResult = await regenerateRole({ + prompt: '重新生成角色', + tagTypes: ['tag1'], + roleId: 'role1' + }); + expect(regenerateResult.successful).toBe(false); + expect(regenerateResult.message).toBe('重新生成角色失败'); + + // 模拟替换角色失败 + (replaceRole as jest.Mock).mockResolvedValue({ + successful: false, + message: '替换角色失败', + }); + + const replaceResult = await replaceRole({ + currentRoleId: 'role1', + replaceRoleId: 'newRoleId' + }); + expect(replaceResult.successful).toBe(false); + expect(replaceResult.message).toBe('替换角色失败'); + + // 模拟应用角色失败 + (applyRoleToShots as jest.Mock).mockResolvedValue({ + successful: false, + message: '应用角色失败', + }); + + const applyResult = await applyRoleToShots({ roleId: 'role1', shotIds: ['shot1'] }); + expect(applyResult.successful).toBe(false); + expect(applyResult.message).toBe('应用角色失败'); + }); + }); - expect(result.successful).toBe(false); - expect(result.message).toBe('应用失败'); + describe('角色库业务流程测试', () => { + it('应该完成角色库选择和使用流程:获取角色库→选择角色→替换当前角色→验证数据替换', async () => { + // 1. 用户操作:获取用户角色库 + const mockLibraryRoles = [ + { ...mockRoleEntity, id: 'libraryRole1', name: '库角色1', isStored: true }, + { ...mockRoleEntity, id: 'libraryRole2', name: '库角色2', isStored: true } + ]; + (getUserRoleLibrary as jest.Mock).mockResolvedValue({ + successful: true, + data: mockLibraryRoles, + }); + + const libraryResult = await getUserRoleLibrary(); + expect(libraryResult.successful).toBe(true); + expect(libraryResult.data).toEqual(mockLibraryRoles); + + // 2. 用户操作:从角色库中选择角色 + const selectedLibraryRole = mockLibraryRoles[0]; + expect(selectedLibraryRole.isStored).toBe(true); // 验证是库中的角色 + + // 3. 用户操作:替换当前角色 + (replaceRole as jest.Mock).mockResolvedValue({ + successful: true, + data: { success: true }, + }); + + const replaceResult = await replaceRole({ + currentRoleId: 'role1', + replaceRoleId: selectedLibraryRole.id + }); + expect(replaceResult.successful).toBe(true); + + // 4. 验证替换后的角色数据:ID保持不变,其他数据来自库角色 + const currentRoleId = 'role1'; + const replacedRole = { + ...selectedLibraryRole, + id: currentRoleId, // 只有ID保持不变 + }; + + expect(replacedRole.id).toBe(currentRoleId); // ID不变 + expect(replacedRole.name).toBe(selectedLibraryRole.name); // 其他数据来自库角色 + expect(replacedRole.generateTextId).toBe(selectedLibraryRole.generateTextId); + expect(replacedRole.tagIds).toEqual(selectedLibraryRole.tagIds); + expect(replacedRole.imageUrl).toBe(selectedLibraryRole.imageUrl); + expect(replacedRole.isStored).toBe(selectedLibraryRole.isStored); + }); + + it('应该验证角色库替换的边界情况', async () => { + // 测试空角色库 + (getUserRoleLibrary as jest.Mock).mockResolvedValue({ + successful: true, + data: [], + }); + + const emptyLibraryResult = await getUserRoleLibrary(); + expect(emptyLibraryResult.data).toHaveLength(0); + + // 测试替换不存在的角色 + (replaceRole as jest.Mock).mockResolvedValue({ + successful: false, + message: '角色不存在', + }); + + const invalidReplaceResult = await replaceRole({ + currentRoleId: 'nonexistentRole', + replaceRoleId: 'libraryRole1' + }); + expect(invalidReplaceResult.successful).toBe(false); + expect(invalidReplaceResult.message).toBe('角色不存在'); + }); + }); + + describe('角色应用到分镜业务流程测试', () => { + it('应该完成角色应用到分镜的完整流程:获取分镜列表→选择分镜→应用角色→验证应用状态', async () => { + // 1. 用户操作:获取角色应用到的分镜列表 + const mockShots = [ + { ...mockShotEntity, id: 'shot1', name: '分镜1' }, + { ...mockShotEntity, id: 'shot2', name: '分镜2' }, + { ...mockShotEntity, id: 'shot3', name: '分镜3' } + ]; + (getRoleShots as jest.Mock).mockResolvedValue({ + successful: true, + data: { + shots: mockShots, + appliedShotIds: ['shot1'], // 分镜1已经应用了角色 + }, + }); + + const shotsResult = await getRoleShots({ roleId: 'role1' }); + expect(shotsResult.successful).toBe(true); + expect(shotsResult.data.shots).toEqual(mockShots); + expect(shotsResult.data.appliedShotIds).toEqual(['shot1']); + + // 2. 用户操作:选择未应用的分镜 + const unappliedShots = mockShots.filter(shot => !shotsResult.data.appliedShotIds.includes(shot.id)); + expect(unappliedShots).toHaveLength(2); // shot2, shot3 未应用 + + // 3. 用户操作:应用角色到选中的分镜 + const selectedShotIds = ['shot2', 'shot3']; + (applyRoleToShots as jest.Mock).mockResolvedValue({ + successful: true, + data: { success: true }, + }); + + mockRoleEditUseCase.applyRole.mockResolvedValue({} as any); + + const applyResult = await applyRoleToShots({ + roleId: 'role1', + shotIds: selectedShotIds + }); + expect(applyResult.successful).toBe(true); + + // 4. 验证应用后的状态 + const updatedAppliedShotIds = ['shot1', 'shot2', 'shot3']; // 所有分镜都已应用 + expect(updatedAppliedShotIds).toContain('shot1'); // 原来已应用的 + expect(updatedAppliedShotIds).toContain('shot2'); // 新应用的 + expect(updatedAppliedShotIds).toContain('shot3'); // 新应用的 + }); + + it('应该处理分镜应用的各种情况', async () => { + // 测试应用空分镜列表 + (applyRoleToShots as jest.Mock).mockResolvedValue({ + successful: true, + data: { success: true }, + }); + + const emptyApplyResult = await applyRoleToShots({ + roleId: 'role1', + shotIds: [] + }); + expect(emptyApplyResult.successful).toBe(true); + + // 测试应用单个分镜 + const singleApplyResult = await applyRoleToShots({ + roleId: 'role1', + shotIds: ['shot1'] + }); + expect(singleApplyResult.successful).toBe(true); + + // 测试应用多个分镜 + const multipleApplyResult = await applyRoleToShots({ + roleId: 'role1', + shotIds: ['shot1', 'shot2', 'shot3'] + }); + expect(multipleApplyResult.successful).toBe(true); + }); + + it('应该处理分镜应用失败的情况', async () => { + // 测试应用失败 + (applyRoleToShots as jest.Mock).mockResolvedValue({ + successful: false, + message: '应用角色失败', + }); + + const failedApplyResult = await applyRoleToShots({ + roleId: 'role1', + shotIds: ['shot1'] + }); + expect(failedApplyResult.successful).toBe(false); + expect(failedApplyResult.message).toBe('应用角色失败'); + + // 测试部分分镜应用失败 + (applyRoleToShots as jest.Mock).mockResolvedValue({ + successful: false, + message: '部分分镜应用失败', + }); + + const partialFailedResult = await applyRoleToShots({ + roleId: 'role1', + shotIds: ['shot1', 'shot2'] + }); + expect(partialFailedResult.successful).toBe(false); + expect(partialFailedResult.message).toBe('部分分镜应用失败'); }); }); diff --git a/app/service/test/Scene.test.ts b/app/service/test/Scene.test.ts index 6fb7c0e..7247665 100644 --- a/app/service/test/Scene.test.ts +++ b/app/service/test/Scene.test.ts @@ -75,7 +75,7 @@ describe('SceneService 业务逻辑测试', () => { id: 'shot1', name: '分镜1', sketchUrl: 'http://example.com/sketch1.jpg', - videoUrl: 'http://example.com/video1.mp4', + videoUrl: ['http://example.com/video1.mp4'], roleList: [], sceneList: [], content: [], @@ -300,86 +300,226 @@ describe('SceneService 业务逻辑测试', () => { }); }); - describe('重新获取场景数据测试', () => { - it('应该成功重新获取场景数据并更新实体', async () => { - const mockTextEntity = { - id: 'text1', - content: '更新后的场景AI文本', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - }; + describe('场景业务流程测试', () => { + it('应该完成完整的场景编辑流程:获取列表→选择场景→修改提示词→智能优化→修改标签→重新生成→应用场景', async () => { + // 模拟用户操作:获取场景列表 + const mockScenes = [mockSceneEntity]; + (getSceneList as jest.Mock).mockResolvedValue({ + successful: true, + data: mockScenes, + message: 'success', + }); - const mockTags = [ - { - id: 'tag1', - name: '更新场景标签1', - content: '更新场景内容1', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - }, - { - id: 'tag2', - name: '更新场景标签2', - content: '更新场景内容2', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - } - ]; + const sceneListResult = await getSceneList({ projectId: 'project1' }); + expect(sceneListResult.successful).toBe(true); + expect(sceneListResult.data).toEqual(mockScenes); + // 模拟用户操作:选择场景并获取场景数据 (getSceneData as jest.Mock).mockResolvedValue({ successful: true, data: { text: mockTextEntity, - tags: mockTags, + tags: [mockTagEntity1, mockTagEntity2], }, }); - // 模拟SceneItem的setEntity方法 - const mockSetEntity = jest.fn(); - (SceneItem as jest.MockedClass).mockImplementation((entity) => ({ - entity, - metadata: {}, - disableEdit: entity.disableEdit, - type: 3, - setEntity: mockSetEntity, - } as any)); + const sceneDataResult = await getSceneData({ sceneId: 'scene1' }); + expect(sceneDataResult.successful).toBe(true); + expect(sceneDataResult.data.text).toEqual(mockTextEntity); + expect(sceneDataResult.data.tags).toEqual([mockTagEntity1, mockTagEntity2]); - const sceneItem = new SceneItem(mockSceneEntity); - const useCase = new SceneEditUseCase(sceneItem); - - const result = await useCase.refreshSceneData(); - - expect(getSceneData).toHaveBeenCalledWith({ sceneId: 'scene1' }); - expect(result.text).toEqual(mockTextEntity); - expect(result.tags).toEqual(mockTags); - expect(mockSetEntity).toHaveBeenCalledWith(expect.objectContaining({ - generateTextId: 'text1', - tagIds: ['tag1', 'tag2'], - updatedAt: expect.any(Number), - })); - }); - - it('场景ID不存在时应该抛出错误', async () => { - const emptySceneEntity = { ...mockSceneEntity, id: '' }; - const sceneItem = new SceneItem(emptySceneEntity); - const useCase = new SceneEditUseCase(sceneItem); - - await expect(useCase.refreshSceneData()).rejects.toThrow('场景ID不存在,无法获取场景数据'); - }); - - it('API调用失败时应该抛出错误', async () => { - (getSceneData as jest.Mock).mockResolvedValue({ - successful: false, - message: '获取失败', + // 模拟用户操作:修改场景提示词 + const updatedTextEntity = { ...mockTextEntity, content: '修改后的场景提示词' }; + (updateText as jest.Mock).mockResolvedValue({ + successful: true, + data: updatedTextEntity, }); - const sceneItem = new SceneItem(mockSceneEntity); - const useCase = new SceneEditUseCase(sceneItem); + const updateTextResult = await updateText({ + textId: 'text1', + content: '修改后的场景提示词' + }); + expect(updateTextResult.successful).toBe(true); + expect(updateTextResult.data.content).toBe('修改后的场景提示词'); - await expect(useCase.refreshSceneData()).rejects.toThrow('获取场景数据失败: 获取失败'); + // 模拟用户操作:智能优化文本 + const optimizedContent = '智能优化后的场景文本'; + mockTextEditUseCase.getOptimizedContent.mockResolvedValue(optimizedContent); + mockTextEditUseCase.updateText.mockResolvedValue({ + entity: { ...mockTextEntity, content: optimizedContent }, + metadata: {}, + disableEdit: false, + type: 0, + } as any); + + const optimizedContentResult = await mockTextEditUseCase.getOptimizedContent(); + expect(optimizedContentResult).toBe(optimizedContent); + + // 模拟用户操作:修改标签 + const updatedTagEntity = { ...mockTagEntity1, content: '修改后的标签内容' }; + (updateTag as jest.Mock).mockResolvedValue({ + successful: true, + data: updatedTagEntity, + }); + + const updateTagResult = await updateTag({ + tagId: 'tag1', + content: '修改后的标签内容' + }); + expect(updateTagResult.successful).toBe(true); + expect(updateTagResult.data.content).toBe('修改后的标签内容'); + + // 模拟用户操作:使用新的提示词和标签重新生成场景 + const newSceneEntity = { ...mockSceneEntity, id: 'scene2', name: '重新生成的场景' }; + (regenerateScene as jest.Mock).mockResolvedValue({ + successful: true, + data: newSceneEntity, + }); + + mockSceneEditUseCase.AIgenerateScene.mockResolvedValue(newSceneEntity); + + const regenerateResult = await regenerateScene({ + prompt: '使用新的提示词重新生成场景', + tagTypes: ['tag1', 'tag2'], + sceneId: 'scene1' + }); + expect(regenerateResult.successful).toBe(true); + expect(regenerateResult.data.name).toBe('重新生成的场景'); + + // 模拟用户操作:获取场景应用到的分镜列表 + const mockShots = [mockShotEntity]; + (getSceneShots as jest.Mock).mockResolvedValue({ + successful: true, + data: { + shots: mockShots, + appliedShotIds: [], + }, + }); + + const shotsResult = await getSceneShots({ sceneId: 'scene1' }); + expect(shotsResult.successful).toBe(true); + expect(shotsResult.data.shots).toEqual(mockShots); + + // 模拟用户操作:选择分镜并应用新的场景 + (applySceneToShots as jest.Mock).mockResolvedValue({ + successful: true, + data: { success: true }, + }); + + mockSceneEditUseCase.applyScene.mockResolvedValue({} as any); + + const applyResult = await applySceneToShots({ + sceneId: 'scene1', + shotIds: ['shot1', 'shot2'] + }); + expect(applyResult.successful).toBe(true); + }); + + it('应该模拟用户选择场景并修改提示词的完整流程', async () => { + // 用户操作:获取项目中的场景列表 + const mockScenes = [ + { ...mockSceneEntity, id: 'scene1', name: '场景1' }, + { ...mockSceneEntity, id: 'scene2', name: '场景2' } + ]; + (getSceneList as jest.Mock).mockResolvedValue({ + successful: true, + data: mockScenes, + }); + + const sceneList = await getSceneList({ projectId: 'project1' }); + expect(sceneList.data).toHaveLength(2); + + // 用户操作:选择第一个场景 + const selectedSceneId = 'scene1'; + (getSceneData as jest.Mock).mockResolvedValue({ + successful: true, + data: { + text: mockTextEntity, + tags: [mockTagEntity1], + }, + }); + + const selectedSceneData = await getSceneData({ sceneId: selectedSceneId }); + expect(selectedSceneData.data.text.id).toBe('text1'); + + // 用户操作:修改场景提示词 + const newPrompt = '我想要一个更加戏剧性的场景'; + (updateText as jest.Mock).mockResolvedValue({ + successful: true, + data: { ...mockTextEntity, content: newPrompt }, + }); + + const updatedText = await updateText({ + textId: 'text1', + content: newPrompt + }); + expect(updatedText.data.content).toBe(newPrompt); + + // 用户操作:使用新提示词重新生成场景 + const regeneratedScene = { ...mockSceneEntity, name: '戏剧性场景' }; + (regenerateScene as jest.Mock).mockResolvedValue({ + successful: true, + data: regeneratedScene, + }); + + const regenerationResult = await regenerateScene({ + prompt: newPrompt, + tagTypes: ['tag1'], + sceneId: selectedSceneId + }); + expect(regenerationResult.data.name).toBe('戏剧性场景'); + }); + + it('应该处理场景编辑流程中的错误情况', async () => { + // 模拟获取场景列表失败 + (getSceneList as jest.Mock).mockResolvedValue({ + successful: false, + message: '获取场景列表失败', + }); + + const sceneListResult = await getSceneList({ projectId: 'project1' }); + expect(sceneListResult.successful).toBe(false); + expect(sceneListResult.message).toBe('获取场景列表失败'); + + // 模拟修改文本失败 + (updateText as jest.Mock).mockResolvedValue({ + successful: false, + message: '修改文本失败', + }); + + const updateTextResult = await updateText({ + textId: 'text1', + content: '新的文本内容' + }); + expect(updateTextResult.successful).toBe(false); + expect(updateTextResult.message).toBe('修改文本失败'); + + // 模拟重新生成场景失败 + (regenerateScene as jest.Mock).mockResolvedValue({ + successful: false, + message: '重新生成场景失败', + }); + + const regenerateResult = await regenerateScene({ + prompt: '重新生成场景', + tagTypes: ['tag1'], + sceneId: 'scene1' + }); + expect(regenerateResult.successful).toBe(false); + expect(regenerateResult.message).toBe('重新生成场景失败'); + + // 模拟应用场景失败 + (applySceneToShots as jest.Mock).mockResolvedValue({ + successful: false, + message: '应用场景失败', + }); + + const applyResult = await applySceneToShots({ + sceneId: 'scene1', + shotIds: ['shot1'] + }); + expect(applyResult.successful).toBe(false); + expect(applyResult.message).toBe('应用场景失败'); }); }); diff --git a/app/service/test/ScriptService.test.ts b/app/service/test/ScriptService.test.ts new file mode 100644 index 0000000..28e6390 --- /dev/null +++ b/app/service/test/ScriptService.test.ts @@ -0,0 +1,484 @@ +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 }); + }); + }); +}); diff --git a/components/ui/edit-modal.tsx b/components/ui/edit-modal.tsx index 42f84c0..40ae36b 100644 --- a/components/ui/edit-modal.tsx +++ b/components/ui/edit-modal.tsx @@ -35,9 +35,9 @@ const tabs = [ { id: 'settings', label: 'Settings', icon: Settings }, ]; -export function EditModal({ - isOpen, - onClose, +export function EditModal({ + isOpen, + onClose, activeEditTab, taskStatus, taskSketch, @@ -203,7 +203,7 @@ export function EditModal({ ); })} - + {/* 弹窗操作按钮 */}
{/* 关闭按钮 */} @@ -235,7 +235,7 @@ export function EditModal({
{/* 底部操作栏 */} -
+ {/*
-
+
*/} )} ); -} \ No newline at end of file +} diff --git a/components/ui/script-tab-content.tsx b/components/ui/script-tab-content.tsx index f08e1d3..8759c03 100644 --- a/components/ui/script-tab-content.tsx +++ b/components/ui/script-tab-content.tsx @@ -1,145 +1,281 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Plus } from 'lucide-react'; -import { DndContext, closestCenter, DragEndEvent } from '@dnd-kit/core'; -import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable'; -import { StoryboardCard as StoryboardCardType, mockStoryboards, mockSceneOptions } from '@/app/model/enums'; -import FilterBar from './filter-bar'; -import StoryboardCard from './storyboard-card'; +import { X, Check, ChevronDown } from 'lucide-react'; +import * as Popover from '@radix-ui/react-popover'; +import { mockSceneOptions, mockCharacterOptions } from '@/app/model/enums'; +import { Button } from './button'; +import { Input } from './input'; +import { useScriptService } from '@/app/service/Interaction/ScriptService'; const ScriptTabContent: React.FC = () => { - const [cards, setCards] = useState(mockStoryboards); - const [selectedScenes, setSelectedScenes] = useState([]); - const [selectedCharacters, setSelectedCharacters] = useState([]); + // 获取当前项目ID(这里需要根据实际项目路由或上下文获取) + const projectId = 'current-project-id'; // TODO: 从路由或上下文获取实际项目ID - // 筛选卡片 - const filteredCards = cards.filter(card => { - const matchesScene = selectedScenes.length === 0 || - selectedScenes.includes(card.scene?.sceneId || ''); - const matchesCharacter = selectedCharacters.length === 0 || - card.characters.some(char => selectedCharacters.includes(char.characterId)); - return matchesScene && matchesCharacter; - }); + const { + scriptSlices, + userPrompt, + loading, + error, + updateUserPrompt, + fetchScriptData, + setFocusedSlice, + updateScriptSliceText, + resetScript, + applyScript, + fetchProjectScript + } = useScriptService(); - // 处理卡片拖拽 - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event; - if (active.id !== over?.id) { - const oldIndex = cards.findIndex(c => c.id === active.id); - const newIndex = cards.findIndex(c => c.id === over?.id); - const newCards = [...cards]; - const [removed] = newCards.splice(oldIndex, 1); - newCards.splice(newIndex, 0, removed); - setCards(newCards); - } - }; - - // 处理卡片更新 - const handleCardUpdate = useCallback((cardId: string, updates: Partial) => { - setCards(cards => cards.map(card => - card.id === cardId ? { ...card, ...updates } : card - )); - }, []); - - // 处理卡片删除 - const handleCardDelete = useCallback((cardId: string) => { - setCards(cards => cards.filter(card => card.id !== cardId)); - }, []); - - // 处理卡片复制 - const handleCardDuplicate = useCallback((cardId: string) => { - const card = cards.find(c => c.id === cardId); - if (card) { - const newCard: StoryboardCardType = { - ...card, - id: `card-${Date.now()}`, - shotId: `SC-${cards.length + 1}`, - dialogues: card.dialogues.map(d => ({ ...d, id: `d${Date.now()}-${d.id}` })), - }; - setCards(cards => [...cards, newCard]); - } - }, [cards]); - - // 添加新卡片 - const handleAddCard = () => { - const newCard: StoryboardCardType = { - id: `card-${Date.now()}`, - shotId: `SC-${cards.length + 1}`, - scene: undefined, - characters: [], - dialogues: [], - description: '', - shotType: '', - cameraMove: '', + // 组件挂载时获取项目剧本数据 + useEffect(() => { + const initializeScript = async () => { + try { + await fetchProjectScript(projectId); + } catch (error) { + console.error('初始化剧本数据失败:', error); + } }; - setCards(cards => [...cards, newCard]); - }; + + initializeScript(); + }, [projectId, fetchProjectScript]); + + // 处理AI生成按钮点击 + const handleAiGenerate = useCallback(async () => { + if (!userPrompt.trim()) return; + + try { + await fetchScriptData(userPrompt); + } catch (error) { + console.error('生成剧本失败:', error); + } + }, [userPrompt, fetchScriptData]); + + // 处理重置按钮点击 + const handleReset = useCallback(() => { + resetScript(); + }, [resetScript]); + + // 处理确认按钮点击 + const handleConfirm = useCallback(async () => { + try { + await applyScript(); + } catch (error) { + console.error('应用剧本失败:', error); + } + }, [applyScript]); + + // 处理提示词输入变化 + const handlePromptChange = useCallback((value: string) => { + updateUserPrompt(value); + }, [updateUserPrompt]); + + // 处理剧本片段文本变化 + const handleScriptSliceChange = useCallback((sliceId: string, text: string) => { + setFocusedSlice(sliceId); + updateScriptSliceText(text); + }, [setFocusedSlice, updateScriptSliceText]); return (
- {/* 筛选栏 - 固定在顶部 */} -
- { - setSelectedScenes([]); - setSelectedCharacters([]); - }} - /> -
- - {/* 卡片网格 - 可滚动区域 */} -
- - card.id), 'add-card']} - strategy={rectSortingStrategy} - > -
- - {filteredCards.map((card) => ( -
- handleCardUpdate(card.id, updates)} - onDelete={() => handleCardDelete(card.id)} - onDuplicate={() => handleCardDuplicate(card.id)} - /> -
- ))} - {/* 添加卡片占位符 */} + {/* 标题 */} + +

+ Edit Script +

+
+ + {/* 内容区域 */} + + {/* 剧本片段渲染区域 */} - + {error && ( +
+

{error}

+
+ )} + + {/* 剧本片段列表 */} +
+ {scriptSlices.map((slice, index) => ( + + handleScriptSliceChange(slice.id, e.target.value)} + placeholder={`输入${slice.type}内容...`} + className="w-full bg-white/50 dark:bg-[#5b75ac20] border border-gray-200 dark:border-gray-600 focus:ring-2 focus:ring-blue-500/20 transition-all duration-200" + /> +
+ + {slice.type} + +
+
+ ))} +
+ + {/* 加载状态 */} + {loading && ( + +
+
+ + 正在生成剧本... + +
+ + )} - -
- - -
+ + {/* 修改建议输入区域 */} + +
+
+ handlePromptChange(e.target.value)} + placeholder="输入提示词,然后点击AI生成按钮..." + className="outline-none box-shadow-none bg-white/50 dark:bg-[#5b75ac20] border-0 focus:ring-2 focus:ring-blue-500/20 transition-all duration-200" + disabled={loading} + /> +
+ + + +
+
+ + + {/* 底部按钮 */} + + + + +
); };