/* @vitest-environment jsdom */ import { fireEvent, render, screen } from '@testing-library/react'; import { useState } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { ASSET_DRAG_MIME_TYPE } from './ImageCanvasEditorModel'; import type { EditorAsset, EditorAssetFolder } from './ImageCanvasEditorTypes'; import { useImageCanvasCanvasDropWorkflow } from './useImageCanvasCanvasDropWorkflow'; function createDataTransferStub({ files = [], }: { files?: File[]; } = {}) { const store = new Map(); return { files, types: files.length ? ['Files'] : ([] as string[]), dropEffect: 'none', setData(type: string, value: string) { store.set(type, value); if (!this.types.includes(type)) { this.types.push(type); } }, getData(type: string) { return store.get(type) ?? ''; }, }; } function dispatchDragEvent( target: Element, type: string, options: { dataTransfer: ReturnType; clientX?: number; clientY?: number; relatedTarget?: EventTarget | null; }, ) { const event = new Event(type, { bubbles: true, cancelable: true }); Object.defineProperty(event, 'dataTransfer', { configurable: true, value: options.dataTransfer, }); Object.defineProperty(event, 'clientX', { configurable: true, value: options.clientX ?? 0, }); Object.defineProperty(event, 'clientY', { configurable: true, value: options.clientY ?? 0, }); Object.defineProperty(event, 'relatedTarget', { configurable: true, value: options.relatedTarget ?? null, }); fireEvent(target, event); } function DropWorkflowHarness({ assets = [ { id: 'asset-1', label: '素材一', src: 'data:image/png;base64,asset', width: 100, height: 80, folderId: 'project', sourceKind: 'uploaded', sourceType: 'uploaded', persisted: true, }, ], folders = [ { id: 'project', label: '项目素材', collapsed: false, systemDefault: true, persisted: true, }, ], addAssetLayer = vi.fn(), addUploadedFiles = vi.fn(), updateAssetMoveDropFolder = vi.fn(), getCanvasDropPoint = vi.fn((clientX: number, clientY: number) => ({ x: clientX - 10, y: clientY - 20, })), }: { assets?: EditorAsset[]; folders?: EditorAssetFolder[]; addAssetLayer?: (asset: EditorAsset, position?: { x: number; y: number }) => void; addUploadedFiles?: ( files: FileList | File[], options: { folderId?: string; canvasPoint: { x: number; y: number }; addToCanvas: true; }, ) => void; updateAssetMoveDropFolder?: (folderId: string | null) => void; getCanvasDropPoint?: (clientX: number, clientY: number) => { x: number; y: number; }; }) { const [uploadDropTarget, setUploadDropTarget] = useState< 'canvas' | 'assets' | null >(null); const workflow = useImageCanvasCanvasDropWorkflow({ assets, assetFolders: folders, setUploadDropTarget, updateAssetMoveDropFolder, getCanvasDropPoint, addAssetLayer, addUploadedFiles, }); return (
setUploadDropTarget('assets')} />
canvas {uploadDropTarget ?? '-'} inner
); } describe('useImageCanvasCanvasDropWorkflow', () => { it('shows a canvas drop target for dragged asset library images', () => { render(); const dataTransfer = createDataTransferStub(); dataTransfer.setData(ASSET_DRAG_MIME_TYPE, 'asset-1'); dispatchDragEvent(screen.getByTestId('canvas'), 'dragover', { dataTransfer, }); expect(dataTransfer.dropEffect).toBe('copy'); expect(screen.getByTestId('drop-target').textContent).toBe('canvas'); }); it('adds an existing asset to the canvas at the drop point', () => { const addAssetLayer = vi.fn(); const updateAssetMoveDropFolder = vi.fn(); render( , ); const dataTransfer = createDataTransferStub(); dataTransfer.setData(ASSET_DRAG_MIME_TYPE, 'asset-1'); dispatchDragEvent(screen.getByTestId('canvas'), 'drop', { clientX: 120, clientY: 90, dataTransfer, }); expect(addAssetLayer).toHaveBeenCalledWith( expect.objectContaining({ id: 'asset-1' }), { x: 110, y: 70 }, ); expect(updateAssetMoveDropFolder).toHaveBeenCalledWith(null); expect(screen.getByTestId('drop-target').textContent).toBe('-'); }); it('uploads dropped files to the default folder and adds them to the canvas', () => { const addUploadedFiles = vi.fn(); render(); const imageFile = new File(['image'], '画布上传.png', { type: 'image/png', }); const dataTransfer = createDataTransferStub({ files: [imageFile] }); dispatchDragEvent(screen.getByTestId('canvas'), 'dragover', { dataTransfer, }); expect(screen.getByTestId('drop-target').textContent).toBe('canvas'); dispatchDragEvent(screen.getByTestId('canvas'), 'drop', { clientX: 300, clientY: 220, dataTransfer, }); expect(addUploadedFiles).toHaveBeenCalledWith([imageFile], { folderId: 'project', canvasPoint: { x: 290, y: 200 }, addToCanvas: true, }); expect(screen.getByTestId('drop-target').textContent).toBe('-'); }); it('keeps unrelated drags untouched and clears only canvas overlays on leave', () => { render(); const dataTransfer = createDataTransferStub(); dispatchDragEvent(screen.getByTestId('canvas'), 'dragover', { dataTransfer, }); expect(dataTransfer.dropEffect).toBe('none'); expect(screen.getByTestId('drop-target').textContent).toBe('-'); fireEvent.dragEnter(screen.getByTestId('outside')); expect(screen.getByTestId('drop-target').textContent).toBe('assets'); dispatchDragEvent(screen.getByTestId('canvas'), 'dragover', { dataTransfer: createDataTransferStub({ files: [new File(['image'], '画布上传.png', { type: 'image/png' })], }), }); expect(screen.getByTestId('drop-target').textContent).toBe('canvas'); dispatchDragEvent(screen.getByTestId('canvas'), 'dragleave', { dataTransfer: createDataTransferStub(), relatedTarget: screen.getByTestId('inner'), }); expect(screen.getByTestId('drop-target').textContent).toBe('canvas'); dispatchDragEvent(screen.getByTestId('canvas'), 'dragleave', { dataTransfer: createDataTransferStub(), relatedTarget: screen.getByTestId('outside'), }); expect(screen.getByTestId('drop-target').textContent).toBe('-'); }); });