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 }, }), ); }); });