video-flow-b/app/service/test/ScriptService.test.ts

485 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ScriptEditUseCase>;
// 测试数据
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<typeof ScriptEditUseCase>).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 });
});
});
});