Files
Genarrative/src/services/image-editor/editorProjectClient.test.ts
T
2026-06-19 14:18:56 +08:00

1134 lines
33 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
createEditorAsset,
createEditorAssetFolder,
createEditorProject,
createEditorProjectResource,
deleteEditorAsset,
deleteEditorAssetFolder,
deleteEditorProject,
editEditorImage,
extractEditorUiDesignAssets,
generateEditorBackgroundMusic,
generateEditorCharacterAnimation,
generateEditorIconSpritesheet,
generateEditorImage,
generateEditorSoundEffect,
generateEditorVideo,
listEditorProjects,
loadEditorAssetLibrary,
loadEditorProject,
loadOrCreateRecentEditorProject,
renameEditorProject,
saveEditorProjectLayout,
updateEditorAsset,
updateEditorAssetFolder,
} from './editorProjectClient';
const requestJsonMock = vi.hoisted(() => vi.fn());
vi.mock('../apiClient', () => ({
requestJson: requestJsonMock,
}));
describe('editorProjectClient', () => {
afterEach(() => {
requestJsonMock.mockReset();
});
it('loads the recent project without creating a duplicate when it exists', async () => {
requestJsonMock.mockResolvedValueOnce({
project: {
projectId: 'editor-project-1',
title: '未命名画布',
canvas: {
canvasId: 'editor-project-1:canvas:default',
projectId: 'editor-project-1',
title: '默认画布',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
});
const project = await loadOrCreateRecentEditorProject();
expect(project.projectId).toBe('editor-project-1');
expect(requestJsonMock).toHaveBeenCalledTimes(1);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/projects/recent',
{ method: 'GET' },
'读取图片画布工程失败',
);
});
it('creates a default project when there is no recent project', async () => {
requestJsonMock
.mockResolvedValueOnce({ project: null })
.mockResolvedValueOnce({
project: {
projectId: 'editor-project-created',
title: '未命名画布',
canvas: {
canvasId: 'editor-project-created:canvas:default',
projectId: 'editor-project-created',
title: '默认画布',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
});
const project = await loadOrCreateRecentEditorProject();
expect(project.projectId).toBe('editor-project-created');
expect(requestJsonMock).toHaveBeenNthCalledWith(
2,
'/api/editor/projects',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '未命名画布' }),
}),
'创建图片画布工程失败',
);
});
it('saves viewport and layer layout through the project API', async () => {
requestJsonMock.mockResolvedValueOnce({
project: {
projectId: 'editor-project-1',
title: '未命名画布',
canvas: {
canvasId: 'editor-project-1:canvas:default',
projectId: 'editor-project-1',
title: '默认画布',
viewport: { x: 12, y: 24, scale: 0.5 },
layers: [
{ layerId: 'layer-1', resourceId: 'resource-1', x: 10, y: 20 },
],
updatedAt: '2026-06-12T00:00:00.000Z',
},
viewport: { x: 12, y: 24, scale: 0.5 },
layers: [
{ layerId: 'layer-1', resourceId: 'resource-1', x: 10, y: 20 },
],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
});
await saveEditorProjectLayout('editor-project-1', {
viewport: { x: 12, y: 24, scale: 0.5 },
layers: [{ layerId: 'layer-1', resourceId: 'resource-1', x: 10, y: 20 }],
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/projects/editor-project-1',
expect.objectContaining({
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
viewport: { x: 12, y: 24, scale: 0.5 },
layers: [
{ layerId: 'layer-1', resourceId: 'resource-1', x: 10, y: 20 },
],
}),
}),
'保存图片画布工程失败',
);
});
it('lists editor projects from the project API', async () => {
requestJsonMock.mockResolvedValueOnce({
projects: [
{
projectId: 'editor-project-1',
title: '角色设定板',
canvas: {
canvasId: 'editor-project-1:canvas:default',
projectId: 'editor-project-1',
title: '默认画布',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
],
});
const projects = await listEditorProjects();
expect(projects).toHaveLength(1);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/projects',
{ method: 'GET' },
'读取图片画布工程列表失败',
);
});
it('loads an explicit project by id', async () => {
requestJsonMock.mockResolvedValueOnce({
project: {
projectId: 'editor-project-1',
title: '角色设定板',
canvas: {
canvasId: 'editor-project-1:canvas:default',
projectId: 'editor-project-1',
title: '默认画布',
viewport: { x: 8, y: 9, scale: 1.5 },
layers: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
viewport: { x: 8, y: 9, scale: 1.5 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
});
const project = await loadEditorProject('editor-project-1');
expect(project.viewport.scale).toBe(1.5);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/projects/editor-project-1',
{ method: 'GET' },
'读取图片画布工程失败',
);
});
it('renames and deletes an editor project', async () => {
requestJsonMock
.mockResolvedValueOnce({
project: {
projectId: 'editor-project-1',
title: '新标题',
canvas: {
canvasId: 'editor-project-1:canvas:default',
projectId: 'editor-project-1',
title: '默认画布',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
})
.mockResolvedValueOnce({ deletedProjectId: 'editor-project-1' });
await renameEditorProject('editor-project-1', '新标题');
await deleteEditorProject('editor-project-1');
expect(requestJsonMock).toHaveBeenNthCalledWith(
1,
'/api/editor/projects/editor-project-1/metadata',
expect.objectContaining({
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '新标题' }),
}),
'重命名图片画布工程失败',
);
expect(requestJsonMock).toHaveBeenNthCalledWith(
2,
'/api/editor/projects/editor-project-1',
{ method: 'DELETE' },
'删除图片画布工程失败',
);
});
it('creates a resource with upload or generated metadata', async () => {
requestJsonMock.mockResolvedValueOnce({
resource: {
resourceId: 'resource-generated-1',
projectId: 'editor-project-1',
imageSrc: '/generated-editor-assets/project/image.png',
objectKey: 'generated-editor-assets/project/image.png',
width: 2048,
height: 2048,
sourceType: 'generated',
prompt: 'dragon knight',
model: 'gpt-image-2',
},
});
await createEditorProjectResource('editor-project-1', {
imageSrc: '/generated-editor-assets/project/image.png',
objectKey: 'generated-editor-assets/project/image.png',
width: 2048,
height: 2048,
sourceType: 'generated',
prompt: 'dragon knight',
model: 'gpt-image-2',
sourceResourceId: 'resource-source',
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/projects/editor-project-1/resources',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
imageSrc: '/generated-editor-assets/project/image.png',
objectKey: 'generated-editor-assets/project/image.png',
width: 2048,
height: 2048,
sourceType: 'generated',
prompt: 'dragon knight',
model: 'gpt-image-2',
sourceResourceId: 'resource-source',
}),
}),
'创建图片画布资源失败',
);
});
it('loads and mutates the account-level asset library', async () => {
requestJsonMock
.mockResolvedValueOnce({
library: {
folders: [
{
folderId: 'folder-project',
label: '项目素材',
sortOrder: 0,
collapsed: false,
systemDefault: true,
},
],
assets: [],
},
})
.mockResolvedValueOnce({
folder: {
folderId: 'folder-role',
label: '角色',
sortOrder: 100,
collapsed: false,
systemDefault: false,
},
})
.mockResolvedValueOnce({
folder: {
folderId: 'folder-role',
label: '角色参考',
sortOrder: 100,
collapsed: true,
systemDefault: false,
},
})
.mockResolvedValueOnce({
library: {
folders: [],
assets: [],
},
});
await loadEditorAssetLibrary();
await createEditorAssetFolder('角色', 100);
await updateEditorAssetFolder('folder-role', {
label: '角色参考',
collapsed: true,
});
await deleteEditorAssetFolder('folder-role');
expect(requestJsonMock).toHaveBeenNthCalledWith(
1,
'/api/editor/assets/library',
{ method: 'GET' },
'读取图片画布素材库失败',
);
expect(requestJsonMock).toHaveBeenNthCalledWith(
2,
'/api/editor/assets/folders',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ label: '角色', sortOrder: 100 }),
}),
'创建图片画布素材文件夹失败',
);
expect(requestJsonMock).toHaveBeenNthCalledWith(
3,
'/api/editor/assets/folders/folder-role',
expect.objectContaining({
method: 'PATCH',
body: JSON.stringify({ label: '角色参考', collapsed: true }),
}),
'更新图片画布素材文件夹失败',
);
expect(requestJsonMock).toHaveBeenNthCalledWith(
4,
'/api/editor/assets/folders/folder-role',
{ method: 'DELETE' },
'删除图片画布素材文件夹失败',
);
});
it('creates, updates, and deletes account-level image assets', async () => {
requestJsonMock
.mockResolvedValueOnce({
asset: {
assetId: 'asset-1',
folderId: 'folder-project',
label: '主视觉.png',
imageSrc: 'data:image/png;base64,ZmFrZQ==',
width: 640,
height: 480,
sourceType: 'uploaded',
},
})
.mockResolvedValueOnce({
asset: {
assetId: 'asset-1',
folderId: 'folder-role',
label: '角色主视觉.png',
imageSrc: 'data:image/png;base64,ZmFrZQ==',
width: 640,
height: 480,
sourceType: 'uploaded',
},
})
.mockResolvedValueOnce({
asset: {
assetId: 'asset-1',
folderId: 'folder-role',
label: '角色主视觉.png',
imageSrc: 'data:image/png;base64,ZmFrZQ==',
width: 640,
height: 480,
sourceType: 'uploaded',
},
});
await createEditorAsset({
folderId: 'folder-project',
label: '主视觉.png',
imageSrc: 'data:image/png;base64,ZmFrZQ==',
width: 640,
height: 480,
sourceType: 'uploaded',
});
await updateEditorAsset('asset-1', {
label: '角色主视觉.png',
folderId: 'folder-role',
});
await deleteEditorAsset('asset-1');
expect(requestJsonMock).toHaveBeenNthCalledWith(
1,
'/api/editor/assets',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
folderId: 'folder-project',
label: '主视觉.png',
imageSrc: 'data:image/png;base64,ZmFrZQ==',
width: 640,
height: 480,
sourceType: 'uploaded',
}),
}),
'创建图片画布素材失败',
);
expect(requestJsonMock).toHaveBeenNthCalledWith(
2,
'/api/editor/assets/asset-1',
expect.objectContaining({
method: 'PATCH',
body: JSON.stringify({
label: '角色主视觉.png',
folderId: 'folder-role',
}),
}),
'更新图片画布素材失败',
);
expect(requestJsonMock).toHaveBeenNthCalledWith(
3,
'/api/editor/assets/asset-1',
{ method: 'DELETE' },
'删除图片画布素材失败',
);
});
it('creates an explicit project from title input', async () => {
requestJsonMock.mockResolvedValueOnce({
project: {
projectId: 'editor-project-explicit',
title: '角色设定板',
canvas: {
canvasId: 'editor-project-explicit:canvas:default',
projectId: 'editor-project-explicit',
title: '默认画布',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
},
});
await createEditorProject({ title: '角色设定板' });
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/projects',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '角色设定板' }),
}),
'创建图片画布工程失败',
);
});
it('generates editor images through the backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
imageSrc: 'data:image/png;base64,abc',
width: 1024,
height: 1024,
sourceType: 'generated',
prompt: '一张画布图片',
actualPrompt: '一张画布图片',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'vector-task-1',
});
const result = await generateEditorImage({ prompt: '一张画布图片' });
expect(result.taskId).toBe('vector-task-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/images/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: '一张画布图片' }),
}),
'生成图片失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('passes image model options to editor image generation', async () => {
requestJsonMock.mockResolvedValueOnce({
imageSrc: 'data:image/png;base64,character',
width: 1024,
height: 1536,
sourceType: 'generated',
prompt: '角色设定',
actualPrompt: '角色设定',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'vector-character-1',
});
await generateEditorImage({
prompt: '角色设定',
kind: 'character',
model: 'gpt-image-2',
aspectRatio: '2:3',
imageSize: '1K',
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/images/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: '角色设定',
kind: 'character',
model: 'gpt-image-2',
aspectRatio: '2:3',
imageSize: '1K',
}),
}),
'生成图片失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('generates icon spritesheets through the dedicated backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
spritesheetImageSrc: 'data:image/png;base64,sheet',
spritesheetWidth: 512,
spritesheetHeight: 512,
iconImageSrcs: [
{
name: '返回按钮',
imageSrc: 'data:image/png;base64,back',
width: 96,
height: 96,
},
],
prompt: '图标素材 prompt',
actualPrompt: '图标素材 prompt',
model: 'gemini-3.1-flash-image-preview',
provider: 'VectorEngine',
taskId: 'icon-spritesheet-task-1',
priceMudPoints: 12,
});
const result = await generateEditorIconSpritesheet({
referenceImageSrc: 'data:image/png;base64,spec',
iconDescriptions: ['返回按钮', '设置按钮'],
priceMudPoints: 12,
});
expect(result.taskId).toBe('icon-spritesheet-task-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/icon-spritesheets/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
referenceImageSrc: 'data:image/png;base64,spec',
iconDescriptions: ['返回按钮', '设置按钮'],
model: 'gemini-3.1-flash-image-preview',
priceMudPoints: 12,
}),
}),
'生成图标素材失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('passes image model options to icon spritesheet generation', async () => {
requestJsonMock.mockResolvedValueOnce({
spritesheetImageSrc: 'data:image/png;base64,sheet',
spritesheetWidth: 1024,
spritesheetHeight: 1024,
iconImageSrcs: [],
prompt: '图标素材 prompt',
actualPrompt: '图标素材 prompt',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'icon-spritesheet-task-2',
priceMudPoints: 20,
});
await generateEditorIconSpritesheet({
referenceImageSrc: 'data:image/png;base64,spec',
iconDescriptions: ['返回按钮'],
model: 'gpt-image-2',
aspectRatio: '1:1',
imageSize: '2K',
priceMudPoints: 20,
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/icon-spritesheets/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
referenceImageSrc: 'data:image/png;base64,spec',
iconDescriptions: ['返回按钮'],
model: 'gpt-image-2',
aspectRatio: '1:1',
imageSize: '2K',
priceMudPoints: 20,
}),
}),
'生成图标素材失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('extracts UI design assets through the dedicated backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
spritesheetImageSrc: 'data:image/png;base64,ui-sheet',
spritesheetWidth: 1024,
spritesheetHeight: 1024,
iconImageSrcs: [
{
name: '素材 1',
imageSrc: 'data:image/png;base64,asset-1',
width: 128,
height: 128,
},
],
prompt: '提取画面中的所有独立并整理成spritesheet',
actualPrompt: '提取画面中的所有独立并整理成spritesheet',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'ui-asset-extraction-1',
priceMudPoints: 20,
});
const result = await extractEditorUiDesignAssets({
sourceImageSrc: 'data:image/png;base64,ui-design',
});
expect(result.taskId).toBe('ui-asset-extraction-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/ui-designs/assets/extractions',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sourceImageSrc: 'data:image/png;base64,ui-design',
}),
}),
'提取UI设计图素材失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('passes spec generation size and kind to the backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
imageSrc: 'data:image/png;base64,spec',
width: 2048,
height: 1152,
sourceType: 'generated',
prompt: '生成规范图',
actualPrompt: '生成规范图',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'vector-spec-1',
});
const result = await generateEditorImage({
prompt: '生成规范图',
size: '2048x1152',
kind: 'spec',
model: 'gpt-image-2',
});
expect(result.width).toBe(2048);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/images/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: '生成规范图',
size: '2048x1152',
kind: 'spec',
model: 'gpt-image-2',
}),
}),
'生成图片失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('passes publication material generation kind and size to the backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
imageSrc: 'data:image/png;base64,publication',
width: 720,
height: 1280,
sourceType: 'generated',
prompt: '宣发素材 prompt',
actualPrompt: '宣发素材 prompt',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'vector-publication-1',
});
const result = await generateEditorImage({
prompt: '宣发素材 prompt',
size: '720x1280',
kind: 'publication-material',
referenceImageSrcs: ['data:image/png;base64,ref'],
});
expect(result.height).toBe(1280);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/images/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: '宣发素材 prompt',
size: '720x1280',
kind: 'publication-material',
referenceImageSrcs: ['data:image/png;base64,ref'],
}),
}),
'生成图片失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('generates editor character animations through the backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
taskId: 'character-animation-1',
model: 'seedance2.0-fast',
prompt: '生成游戏角色动画\n动作描述:\n待机',
previewVideoPath: '/generated-character-drafts/editor/preview.mp4',
frames: [
{
frameIndex: 1,
imageSrc: '/generated-character-drafts/editor/frame01.png',
width: 1024,
height: 1024,
},
],
frameCount: 32,
durationSeconds: 4,
fps: 8,
priceMudPoints: 40,
});
const result = await generateEditorCharacterAnimation({
sourceLayerId: 'layer-character',
sourceImageSrc: 'data:image/png;base64,character',
sourceWidth: 1024,
sourceHeight: 1024,
promptText: '待机',
resolution: '480p',
ratio: 'same',
frameCount: 32,
durationSeconds: 4,
priceMudPoints: 40,
model: 'seedance2.0-fast',
});
expect(result.taskId).toBe('character-animation-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/character-animations/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sourceLayerId: 'layer-character',
sourceImageSrc: 'data:image/png;base64,character',
sourceWidth: 1024,
sourceHeight: 1024,
promptText: '待机',
resolution: '480p',
ratio: 'same',
frameCount: 32,
durationSeconds: 4,
priceMudPoints: 40,
model: 'seedance2.0-fast',
}),
}),
'生成角色动画失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('generates editor videos through the backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
videoSrc: '/generated-editor-videos/video-task-1/preview.mp4',
width: 1280,
height: 720,
sourceType: 'generated',
prompt: '让角色向镜头挥手',
actualPrompt: '让角色向镜头挥手',
model: 'kling3.0-omni',
provider: 'VectorEngine',
taskId: 'video-task-1',
durationSeconds: 5,
resolution: '480p',
priceMudPoints: 50,
});
const result = await generateEditorVideo({
prompt: '让角色向镜头挥手',
model: 'kling3.0-omni',
aspectRatio: '16:9',
durationSeconds: 5,
resolution: '480p',
mode: 'std',
sound: 'off',
priceMudPoints: 50,
});
expect(result.taskId).toBe('video-task-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/videos/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: '让角色向镜头挥手',
model: 'kling3.0-omni',
aspectRatio: '16:9',
durationSeconds: 5,
resolution: '480p',
mode: 'std',
sound: 'off',
priceMudPoints: 50,
}),
}),
'生成视频失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('passes seedance video multimodal references through the backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
videoSrc: '/generated-editor-videos/video-task-2/preview.mp4',
width: 1280,
height: 720,
sourceType: 'generated',
prompt: '参考素材生成视频',
actualPrompt: '参考素材生成视频',
model: 'seedance2.0-fast',
provider: 'VectorEngine',
taskId: 'video-task-2',
durationSeconds: 5,
resolution: '720p',
priceMudPoints: 100,
});
await generateEditorVideo({
prompt: '参考素材生成视频',
model: 'seedance2.0-fast',
aspectRatio: '16:9',
durationSeconds: 5,
resolution: '720p',
mode: 'std',
sound: 'off',
priceMudPoints: 100,
referenceImageSrcs: ['data:image/png;base64,image'],
referenceVideoSrcs: [
'generated-character-drafts/editor/seedance-references/video/video.mp4',
],
referenceAudioSrcs: [
'generated-character-drafts/editor/seedance-references/audio/audio.mp3',
],
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/videos/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: '参考素材生成视频',
model: 'seedance2.0-fast',
aspectRatio: '16:9',
durationSeconds: 5,
resolution: '720p',
mode: 'std',
sound: 'off',
priceMudPoints: 100,
referenceImageSrcs: ['data:image/png;base64,image'],
referenceVideoSrcs: [
'generated-character-drafts/editor/seedance-references/video/video.mp4',
],
referenceAudioSrcs: [
'generated-character-drafts/editor/seedance-references/audio/audio.mp3',
],
}),
}),
'生成视频失败',
expect.objectContaining({ timeoutMs: 1_200_000 }),
);
});
it('generates editor sound effects through the backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
audioSrc: '/generated-character-drafts/editor-audios/sfx.mp3',
width: 420,
height: 120,
sourceType: 'generated',
prompt: '金币掉落叮当声',
actualPrompt: '金币掉落叮当声',
model: 'audio1.0',
provider: 'VectorEngine',
taskId: 'sound-task-1',
priceMudPoints: 10,
audioKind: 'sound-effect',
});
const result = await generateEditorSoundEffect({
prompt: '金币掉落叮当声',
model: 'audio1.0',
duration: 7,
priceMudPoints: 10,
});
expect(result.taskId).toBe('sound-task-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/audios/sound-effects/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: '金币掉落叮当声',
model: 'audio1.0',
duration: 7,
priceMudPoints: 10,
}),
}),
'生成游戏音效失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('sends editor sound effect duration to Vidu instead of legacy type and tempo', async () => {
requestJsonMock.mockResolvedValueOnce({
audioSrc: '/generated-character-drafts/editor-audios/sfx-null.mp3',
width: 420,
height: 120,
sourceType: 'generated',
prompt: '按钮确认短促音',
actualPrompt: '按钮确认短促音',
model: 'audio1.0',
provider: 'VectorEngine',
taskId: 'sound-task-null',
priceMudPoints: 10,
audioKind: 'sound-effect',
});
await generateEditorSoundEffect({
prompt: '按钮确认短促音',
model: 'audio1.0',
duration: 5,
priceMudPoints: 10,
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/audios/sound-effects/generations',
expect.objectContaining({
body: JSON.stringify({
prompt: '按钮确认短促音',
model: 'audio1.0',
duration: 5,
priceMudPoints: 10,
}),
}),
'生成游戏音效失败',
expect.any(Object),
);
});
it('generates editor background music through the backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
audioSrc: '/generated-character-drafts/editor-audios/bgm.mp3',
width: 420,
height: 120,
sourceType: 'generated',
prompt: '森林冒险背景音乐',
actualPrompt: '森林冒险背景音乐',
model: 'chirp-v4',
provider: 'VectorEngine',
taskId: 'music-task-1',
priceMudPoints: 5,
audioKind: 'background-music',
});
const result = await generateEditorBackgroundMusic({
gptDescriptionPrompt: '森林冒险背景音乐',
makeInstrumental: true,
priceMudPoints: 5,
});
expect(result.taskId).toBe('music-task-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/audios/background-music/generations',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
gptDescriptionPrompt: '森林冒险背景音乐',
makeInstrumental: true,
priceMudPoints: 5,
}),
}),
'生成游戏背景音乐失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
it('edits editor images through the backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
imageSrc: 'data:image/png;base64,edited',
width: 1024,
height: 1024,
sourceType: 'generated',
prompt: '把画面改成黄昏光线',
actualPrompt: '把画面改成黄昏光线',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'vector-edit-1',
});
const result = await editEditorImage({
prompt: '把画面改成黄昏光线',
sourceImageSrc: 'data:image/png;base64,source',
size: '1024x1024',
model: 'gpt-image-2',
});
expect(result.taskId).toBe('vector-edit-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/images/edits',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: '把画面改成黄昏光线',
sourceImageSrc: 'data:image/png;base64,source',
size: '1024x1024',
model: 'gpt-image-2',
}),
}),
'修改图片失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
});