/* @vitest-environment jsdom */ import { act, render, screen, waitFor } from '@testing-library/react'; import { useRef, useState } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { CanvasLayer, CanvasViewport } from './ImageCanvasEditorTypes'; import { useImageCanvasProjectPersistence } from './useImageCanvasProjectPersistence'; const createEditorProjectResourceMock = vi.hoisted(() => vi.fn()); const loadEditorProjectMock = vi.hoisted(() => vi.fn()); const loadOrCreateRecentEditorProjectMock = vi.hoisted(() => vi.fn()); const saveEditorProjectLayoutMock = vi.hoisted(() => vi.fn()); vi.mock('../../services/image-editor/editorProjectClient', async () => { const actual = await vi.importActual< typeof import('../../services/image-editor/editorProjectClient') >('../../services/image-editor/editorProjectClient'); return { ...actual, createEditorProjectResource: createEditorProjectResourceMock, loadEditorProject: loadEditorProjectMock, loadOrCreateRecentEditorProject: loadOrCreateRecentEditorProjectMock, saveEditorProjectLayout: saveEditorProjectLayoutMock, }; }); function createLayer(id: string): CanvasLayer { return { id, resourceId: `local-${id}`, title: '账号素材A', src: 'data:image/png;base64,YQ==', x: 10, y: 20, width: 320, height: 240, originalWidth: 320, originalHeight: 240, zIndex: 3, sourceType: 'uploaded', sourceAssetId: 'asset-a', }; } function ProjectPersistenceHarness() { const [layers, setLayers] = useState([]); const [viewport, setViewport] = useState({ x: 0, y: 0, scale: 1, }); const [projectTitle, setProjectTitle] = useState(''); const [projectRenameValue, setProjectRenameValue] = useState(''); const layersRef = useRef(layers); const viewportRef = useRef(viewport); const selectedLayerRef = useRef(null); const layerCounterRef = useRef(0); layersRef.current = layers; viewportRef.current = viewport; const persistence = useImageCanvasProjectPersistence({ refs: { layersRef, viewportRef, }, setters: { setProjectTitle, setProjectRenameValue, setViewport, setLayers, selectSingleLayer: (layerId) => { selectedLayerRef.current = layerId; }, setLayerCounter: (value) => { layerCounterRef.current = value; }, }, layers, viewport, openEditorLoginModal: vi.fn(), }); return (
{persistence.projectId ?? '-'} {projectTitle} {projectRenameValue} {layers.map((layer) => `${layer.id}:${layer.resourceId}`).join(',')} {selectedLayerRef.current ?? '-'} {layerCounterRef.current}
); } describe('useImageCanvasProjectPersistence', () => { beforeEach(() => { vi.clearAllMocks(); loadOrCreateRecentEditorProjectMock.mockResolvedValue({ projectId: 'editor-project-default', title: '空画布项目', viewport: { x: 0, y: 0, scale: 1 }, layers: [], resources: [], updatedAt: '2026-06-12T00:00:00.000Z', }); createEditorProjectResourceMock.mockResolvedValue({ resourceId: 'resource-added-asset-a', projectId: 'editor-project-default', imageSrc: 'data:image/png;base64,YQ==', width: 320, height: 240, sourceType: 'uploaded', }); saveEditorProjectLayoutMock.mockResolvedValue({ projectId: 'editor-project-default', title: '空画布项目', viewport: { x: 0, y: 0, scale: 1 }, layers: [], resources: [], updatedAt: '2026-06-12T00:00:00.000Z', }); }); it('saves appended layers with the server resource id immediately after resource creation', async () => { render(); expect(await screen.findByText('editor-project-default')).toBeTruthy(); expect(screen.getByTestId('project-title').textContent).toBe('空画布项目'); expect(screen.getByTestId('project-rename').textContent).toBe( '空画布项目', ); act(() => { screen.getByRole('button', { name: 'append' }).click(); }); expect(screen.getByTestId('layers').textContent).toBe( 'layer-a:local-layer-a', ); await waitFor(() => { expect(screen.getByTestId('layers').textContent).toBe( 'layer-a:resource-added-asset-a', ); }); await waitFor(() => { expect(saveEditorProjectLayoutMock).toHaveBeenCalledWith( 'editor-project-default', expect.objectContaining({ layers: expect.arrayContaining([ expect.objectContaining({ layerId: 'layer-a', resourceId: 'resource-added-asset-a', sourceAssetId: 'asset-a', }), ]), }), ); }); }); });