forked from 77media/video-flow
485 lines
18 KiB
TypeScript
485 lines
18 KiB
TypeScript
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 });
|
||
});
|
||
});
|
||
});
|