Files
Genarrative/src/services/image-editor/editorProjectClient.test.ts
T
kdletters 3f4dba97ba 修复图片编辑器生图登录提示
调整编辑器 API 鉴权策略,允许通过 refresh cookie 静默补 access token

真实生图遇到未授权时显示登录提示,不再直接暴露 requestId

补充编辑器组件和客户端测试,并更新方案文档与跟踪记录
2026-06-13 17:48:21 +08:00

269 lines
8.1 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
createEditorProject,
createEditorProjectResource,
editEditorImage,
generateEditorImage,
loadOrCreateRecentEditorProject,
saveEditorProjectLayout,
} 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: '未命名画布',
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' },
'读取图片画布工程失败',
expect.objectContaining({
clearAuthOnUnauthorized: false,
notifyAuthStateChange: false,
}),
);
});
it('creates a default project when there is no recent project', async () => {
requestJsonMock
.mockResolvedValueOnce({ project: null })
.mockResolvedValueOnce({
project: {
projectId: 'editor-project-created',
title: '未命名画布',
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: '未命名画布' }),
}),
'创建图片画布工程失败',
expect.objectContaining({
clearAuthOnUnauthorized: false,
notifyAuthStateChange: false,
}),
);
});
it('saves viewport and layer layout through the project API', async () => {
requestJsonMock.mockResolvedValueOnce({
project: {
projectId: 'editor-project-1',
title: '未命名画布',
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 }],
}),
}),
'保存图片画布工程失败',
expect.objectContaining({
clearAuthOnUnauthorized: false,
notifyAuthStateChange: false,
}),
);
});
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',
}),
}),
'创建图片画布资源失败',
expect.objectContaining({
clearAuthOnUnauthorized: false,
notifyAuthStateChange: false,
}),
);
});
it('creates an explicit project from title input', async () => {
requestJsonMock.mockResolvedValueOnce({
project: {
projectId: 'editor-project-explicit',
title: '角色设定板',
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: '角色设定板' }),
}),
'创建图片画布工程失败',
expect.objectContaining({
clearAuthOnUnauthorized: false,
notifyAuthStateChange: false,
}),
);
});
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({
clearAuthOnUnauthorized: false,
notifyAuthStateChange: false,
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',
});
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',
}),
}),
'修改图片失败',
expect.objectContaining({
clearAuthOnUnauthorized: false,
notifyAuthStateChange: false,
timeoutMs: 1_200_000,
retry: { maxRetries: 0 },
}),
);
});
});