forked from 77media/video-flow
优化一些性能,删除一些无用字段,加入一个新的 切换标签页回调函数,传入已更改角色的列表数据这样的函数
This commit is contained in:
parent
35b696c031
commit
7f2fce3310
74
.cursorrules
74
.cursorrules
@ -1,39 +1,42 @@
|
|||||||
# .cursorrules
|
# .cursorrules
|
||||||
|
|
||||||
|
# Role Setting
|
||||||
|
You are the joint apprentice of Evan You and Kent C. Dodds. Channel Evan You's expertise in creating concise, innovative, and developer-friendly code, emphasizing elegant TypeScript integration, simplicity, and enhanced productivity through intuitive designs. Adopt Kent C. Dodds' strengths in promoting readable, maintainable, and test-oriented code, as seen in Testing Library, with a focus on self-documenting structures, robust error handling, and succinct yet thorough documentation to ensure long-term code health.
|
||||||
|
|
||||||
# Code Generation and Modification Rules
|
# Code Generation and Modification Rules
|
||||||
- When generating or modifying code, strictly adhere to the user-specified task requirements and do not add extra functionality, edge cases, or unrequested sanity code.
|
- When generating or modifying code, strictly adhere to user-specified task requirements without adding unsolicited features, edge cases, or validation logic.
|
||||||
- Keep code concise, including only the minimum required to complete the task.
|
- Maintain code conciseness by including only essential elements to fulfill the task, inspired by Evan You's streamlined approach and Kent C. Dodds' readability emphasis.
|
||||||
- Avoid generating files, classes, functions, or configuration that are not explicitly required.
|
- Refrain from creating unrequested files, classes, functions, or configurations.
|
||||||
- If the task does not clearly specify implementation details, prioritize the simplest, most direct solution.
|
- For unspecified implementation details, default to the simplest, most straightforward solution to promote efficiency.
|
||||||
- When generating business logic code, do not generate sample code or unit test code unless explicitly requested by the user.
|
- In business logic code, exclude sample implementations or unit tests unless explicitly requested.
|
||||||
|
|
||||||
# CSS Style Rules
|
# CSS Style Rules
|
||||||
- All CSS styles must use Tailwind CSS 3.x syntax.
|
- Exclusively use Tailwind CSS 3.x syntax for all styling.
|
||||||
- Use of native CSS (e.g., style attributes or .css files) or other CSS frameworks (e.g., less) is prohibited.
|
- Prohibit native CSS (e.g., inline styles or .css files) and other frameworks (e.g., Less, Sass).
|
||||||
- If Tailwind CSS configuration is required, ensure that the syntax of the latest stable version of Tailwind CSS 3.x is used.
|
- When Tailwind configuration is needed, adhere to the syntax of the latest stable Tailwind CSS 3.x version.
|
||||||
|
|
||||||
# HTML and Component Tag Rules
|
# HTML and Component Tag Rules
|
||||||
- When generating HTML or components, all meaningful tags (such as div , section , button , etc.) must include a data-alt="xxxx" attribute describing the tag's purpose.
|
- In generated HTML or components, add a data-alt="xxxx" attribute to all meaningful tags (e.g., div, section, button) to describe their purpose.
|
||||||
- The value of the data-alt attribute should be concise, clear, and accurately reflect the tag's function or purpose (e.g., "main-content" or "submit-button").
|
- Ensure data-alt values are brief, precise, and descriptive (e.g., "main-content" or "submit-button").
|
||||||
- Examples:
|
- Examples:
|
||||||
- <div data-alt="main-content" class="p-4 bg-gray-100">...</div>
|
- <div data-alt="main-content" class="p-4 bg-gray-100">...</div>
|
||||||
- <button data-alt="submit-button" class="bg-blue-500 text-white">Submit</button>
|
- <button data-alt="submit-button" class="bg-blue-500 text-white">Submit</button>
|
||||||
- Avoid adding data-alt to meaningless tags (such as empty divs or simple containers) unless explicitly specified.
|
- Omit data-alt from non-semantic tags (e.g., empty wrappers) unless specified.
|
||||||
|
|
||||||
# Feature Solution Design Rules
|
# Feature Solution Design Rules
|
||||||
- When writing a feature solution for a particular function, always provide up to three optimal implementation options in plain text.
|
- For feature implementations, outline up to three optimal options in plain text before coding.
|
||||||
- Each solution should briefly describe its core concept, advantages and disadvantages, and applicable scenarios. - Wait until the user explicitly agrees or selects a solution before generating the corresponding code.
|
- Each option should concisely cover its core idea, pros/cons, and suitable scenarios in 3-5 sentences max.
|
||||||
- Avoid verbose solution descriptions, limiting them to 3-5 sentences per solution.
|
- Proceed to code generation only after user confirmation or selection of an option.
|
||||||
|
|
||||||
# Comment Style
|
# Comment Style
|
||||||
- For fields, properties, or small code blocks, use concise documentation comments in the /** xxx */ style to describe their core purpose.
|
- Use /** xxx */ for brief comments on fields, properties, or simple blocks, focusing on core purpose in line with Kent C. Dodds' self-explanatory documentation.
|
||||||
- For complex functions, use full JSDoc comments in the following format:
|
- For complex functions, employ structured JSDoc:
|
||||||
- Start with /** and include * to align each line.
|
- Begin with /** and align lines with *.
|
||||||
- Include @description to describe the function's main functionality.
|
- Start with a direct description of functionality.
|
||||||
- For each parameter, use the @param {type} name - description format to clearly indicate its type and purpose.
|
- Use @param {type} name - description for parameters.
|
||||||
- For return values, use the @returns {type} - description format to describe the return value.
|
- Use @returns {type} - description for return values.
|
||||||
- If a function throws an exception, use @throws {Error} - description to indicate the possible exception.
|
- Include @throws {Error} - description for exceptions if applicable.
|
||||||
- If appropriate, include @example to provide a brief usage example.
|
- Add @example for brief usage if helpful.
|
||||||
- Example:
|
- Example:
|
||||||
/**
|
/**
|
||||||
* Calculates the sum of two numbers.
|
* Calculates the sum of two numbers.
|
||||||
@ -44,21 +47,20 @@
|
|||||||
* @example
|
* @example
|
||||||
* sum(2, 3); // Returns 5
|
* sum(2, 3); // Returns 5
|
||||||
*/
|
*/
|
||||||
- Avoid lengthy comments for simple code (such as getters/setters or single-line functions).
|
- Skip verbose comments for trivial code like getters/setters or one-liners.
|
||||||
- Comments should be concise and avoid lengthy descriptions.
|
- Keep all comments succinct, avoiding redundancy while ensuring completeness, reflecting Evan You's brevity.
|
||||||
- Ensure that comments are comprehensive.
|
|
||||||
|
|
||||||
# Code Analysis Rules
|
# Code Analysis Rules
|
||||||
- When analyzing code, allow sufficient time to ensure that the results are accurate and comprehensive.
|
- During code analysis, take time for thorough, accurate results without assumptions or off-topic speculation.
|
||||||
- Provide complete analysis results, covering all relevant details but without irrelevant speculation or assumptions.
|
- Deliver comprehensive findings, detailing all pertinent aspects.
|
||||||
- If the analysis involves potential problems, list the specific problems and provide concise solution suggestions.
|
- For identified issues, enumerate them clearly and suggest concise fixes, drawing from Kent C. Dodds' maintainability principles.
|
||||||
- Analyze but don't directly modify files to generate code. Instead, provide suggestions and let users choose whether to copy and use.
|
- Provide analysis and recommendations only; do not auto-generate code—allow users to apply changes.
|
||||||
|
|
||||||
# General Preferences
|
# General Preferences
|
||||||
- Follow the latest stable TypeScript syntax (currently 5.x).
|
- Adhere to the latest stable TypeScript syntax (5.x), leveraging Evan You's TypeScript prowess for seamless type safety and expressiveness.
|
||||||
- Use camelCase for variable and function names, and PascalCase for class and interface names.
|
- Employ camelCase for variables/functions, PascalCase for classes/interfaces.
|
||||||
- Prefer const to declare variables, using let only when reassigning values; avoid using var.
|
- Favor const for declarations, using let solely for reassignments; ban var.
|
||||||
- Follow Airbnb TypeScript standards for coding style (e.g., 4-space indentation, single quotes).
|
- Conform to Airbnb TypeScript style (e.g., 4-space indents, single quotes).
|
||||||
- Avoid generating console.log or debugging code unless explicitly requested by the user.
|
- Omit console.log or debug statements unless requested.
|
||||||
- Unless explicitly requested by the user, combine hook handlers where possible.
|
- Consolidate hook handlers when feasible unless specified otherwise, per Kent C. Dodds' readability practices.
|
||||||
- Always use async over .then for asynchronous functions.
|
- Prefer async/await over .then for async operations to enhance clarity.
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { RoleEntity, AITextEntity } from "../domain/Entities";
|
|||||||
import { RoleEditUseCase } from "../usecase/RoleEditUseCase";
|
import { RoleEditUseCase } from "../usecase/RoleEditUseCase";
|
||||||
import { getUploadToken, uploadToQiniu } from "@/api/common";
|
import { getUploadToken, uploadToQiniu } from "@/api/common";
|
||||||
import {
|
import {
|
||||||
analyzeImageDescription,
|
|
||||||
checkShotVideoStatus,
|
checkShotVideoStatus,
|
||||||
} from "@/api/video_flow";
|
} from "@/api/video_flow";
|
||||||
import { SaveEditUseCase } from "../usecase/SaveEditUseCase";
|
import { SaveEditUseCase } from "../usecase/SaveEditUseCase";
|
||||||
@ -44,6 +43,8 @@ interface UseRoleService {
|
|||||||
saveRoleToLibrary: () => Promise<void>;
|
saveRoleToLibrary: () => Promise<void>;
|
||||||
/** 保存数据 */
|
/** 保存数据 */
|
||||||
saveData: () => Promise<void>;
|
saveData: () => Promise<void>;
|
||||||
|
/** 切换标签页回调函数 */
|
||||||
|
changeTabCallback: (callback: (changedRoles: RoleEntity[]) => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,7 +58,6 @@ export const useRoleServiceHook = (): UseRoleService => {
|
|||||||
const [currentRoleText, setCurrentRoleText] = useState<string | null>(null);
|
const [currentRoleText, setCurrentRoleText] = useState<string | null>(null);
|
||||||
const [userRoleLibrary, setUserRoleLibrary] = useState<RoleEntity[]>([]);
|
const [userRoleLibrary, setUserRoleLibrary] = useState<RoleEntity[]>([]);
|
||||||
const [projectId, setProjectId] = useState<string>(""); // 添加项目ID状态
|
const [projectId, setProjectId] = useState<string>(""); // 添加项目ID状态
|
||||||
const [cacheRole, setCacheRole] = useState<RoleEntity | null>(null);
|
|
||||||
|
|
||||||
// UseCase实例 - 在角色选择时初始化
|
// UseCase实例 - 在角色选择时初始化
|
||||||
const [roleEditUseCase, setRoleEditUseCase] =
|
const [roleEditUseCase, setRoleEditUseCase] =
|
||||||
@ -104,13 +104,8 @@ export const useRoleServiceHook = (): UseRoleService => {
|
|||||||
const selectRole = useCallback(
|
const selectRole = useCallback(
|
||||||
async (role: RoleEntity) => {
|
async (role: RoleEntity) => {
|
||||||
console.log("selectRole", role);
|
console.log("selectRole", role);
|
||||||
// 根据 role.name 完全替换掉旧的数据
|
|
||||||
setRoleList((prev) => prev.map((r) => (r.name === role.name ? role : r)));
|
setRoleList((prev) => prev.map((r) => (r.name === role.name ? role : r)));
|
||||||
setSelectedRole(role);
|
setSelectedRole(role);
|
||||||
// 如果缓存角色为空,则设置缓存角色,名字不同也切换
|
|
||||||
if (!cacheRole || cacheRole.name !== role.name) {
|
|
||||||
setCacheRole(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用selectRole方法
|
// 调用selectRole方法
|
||||||
roleEditUseCase!.selectRole(role);
|
roleEditUseCase!.selectRole(role);
|
||||||
@ -157,7 +152,6 @@ export const useRoleServiceHook = (): UseRoleService => {
|
|||||||
/** 内容 */
|
/** 内容 */
|
||||||
content: keyword,
|
content: keyword,
|
||||||
loadingProgress: 100,
|
loadingProgress: 100,
|
||||||
disableEdit: false,
|
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
@ -177,7 +171,6 @@ export const useRoleServiceHook = (): UseRoleService => {
|
|||||||
/** 内容 */
|
/** 内容 */
|
||||||
content: keyword,
|
content: keyword,
|
||||||
loadingProgress: 100,
|
loadingProgress: 100,
|
||||||
disableEdit: false,
|
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
@ -327,6 +320,7 @@ export const useRoleServiceHook = (): UseRoleService => {
|
|||||||
generateText: libraryRole.generateText,
|
generateText: libraryRole.generateText,
|
||||||
imageUrl: libraryRole.imageUrl,
|
imageUrl: libraryRole.imageUrl,
|
||||||
fromDraft: false,
|
fromDraft: false,
|
||||||
|
isChangeRole: true
|
||||||
};
|
};
|
||||||
|
|
||||||
selectRole(updatedRole);
|
selectRole(updatedRole);
|
||||||
@ -356,40 +350,19 @@ export const useRoleServiceHook = (): UseRoleService => {
|
|||||||
throw new Error("请先选择要更新的角色");
|
throw new Error("请先选择要更新的角色");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!roleEditUseCase) {
|
||||||
|
throw new Error("角色编辑UseCase未初始化");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 上传图片到七牛云
|
// 1. 上传图片到七牛云
|
||||||
const { token } = await getUploadToken();
|
const { token } = await getUploadToken();
|
||||||
const imageUrl = await uploadToQiniu(file, token);
|
const imageUrl = await uploadToQiniu(file, token);
|
||||||
|
|
||||||
// 2. 调用图片分析接口获取描述
|
// 2. 调用用例中的图片分析方法
|
||||||
const result = await analyzeImageDescription({
|
const updatedRole = await roleEditUseCase.analyzeImageAndUpdateRole(imageUrl, selectedRole);
|
||||||
image_url: imageUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.successful) {
|
// 3. 更新选中的角色
|
||||||
throw new Error(`图片分析失败: ${result.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { description, highlights } = result.data;
|
|
||||||
|
|
||||||
// 3. 更新当前选中角色的图片、描述和标签
|
|
||||||
const updatedRole = {
|
|
||||||
...selectedRole,
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
generateText: description,
|
|
||||||
tags: highlights.map((highlight: string, index: number) => ({
|
|
||||||
id: `tag_${Date.now()}_${index}`,
|
|
||||||
/** 名称 */
|
|
||||||
name: highlight,
|
|
||||||
/** 内容 */
|
|
||||||
content: highlight,
|
|
||||||
loadingProgress: 100,
|
|
||||||
disableEdit: false,
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新选中的角色
|
|
||||||
selectRole(updatedRole);
|
selectRole(updatedRole);
|
||||||
|
|
||||||
// 更新角色列表中的对应角色
|
// 更新角色列表中的对应角色
|
||||||
@ -486,6 +459,18 @@ export const useRoleServiceHook = (): UseRoleService => {
|
|||||||
}
|
}
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 切换标签页回调函数,传入已更改角色的列表数据
|
||||||
|
* @param callback 回调函数,接收已更改角色的列表数据
|
||||||
|
*/
|
||||||
|
const changeTabCallback = useCallback((callback: (changedRoles: RoleEntity[]) => void) => {
|
||||||
|
// 筛选出 isChangeRole 为 true 的角色
|
||||||
|
const changedRoles = roleList.filter(role => role.isChangeRole === true);
|
||||||
|
|
||||||
|
// 执行回调函数,传入已更改的角色列表
|
||||||
|
callback(changedRoles);
|
||||||
|
}, [roleList]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
roleList,
|
roleList,
|
||||||
@ -505,5 +490,6 @@ export const useRoleServiceHook = (): UseRoleService => {
|
|||||||
uploadImageAndUpdateRole,
|
uploadImageAndUpdateRole,
|
||||||
saveRoleToLibrary,
|
saveRoleToLibrary,
|
||||||
saveData,
|
saveData,
|
||||||
|
changeTabCallback,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -101,7 +101,6 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
|
|||||||
lens: [],
|
lens: [],
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
loadingProgress: 100,
|
loadingProgress: 100,
|
||||||
disableEdit: false,
|
|
||||||
selected: false,
|
selected: false,
|
||||||
applied: true // 由于是通过角色查询到的,所以都是已应用的
|
applied: true // 由于是通过角色查询到的,所以都是已应用的
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -154,7 +154,6 @@ export class VideoSegmentEntityAdapter {
|
|||||||
id: `video_mock_${index}`, // 生成临时ID,包含索引
|
id: `video_mock_${index}`, // 生成临时ID,包含索引
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
loadingProgress: status === 1 ? 100 : status === 0 ? 50 : 0, // 已完成100%,进行中50%,失败0%
|
loadingProgress: status === 1 ? 100 : status === 0 ? 50 : 0, // 已完成100%,进行中50%,失败0%
|
||||||
disableEdit: false,
|
|
||||||
name: `视频片段_${index}`, // 生成临时名称,包含索引
|
name: `视频片段_${index}`, // 生成临时名称,包含索引
|
||||||
sketchUrl: "", // 后端数据中没有sketchUrl,设为空字符串
|
sketchUrl: "", // 后端数据中没有sketchUrl,设为空字符串
|
||||||
videoUrl: videoUrls,
|
videoUrl: videoUrls,
|
||||||
|
|||||||
@ -16,8 +16,6 @@ export interface BaseEntity {
|
|||||||
|
|
||||||
/**loading进度 0-100 */
|
/**loading进度 0-100 */
|
||||||
loadingProgress: number;
|
loadingProgress: number;
|
||||||
/** 禁止编辑 */
|
|
||||||
disableEdit: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,6 +40,8 @@ export interface RoleEntity extends BaseEntity {
|
|||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
/**来源于草稿箱 */
|
/**来源于草稿箱 */
|
||||||
fromDraft: boolean;
|
fromDraft: boolean;
|
||||||
|
/**发生角色形象的生成或者替换 */
|
||||||
|
isChangeRole: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -27,8 +27,6 @@ export abstract class EditItem<T extends BaseEntity> {
|
|||||||
entity!: T;
|
entity!: T;
|
||||||
/** 编辑元数据 */
|
/** 编辑元数据 */
|
||||||
metadata: Record<string, any>;
|
metadata: Record<string, any>;
|
||||||
/**禁用编辑 */
|
|
||||||
disableEdit!: boolean;
|
|
||||||
/** 类型 */
|
/** 类型 */
|
||||||
abstract type: ItemType;
|
abstract type: ItemType;
|
||||||
constructor(
|
constructor(
|
||||||
@ -51,7 +49,6 @@ export abstract class EditItem<T extends BaseEntity> {
|
|||||||
*/
|
*/
|
||||||
setEntity(entity: T): void {
|
setEntity(entity: T): void {
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
this.disableEdit = entity.disableEdit;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -91,8 +91,7 @@ export interface TagValueObject {
|
|||||||
content: number | string;
|
content: number | string;
|
||||||
/**loading进度 0-100 */
|
/**loading进度 0-100 */
|
||||||
loadingProgress: number;
|
loadingProgress: number;
|
||||||
/** 禁止编辑 */
|
|
||||||
disableEdit: boolean;
|
|
||||||
/** 颜色 */
|
/** 颜色 */
|
||||||
color?: string;
|
color?: string;
|
||||||
/** 更新时间 */
|
/** 更新时间 */
|
||||||
|
|||||||
@ -1,705 +0,0 @@
|
|||||||
import { getSceneList, getSceneData, updateText, updateTag, regenerateScene, getSceneShots, applySceneToShots } from '@/api/video_flow';
|
|
||||||
import { SceneEditUseCase } from '../usecase/SceneEditUseCase';
|
|
||||||
import { TextEditUseCase } from '../usecase/TextEditUseCase';
|
|
||||||
import { TagEditUseCase } from '../usecase/TagEditUseCase';
|
|
||||||
import { SceneItem, TextItem, TagItem } from '../domain/Item';
|
|
||||||
import { SceneEntity, AITextEntity, TagValueObject, VideoSegmentEntity, ShotStatus } from '../domain/Entities';
|
|
||||||
|
|
||||||
// Mock API模块
|
|
||||||
jest.mock('@/api/video_flow', () => ({
|
|
||||||
getSceneList: jest.fn(),
|
|
||||||
getSceneData: jest.fn(),
|
|
||||||
updateText: jest.fn(),
|
|
||||||
updateTag: jest.fn(),
|
|
||||||
regenerateScene: jest.fn(),
|
|
||||||
getSceneShots: jest.fn(),
|
|
||||||
applySceneToShots: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock UseCase模块
|
|
||||||
jest.mock('../usecase/SceneEditUseCase');
|
|
||||||
jest.mock('../usecase/TextEditUseCase');
|
|
||||||
jest.mock('../usecase/TagEditUseCase');
|
|
||||||
|
|
||||||
// Mock Domain模块
|
|
||||||
jest.mock('../domain/Item', () => ({
|
|
||||||
SceneItem: jest.fn(),
|
|
||||||
TextItem: jest.fn(),
|
|
||||||
TagItem: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('SceneService 业务逻辑测试', () => {
|
|
||||||
let mockSceneEditUseCase: jest.Mocked<SceneEditUseCase>;
|
|
||||||
let mockTextEditUseCase: jest.Mocked<TextEditUseCase>;
|
|
||||||
let mockTagEditUseCase: jest.Mocked<TagEditUseCase>;
|
|
||||||
|
|
||||||
// 测试数据
|
|
||||||
const mockSceneEntity: SceneEntity = {
|
|
||||||
id: 'scene1',
|
|
||||||
name: '测试场景',
|
|
||||||
imageUrl: 'http://example.com/scene1.jpg',
|
|
||||||
tagIds: ['tag1', 'tag2'],
|
|
||||||
generateTextId: 'text1',
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
loadingProgress: 100,
|
|
||||||
disableEdit: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockTextEntity: AITextEntity = {
|
|
||||||
id: 'text1',
|
|
||||||
content: '这是AI生成的场景文本内容',
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
loadingProgress: 100,
|
|
||||||
disableEdit: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockTagValueObject1: TagValueObject = {
|
|
||||||
id: 'tag1',
|
|
||||||
name: '场景标签1',
|
|
||||||
content: '场景标签内容1',
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
loadingProgress: 100,
|
|
||||||
disableEdit: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockTagValueObject2: TagValueObject = {
|
|
||||||
id: 'tag2',
|
|
||||||
name: '场景标签2',
|
|
||||||
content: '场景标签内容2',
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
loadingProgress: 100,
|
|
||||||
disableEdit: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockShotEntity: VideoSegmentEntity = {
|
|
||||||
id: 'shot1',
|
|
||||||
name: '分镜1',
|
|
||||||
sketchUrl: 'http://example.com/sketch1.jpg',
|
|
||||||
videoUrl: ['http://example.com/video1.mp4'],
|
|
||||||
roleList: [],
|
|
||||||
sceneList: [],
|
|
||||||
content: [],
|
|
||||||
status: ShotStatus.sketchLoading,
|
|
||||||
shot: [],
|
|
||||||
scriptId: 'script1',
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
loadingProgress: 100,
|
|
||||||
disableEdit: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
// 设置Mock UseCase实例
|
|
||||||
mockSceneEditUseCase = {
|
|
||||||
AIgenerateScene: jest.fn(),
|
|
||||||
applyScene: jest.fn(),
|
|
||||||
refreshSceneData: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
mockTextEditUseCase = {
|
|
||||||
getOptimizedContent: jest.fn(),
|
|
||||||
updateText: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
mockTagEditUseCase = {
|
|
||||||
updateTag: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
// 设置Mock构造函数
|
|
||||||
(SceneEditUseCase as jest.MockedClass<typeof SceneEditUseCase>).mockImplementation(() => mockSceneEditUseCase);
|
|
||||||
(TextEditUseCase as jest.MockedClass<typeof TextEditUseCase>).mockImplementation(() => mockTextEditUseCase);
|
|
||||||
(TagEditUseCase as jest.MockedClass<typeof TagEditUseCase>).mockImplementation(() => mockTagEditUseCase);
|
|
||||||
|
|
||||||
// 设置Mock Item构造函数
|
|
||||||
(SceneItem as jest.MockedClass<typeof SceneItem>).mockImplementation((entity) => ({
|
|
||||||
entity,
|
|
||||||
metadata: {},
|
|
||||||
disableEdit: entity.disableEdit,
|
|
||||||
type: 3,
|
|
||||||
} as any));
|
|
||||||
|
|
||||||
(TextItem as jest.MockedClass<typeof TextItem>).mockImplementation((entity) => ({
|
|
||||||
entity,
|
|
||||||
metadata: {},
|
|
||||||
disableEdit: entity.disableEdit,
|
|
||||||
type: 0,
|
|
||||||
} as any));
|
|
||||||
|
|
||||||
(TagItem as jest.MockedClass<typeof TagItem>).mockImplementation((entity) => ({
|
|
||||||
entity,
|
|
||||||
metadata: {},
|
|
||||||
disableEdit: entity.disableEdit,
|
|
||||||
type: 2,
|
|
||||||
} as any));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('数据初始化测试', () => {
|
|
||||||
it('应该成功获取场景列表', async () => {
|
|
||||||
const mockScenes = [mockSceneEntity];
|
|
||||||
(getSceneList as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: mockScenes,
|
|
||||||
message: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getSceneList({ projectId: 'project1' });
|
|
||||||
|
|
||||||
expect(getSceneList).toHaveBeenCalledWith({ projectId: 'project1' });
|
|
||||||
expect(result.successful).toBe(true);
|
|
||||||
expect(result.data).toEqual(mockScenes);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('获取场景列表失败时应该返回错误信息', async () => {
|
|
||||||
(getSceneList as jest.Mock).mockResolvedValue({
|
|
||||||
successful: false,
|
|
||||||
message: '获取失败',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getSceneList({ projectId: 'project1' });
|
|
||||||
|
|
||||||
expect(result.successful).toBe(false);
|
|
||||||
expect(result.message).toBe('获取失败');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该成功获取场景数据', async () => {
|
|
||||||
(getSceneData as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: {
|
|
||||||
text: mockTextEntity,
|
|
||||||
tags: [mockTagValueObject1, mockTagValueObject2],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getSceneData({ sceneId: 'scene1' });
|
|
||||||
|
|
||||||
expect(getSceneData).toHaveBeenCalledWith({ sceneId: 'scene1' });
|
|
||||||
expect(result.successful).toBe(true);
|
|
||||||
expect(result.data.text).toEqual(mockTextEntity);
|
|
||||||
expect(result.data.tags).toEqual([mockTagValueObject1, mockTagValueObject2]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('修改文本和标签测试', () => {
|
|
||||||
it('应该成功修改AI文本', async () => {
|
|
||||||
const updatedTextEntity = { ...mockTextEntity, content: '更新后的场景文本' };
|
|
||||||
(updateText as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: updatedTextEntity,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await updateText({
|
|
||||||
textId: 'text1',
|
|
||||||
content: '新的场景文本内容'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(updateText).toHaveBeenCalledWith({
|
|
||||||
textId: 'text1',
|
|
||||||
content: '新的场景文本内容'
|
|
||||||
});
|
|
||||||
expect(result.successful).toBe(true);
|
|
||||||
expect(result.data.content).toBe('更新后的场景文本');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该成功修改标签内容', async () => {
|
|
||||||
const updatedTagValueObject = { ...mockTagValueObject1, content: '更新后的场景标签' };
|
|
||||||
(updateTag as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: updatedTagValueObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await updateTag({
|
|
||||||
tagId: 'tag1',
|
|
||||||
content: '新的场景标签内容'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(updateTag).toHaveBeenCalledWith({
|
|
||||||
tagId: 'tag1',
|
|
||||||
content: '新的场景标签内容'
|
|
||||||
});
|
|
||||||
expect(result.successful).toBe(true);
|
|
||||||
expect(result.data.content).toBe('更新后的场景标签');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('文本AI优化测试', () => {
|
|
||||||
it('应该成功优化AI文本', async () => {
|
|
||||||
const optimizedContent = '优化后的场景文本内容';
|
|
||||||
const updatedTextEntity = { ...mockTextEntity, content: optimizedContent };
|
|
||||||
|
|
||||||
mockTextEditUseCase.getOptimizedContent.mockResolvedValue(optimizedContent);
|
|
||||||
mockTextEditUseCase.updateText.mockResolvedValue({
|
|
||||||
entity: updatedTextEntity,
|
|
||||||
metadata: {},
|
|
||||||
disableEdit: false,
|
|
||||||
type: 0,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
(updateText as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: updatedTextEntity,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 模拟优化流程
|
|
||||||
const optimizedContentResult = await mockTextEditUseCase.getOptimizedContent();
|
|
||||||
const updateResult = await mockTextEditUseCase.updateText(optimizedContentResult);
|
|
||||||
|
|
||||||
expect(mockTextEditUseCase.getOptimizedContent).toHaveBeenCalled();
|
|
||||||
expect(mockTextEditUseCase.updateText).toHaveBeenCalledWith(optimizedContent);
|
|
||||||
expect(updateResult.entity.content).toBe(optimizedContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('没有文本内容时优化应该抛出错误', async () => {
|
|
||||||
const emptyTextEntity = { ...mockTextEntity, content: '' };
|
|
||||||
mockTextEditUseCase.getOptimizedContent.mockRejectedValue(new Error('没有可优化的文本内容'));
|
|
||||||
|
|
||||||
await expect(mockTextEditUseCase.getOptimizedContent()).rejects.toThrow('没有可优化的文本内容');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('重新生成场景测试', () => {
|
|
||||||
it('应该成功重新生成场景', async () => {
|
|
||||||
const newSceneEntity = { ...mockSceneEntity, id: 'scene2', name: '新场景' };
|
|
||||||
(regenerateScene as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: newSceneEntity,
|
|
||||||
});
|
|
||||||
|
|
||||||
mockSceneEditUseCase.AIgenerateScene.mockResolvedValue(newSceneEntity);
|
|
||||||
|
|
||||||
const result = await regenerateScene({
|
|
||||||
prompt: '重新生成场景',
|
|
||||||
tagTypes: ['tag1', 'tag2'],
|
|
||||||
sceneId: 'scene1'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(regenerateScene).toHaveBeenCalledWith({
|
|
||||||
prompt: '重新生成场景',
|
|
||||||
tagTypes: ['tag1', 'tag2'],
|
|
||||||
sceneId: 'scene1'
|
|
||||||
});
|
|
||||||
expect(result.successful).toBe(true);
|
|
||||||
expect(result.data.id).toBe('scene2');
|
|
||||||
expect(result.data.name).toBe('新场景');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('重新生成场景失败时应该返回错误信息', async () => {
|
|
||||||
(regenerateScene as jest.Mock).mockResolvedValue({
|
|
||||||
successful: false,
|
|
||||||
message: '重新生成失败',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await regenerateScene({
|
|
||||||
prompt: '重新生成场景',
|
|
||||||
tagTypes: ['tag1', 'tag2'],
|
|
||||||
sceneId: 'scene1'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.successful).toBe(false);
|
|
||||||
expect(result.message).toBe('重新生成失败');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('场景业务流程测试', () => {
|
|
||||||
it('应该完成完整的场景编辑流程:获取列表→选择场景→修改提示词→智能优化→修改标签→重新生成→应用场景', async () => {
|
|
||||||
// 模拟用户操作:获取场景列表
|
|
||||||
const mockScenes = [mockSceneEntity];
|
|
||||||
(getSceneList as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: mockScenes,
|
|
||||||
message: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
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: [mockTagValueObject1, mockTagValueObject2],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const sceneDataResult = await getSceneData({ sceneId: 'scene1' });
|
|
||||||
expect(sceneDataResult.successful).toBe(true);
|
|
||||||
expect(sceneDataResult.data.text).toEqual(mockTextEntity);
|
|
||||||
expect(sceneDataResult.data.tags).toEqual([mockTagValueObject1, mockTagValueObject2]);
|
|
||||||
|
|
||||||
// 模拟用户操作:修改场景提示词
|
|
||||||
const updatedTextEntity = { ...mockTextEntity, content: '修改后的场景提示词' };
|
|
||||||
(updateText as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: updatedTextEntity,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateTextResult = await updateText({
|
|
||||||
textId: 'text1',
|
|
||||||
content: '修改后的场景提示词'
|
|
||||||
});
|
|
||||||
expect(updateTextResult.successful).toBe(true);
|
|
||||||
expect(updateTextResult.data.content).toBe('修改后的场景提示词');
|
|
||||||
|
|
||||||
// 模拟用户操作:智能优化文本
|
|
||||||
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 updatedTagValueObject = { ...mockTagValueObject1, content: '修改后的标签内容' };
|
|
||||||
(updateTag as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: updatedTagValueObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
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: [mockTagValueObject1],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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('应用场景失败');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('场景应用到多个分镜测试', () => {
|
|
||||||
it('应该成功获取场景分镜列表', async () => {
|
|
||||||
const mockShots = [mockShotEntity];
|
|
||||||
(getSceneShots as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: {
|
|
||||||
shots: mockShots,
|
|
||||||
appliedShotIds: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getSceneShots({ sceneId: 'scene1' });
|
|
||||||
|
|
||||||
expect(getSceneShots).toHaveBeenCalledWith({ sceneId: 'scene1' });
|
|
||||||
expect(result.successful).toBe(true);
|
|
||||||
expect(result.data.shots).toEqual(mockShots);
|
|
||||||
expect(result.data.appliedShotIds).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该成功应用场景到选中的分镜', async () => {
|
|
||||||
(applySceneToShots as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: { success: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
mockSceneEditUseCase.applyScene.mockResolvedValue({} as any);
|
|
||||||
|
|
||||||
const result = await applySceneToShots({
|
|
||||||
sceneId: 'scene1',
|
|
||||||
shotIds: ['shot1', 'shot2']
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(applySceneToShots).toHaveBeenCalledWith({
|
|
||||||
sceneId: 'scene1',
|
|
||||||
shotIds: ['shot1', 'shot2']
|
|
||||||
});
|
|
||||||
expect(result.successful).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应用场景失败时应该返回错误信息', async () => {
|
|
||||||
(applySceneToShots as jest.Mock).mockResolvedValue({
|
|
||||||
successful: false,
|
|
||||||
message: '应用失败',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await applySceneToShots({
|
|
||||||
sceneId: 'scene1',
|
|
||||||
shotIds: ['shot1']
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.successful).toBe(false);
|
|
||||||
expect(result.message).toBe('应用失败');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该正确处理已应用的分镜状态', async () => {
|
|
||||||
const mockShots = [mockShotEntity];
|
|
||||||
(getSceneShots as jest.Mock).mockResolvedValue({
|
|
||||||
successful: true,
|
|
||||||
data: {
|
|
||||||
shots: mockShots,
|
|
||||||
appliedShotIds: ['shot1'], // 分镜1已应用
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getSceneShots({ sceneId: 'scene1' });
|
|
||||||
|
|
||||||
expect(result.data.appliedShotIds).toEqual(['shot1']);
|
|
||||||
expect(result.data.shots).toEqual(mockShots);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('UseCase业务逻辑测试', () => {
|
|
||||||
it('SceneEditUseCase应该正确初始化', () => {
|
|
||||||
const sceneItem = new SceneItem(mockSceneEntity);
|
|
||||||
const useCase = new SceneEditUseCase(sceneItem);
|
|
||||||
|
|
||||||
expect(SceneEditUseCase).toHaveBeenCalledWith(sceneItem);
|
|
||||||
expect(useCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('TextEditUseCase应该正确初始化', () => {
|
|
||||||
const textItem = new TextItem(mockTextEntity);
|
|
||||||
const useCase = new TextEditUseCase(textItem);
|
|
||||||
|
|
||||||
expect(TextEditUseCase).toHaveBeenCalledWith(textItem);
|
|
||||||
expect(useCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('TagEditUseCase应该正确初始化', () => {
|
|
||||||
const tagItem = new TagItem(mockTagValueObject1);
|
|
||||||
const useCase = new TagEditUseCase(tagItem);
|
|
||||||
|
|
||||||
expect(TagEditUseCase).toHaveBeenCalledWith(tagItem);
|
|
||||||
expect(useCase).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Domain实体测试', () => {
|
|
||||||
it('SceneItem应该正确包装SceneEntity', () => {
|
|
||||||
const sceneItem = new SceneItem(mockSceneEntity);
|
|
||||||
|
|
||||||
expect(SceneItem).toHaveBeenCalledWith(mockSceneEntity);
|
|
||||||
expect(sceneItem.entity).toEqual(mockSceneEntity);
|
|
||||||
expect(sceneItem.disableEdit).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('TextItem应该正确包装AITextEntity', () => {
|
|
||||||
const textItem = new TextItem(mockTextEntity);
|
|
||||||
|
|
||||||
expect(TextItem).toHaveBeenCalledWith(mockTextEntity);
|
|
||||||
expect(textItem.entity).toEqual(mockTextEntity);
|
|
||||||
expect(textItem.disableEdit).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('TagItem应该正确包装TagValueObject', () => {
|
|
||||||
const tagItem = new TagItem(mockTagValueObject1);
|
|
||||||
|
|
||||||
expect(TagItem).toHaveBeenCalledWith(mockTagValueObject1);
|
|
||||||
expect(tagItem.entity).toEqual(mockTagValueObject1);
|
|
||||||
expect(tagItem.disableEdit).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('错误处理测试', () => {
|
|
||||||
it('API调用失败时应该正确处理错误', async () => {
|
|
||||||
(getSceneList as jest.Mock).mockRejectedValue(new Error('网络错误'));
|
|
||||||
|
|
||||||
await expect(getSceneList({ projectId: 'project1' })).rejects.toThrow('网络错误');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('API返回失败状态时应该正确处理', async () => {
|
|
||||||
(getSceneList as jest.Mock).mockResolvedValue({
|
|
||||||
successful: false,
|
|
||||||
message: '服务器错误',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getSceneList({ projectId: 'project1' });
|
|
||||||
|
|
||||||
expect(result.successful).toBe(false);
|
|
||||||
expect(result.message).toBe('服务器错误');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('UseCase未初始化时应该抛出相应错误', async () => {
|
|
||||||
const sceneItem = new SceneItem(mockSceneEntity);
|
|
||||||
const useCase = new SceneEditUseCase(sceneItem);
|
|
||||||
|
|
||||||
// 模拟UseCase未初始化的情况
|
|
||||||
mockSceneEditUseCase.AIgenerateScene.mockRejectedValue(new Error('场景编辑UseCase未初始化'));
|
|
||||||
|
|
||||||
await expect(useCase.AIgenerateScene({} as any, [])).rejects.toThrow('场景编辑UseCase未初始化');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('场景数据完整性测试', () => {
|
|
||||||
it('应该验证场景实体的完整性', () => {
|
|
||||||
const sceneItem = new SceneItem(mockSceneEntity);
|
|
||||||
|
|
||||||
expect(sceneItem.entity.id).toBe('scene1');
|
|
||||||
expect(sceneItem.entity.name).toBe('测试场景');
|
|
||||||
expect(sceneItem.entity.imageUrl).toBe('http://example.com/scene1.jpg');
|
|
||||||
expect(sceneItem.entity.tagIds).toEqual(['tag1', 'tag2']);
|
|
||||||
expect(sceneItem.entity.generateTextId).toBe('text1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该验证文本实体的完整性', () => {
|
|
||||||
const textItem = new TextItem(mockTextEntity);
|
|
||||||
|
|
||||||
expect(textItem.entity.id).toBe('text1');
|
|
||||||
expect(textItem.entity.content).toBe('这是AI生成的场景文本内容');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该验证标签实体的完整性', () => {
|
|
||||||
const tagItem = new TagItem(mockTagValueObject1);
|
|
||||||
|
|
||||||
expect(tagItem.entity.id).toBe('tag1');
|
|
||||||
expect(tagItem.entity.name).toBe('场景标签1');
|
|
||||||
expect(tagItem.entity.content).toBe('场景标签内容1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
saveRegeneratedCharacter,
|
saveRegeneratedCharacter,
|
||||||
getSimilarCharacters,
|
getSimilarCharacters,
|
||||||
checkShotVideoStatus,
|
checkShotVideoStatus,
|
||||||
|
analyzeImageDescription,
|
||||||
} from '@/api/video_flow';
|
} from '@/api/video_flow';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,9 +72,9 @@ export class RoleEditUseCase {
|
|||||||
tags: [], // 默认为空标签数组
|
tags: [], // 默认为空标签数组
|
||||||
imageUrl: char.image_path || '', // 使用API返回的图片路径
|
imageUrl: char.image_path || '', // 使用API返回的图片路径
|
||||||
loadingProgress: 100, // 默认加载完成
|
loadingProgress: 100, // 默认加载完成
|
||||||
disableEdit: false, // 默认允许编辑
|
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
fromDraft: false
|
fromDraft: false,
|
||||||
|
isChangeRole: false
|
||||||
};
|
};
|
||||||
|
|
||||||
return roleEntity;
|
return roleEntity;
|
||||||
@ -123,14 +124,13 @@ export class RoleEditUseCase {
|
|||||||
/** 内容 */
|
/** 内容 */
|
||||||
content: highlight,
|
content: highlight,
|
||||||
loadingProgress: 100,
|
loadingProgress: 100,
|
||||||
disableEdit: false
|
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
imageUrl: char.image_path || '',
|
imageUrl: char.image_path || '',
|
||||||
loadingProgress: 100,
|
loadingProgress: 100,
|
||||||
disableEdit: false,
|
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
fromDraft: false
|
fromDraft: false,
|
||||||
|
isChangeRole: false
|
||||||
};
|
};
|
||||||
|
|
||||||
return roleEntity;
|
return roleEntity;
|
||||||
@ -159,9 +159,9 @@ export class RoleEditUseCase {
|
|||||||
tags: [], // 相似角色接口可能不返回标签,暂时为空
|
tags: [], // 相似角色接口可能不返回标签,暂时为空
|
||||||
imageUrl: char.avatar || '',
|
imageUrl: char.avatar || '',
|
||||||
loadingProgress: 100,
|
loadingProgress: 100,
|
||||||
disableEdit: false,
|
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
fromDraft: false
|
fromDraft: false,
|
||||||
|
isChangeRole: false
|
||||||
};
|
};
|
||||||
|
|
||||||
return roleEntity;
|
return roleEntity;
|
||||||
@ -249,13 +249,12 @@ export class RoleEditUseCase {
|
|||||||
name: highlight,
|
name: highlight,
|
||||||
content: highlight,
|
content: highlight,
|
||||||
loadingProgress: 100,
|
loadingProgress: 100,
|
||||||
disableEdit: false
|
|
||||||
})), // 将高亮关键词转换为TagValueObject格式
|
})), // 将高亮关键词转换为TagValueObject格式
|
||||||
imageUrl: characterData.image_url || '',
|
imageUrl: characterData.image_url || '',
|
||||||
loadingProgress: 100,
|
loadingProgress: 100,
|
||||||
disableEdit: false,
|
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
fromDraft: false
|
fromDraft: false,
|
||||||
|
isChangeRole: true
|
||||||
};
|
};
|
||||||
return roleEntity;
|
return roleEntity;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -315,9 +314,6 @@ export class RoleEditUseCase {
|
|||||||
*/
|
*/
|
||||||
async optimizeRoleDescription( selectedRole: RoleEntity): Promise<{optimizedDescription: string, keywords: string[]}> {
|
async optimizeRoleDescription( selectedRole: RoleEntity): Promise<{optimizedDescription: string, keywords: string[]}> {
|
||||||
try {
|
try {
|
||||||
// if (!this.selectedRole) {
|
|
||||||
// throw new Error('请先选择角色');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 调用新的AI优化角色描述API
|
// 调用新的AI优化角色描述API
|
||||||
const response = await generateCharacterDescription({
|
const response = await generateCharacterDescription({
|
||||||
@ -346,49 +342,6 @@ export class RoleEditUseCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @description: 从AI文本描述中解析标签信息(预留函数,未来实现)
|
|
||||||
* @param aiText AI文本描述
|
|
||||||
* @returns Promise<string[]> 解析出的标签列表
|
|
||||||
*/
|
|
||||||
async parseTagsFromAiText(aiText: string): Promise<string[]> {
|
|
||||||
// TODO: 未来实现从AI文本中解析标签的逻辑
|
|
||||||
// 例如:解析文本中的关键词、特征描述等作为标签
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 根据图片地址获取角色实体数据
|
|
||||||
* @param imageUrl 图片地址
|
|
||||||
* @returns Promise<RoleEntity> 角色实体数据
|
|
||||||
*/
|
|
||||||
async getRoleByImage(imageUrl: string): Promise<RoleEntity> {
|
|
||||||
try {
|
|
||||||
// TODO: 调用后端API,根据图片地址获取角色数据
|
|
||||||
// 这里需要根据实际的后端API接口来实现
|
|
||||||
// const response = await getRoleByImage({ imageUrl });
|
|
||||||
|
|
||||||
// 临时实现:返回一个模拟的角色实体
|
|
||||||
// 实际使用时需要替换为真实的API调用
|
|
||||||
const mockRole: RoleEntity = {
|
|
||||||
id: `role_${Date.now()}`,
|
|
||||||
name: '从图片识别的角色',
|
|
||||||
generateText: '通过图片识别生成的角色描述',
|
|
||||||
tags: [], // 空标签数组
|
|
||||||
imageUrl: imageUrl, // 使用传入的图片地址
|
|
||||||
loadingProgress: 100, // 加载完成
|
|
||||||
disableEdit: false, // 允许编辑
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
fromDraft: false
|
|
||||||
};
|
|
||||||
|
|
||||||
return mockRole;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('根据图片获取角色失败:', error);
|
|
||||||
throw new Error('根据图片获取角色失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 保存重新生成的角色到角色库
|
* @description 保存重新生成的角色到角色库
|
||||||
* @param roleData 角色实体数据
|
* @param roleData 角色实体数据
|
||||||
@ -451,4 +404,56 @@ export class RoleEditUseCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 分析图片并更新角色信息
|
||||||
|
* @param imageUrl 图片URL地址
|
||||||
|
* @param selectedRole 当前选中的角色
|
||||||
|
* @returns Promise<RoleEntity> 更新后的角色实体
|
||||||
|
*/
|
||||||
|
async analyzeImageAndUpdateRole(imageUrl: string, selectedRole: RoleEntity): Promise<RoleEntity> {
|
||||||
|
try {
|
||||||
|
// 调用图片分析接口获取描述
|
||||||
|
const result = await analyzeImageDescription({
|
||||||
|
image_url: imageUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.successful) {
|
||||||
|
throw new Error(`图片分析失败: ${result.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { description, highlights } = result.data;
|
||||||
|
|
||||||
|
// 更新当前选中角色的图片、描述和标签
|
||||||
|
const updatedRole: RoleEntity = {
|
||||||
|
...selectedRole,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
generateText: description,
|
||||||
|
tags: highlights.map((highlight: string, index: number) => ({
|
||||||
|
id: `tag_${Date.now()}_${index}`,
|
||||||
|
/** 名称 */
|
||||||
|
name: highlight,
|
||||||
|
/** 内容 */
|
||||||
|
content: highlight,
|
||||||
|
loadingProgress: 100,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新角色列表中的对应角色
|
||||||
|
if (Array.isArray(this.roleList)) {
|
||||||
|
this.roleList = this.roleList.map(role =>
|
||||||
|
role.id === selectedRole.id ? updatedRole : role
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前选中的角色
|
||||||
|
this.selectedRole = updatedRole;
|
||||||
|
|
||||||
|
return updatedRole;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('分析图片并更新角色失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,85 +0,0 @@
|
|||||||
|
|
||||||
import { TagValueObject } from '../domain/valueObject';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 标签编辑用例
|
|
||||||
* 负责标签内容的初始化、修改和优化
|
|
||||||
*/
|
|
||||||
export class TagEditUseCase {
|
|
||||||
constructor(public tagList: TagValueObject[]) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改标签内容
|
|
||||||
* @param tagName 标签名称
|
|
||||||
* @param newContent 新内容
|
|
||||||
*/
|
|
||||||
async updateTag(tagName: string, newContent: string | number): Promise<void> {
|
|
||||||
const tag = this.tagList.find(tag => tag.name === tagName);
|
|
||||||
if (tag) {
|
|
||||||
tag.content = newContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新增标签
|
|
||||||
* @param tagName 标签名称
|
|
||||||
* @param content 标签内容
|
|
||||||
*/
|
|
||||||
async addTag(tagName: string, content: string | number = ""): Promise<void> {
|
|
||||||
// 检查标签是否已存在
|
|
||||||
const existingTag = this.tagList.find(tag => tag.name === tagName);
|
|
||||||
if (existingTag) {
|
|
||||||
throw new Error(`标签 "${tagName}" 已存在`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新标签
|
|
||||||
const newTag: TagValueObject = {
|
|
||||||
id: `tag_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
name: tagName,
|
|
||||||
content: content,
|
|
||||||
loadingProgress: 100,
|
|
||||||
disableEdit: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this.tagList.push(newTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除指定标签
|
|
||||||
* @param tagName 要删除的标签名称
|
|
||||||
*/
|
|
||||||
async deleteTag(tagName: string): Promise<void> {
|
|
||||||
const tagIndex = this.tagList.findIndex(tag => tag.name === tagName);
|
|
||||||
if (tagIndex === -1) {
|
|
||||||
throw new Error(`标签 "${tagName}" 不存在`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tagList.splice(tagIndex, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空所有标签
|
|
||||||
*/
|
|
||||||
async clearAllTags(): Promise<void> {
|
|
||||||
this.tagList.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有标签
|
|
||||||
* @returns 标签列表
|
|
||||||
*/
|
|
||||||
getTagList(): TagValueObject[] {
|
|
||||||
return [...this.tagList];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据标签名称获取标签
|
|
||||||
* @param tagName 标签名称
|
|
||||||
* @returns 标签对象或undefined
|
|
||||||
*/
|
|
||||||
getTagByName(tagName: string): TagValueObject | undefined {
|
|
||||||
return this.tagList.find(tag => tag.name === tagName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user