新增画布拖拽 drop workflow,承接素材库图片和本地文件拖入画布分流 补充拖拽入画布 hook 测试,覆盖遮罩、默认文件夹和无关拖拽不拦截 更新前端拆分文档和 TRACKING 浏览器回归记录
245 lines
7.0 KiB
TypeScript
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('-');
|
|
});
|
|
});
|