3f4dba97ba
调整编辑器 API 鉴权策略,允许通过 refresh cookie 静默补 access token 真实生图遇到未授权时显示登录提示,不再直接暴露 requestId 补充编辑器组件和客户端测试,并更新方案文档与跟踪记录
269 lines
8.1 KiB
TypeScript
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 },
|
|
}),
|
|
);
|
|
});
|
|
});
|