Files
Genarrative/src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx
kdletters 53d1283083 拆分图片画布拖拽入画布流程
新增画布拖拽 drop workflow,承接素材库图片和本地文件拖入画布分流

补充拖拽入画布 hook 测试,覆盖遮罩、默认文件夹和无关拖拽不拦截

更新前端拆分文档和 TRACKING 浏览器回归记录
2026-06-17 10:17:07 +08:00

245 lines
7.0 KiB
TypeScript

/* @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<string, string>();
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<typeof createDataTransferStub>;
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 (
<div>
<div
data-testid="outside"
onDragEnter={() => setUploadDropTarget('assets')}
/>
<div
data-testid="canvas"
onDragOver={workflow.handleCanvasDragOver}
onDragLeave={workflow.handleCanvasDragLeave}
onDrop={workflow.handleCanvasDrop}
>
canvas
<span data-testid="drop-target">{uploadDropTarget ?? '-'}</span>
<span data-testid="inner">inner</span>
</div>
</div>
);
}
describe('useImageCanvasCanvasDropWorkflow', () => {
it('shows a canvas drop target for dragged asset library images', () => {
render(<DropWorkflowHarness />);
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(
<DropWorkflowHarness
addAssetLayer={addAssetLayer}
updateAssetMoveDropFolder={updateAssetMoveDropFolder}
/>,
);
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(<DropWorkflowHarness addUploadedFiles={addUploadedFiles} />);
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(<DropWorkflowHarness />);
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('-');
});
});