拆分图片画布素材拖拽桥接
新增素材拖拽桥接 hook,承接素材拖向画布或文件夹的全局 pointer 监听 恢复认证弹窗 portal 渲染,避免全屏画布遮住账号入口 优化画布背景设置面板,补回当前色、色域、色相、预设、HEX 和恢复默认 补充素材拖拽、认证弹窗和背景面板回归测试并更新文档与 TRACKING
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
AssetPointerDragState,
|
||||
EditorAsset,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { useImageCanvasAssetPointerDragBridge } from './useImageCanvasAssetPointerDragBridge';
|
||||
|
||||
const defaultAssets: EditorAsset[] = [
|
||||
{
|
||||
id: 'asset-1',
|
||||
label: '素材一',
|
||||
src: 'data:image/png;base64,asset-1',
|
||||
width: 320,
|
||||
height: 240,
|
||||
folderId: 'project',
|
||||
sourceKind: 'uploaded',
|
||||
sourceType: 'uploaded',
|
||||
persisted: true,
|
||||
},
|
||||
];
|
||||
|
||||
function dispatchPointerEvent(
|
||||
type: string,
|
||||
init: MouseEventInit & { pointerId: number },
|
||||
) {
|
||||
const event = new MouseEvent(type, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
...init,
|
||||
});
|
||||
Object.defineProperty(event, 'pointerId', { value: init.pointerId });
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
function AssetPointerDragBridgeHarness({
|
||||
initialDrag,
|
||||
assets = defaultAssets,
|
||||
resolveAssetFolderId = vi.fn(() => null),
|
||||
resolveCanvasPoint = vi.fn(() => null),
|
||||
moveAssetToFolder = vi.fn(),
|
||||
addAssetLayer = vi.fn(),
|
||||
}: {
|
||||
initialDrag?: AssetPointerDragState | null;
|
||||
assets?: EditorAsset[];
|
||||
resolveAssetFolderId?: (clientX: number, clientY: number) => string | null;
|
||||
resolveCanvasPoint?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
) => { x: number; y: number } | null;
|
||||
moveAssetToFolder?: (assetId: string, folderId: string) => void;
|
||||
addAssetLayer?: (asset: EditorAsset, position?: { x: number; y: number }) => void;
|
||||
}) {
|
||||
const assetPointerDragRef = useRef<AssetPointerDragState | null>(
|
||||
initialDrag ?? {
|
||||
assetId: 'asset-1',
|
||||
pointerId: 7,
|
||||
startClientX: 10,
|
||||
startClientY: 10,
|
||||
currentClientX: 10,
|
||||
currentClientY: 10,
|
||||
active: false,
|
||||
dropFolderId: null,
|
||||
},
|
||||
);
|
||||
const suppressAssetClickRef = useRef(false);
|
||||
const [assetPointerDrag, setAssetPointerDrag] =
|
||||
useState<AssetPointerDragState | null>(assetPointerDragRef.current);
|
||||
const [uploadDropTarget, setUploadDropTarget] = useState<
|
||||
'canvas' | 'assets' | null
|
||||
>(null);
|
||||
const [dropFolderId, updateAssetMoveDropFolder] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useImageCanvasAssetPointerDragBridge({
|
||||
assetPointerDragRef,
|
||||
suppressAssetClickRef,
|
||||
assets,
|
||||
resolveAssetFolderId,
|
||||
resolveCanvasPoint,
|
||||
setAssetPointerDrag,
|
||||
setUploadDropTarget,
|
||||
updateAssetMoveDropFolder,
|
||||
moveAssetToFolder,
|
||||
addAssetLayer,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span data-testid="drag-active">
|
||||
{assetPointerDrag ? String(assetPointerDrag.active) : 'none'}
|
||||
</span>
|
||||
<span data-testid="drag-x">
|
||||
{assetPointerDrag ? assetPointerDrag.currentClientX : 'none'}
|
||||
</span>
|
||||
<span data-testid="drop-target">{uploadDropTarget ?? '-'}</span>
|
||||
<span data-testid="drop-folder">{dropFolderId ?? '-'}</span>
|
||||
<span data-testid="suppressed">{String(suppressAssetClickRef.current)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe('useImageCanvasAssetPointerDragBridge', () => {
|
||||
it('activates sidebar asset drags and updates canvas and folder drop hints', () => {
|
||||
const resolveAssetFolderId = vi.fn(() => 'folder-1');
|
||||
const resolveCanvasPoint = vi.fn(() => ({ x: 100, y: 80 }));
|
||||
render(
|
||||
<AssetPointerDragBridgeHarness
|
||||
resolveAssetFolderId={resolveAssetFolderId}
|
||||
resolveCanvasPoint={resolveCanvasPoint}
|
||||
/>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
dispatchPointerEvent('pointermove', {
|
||||
pointerId: 7,
|
||||
clientX: 18,
|
||||
clientY: 18,
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('drag-active').textContent).toBe('true');
|
||||
expect(screen.getByTestId('drag-x').textContent).toBe('18');
|
||||
expect(screen.getByTestId('drop-target').textContent).toBe('canvas');
|
||||
expect(screen.getByTestId('drop-folder').textContent).toBe('folder-1');
|
||||
});
|
||||
|
||||
it('moves an active asset drag into a different folder on pointer up', () => {
|
||||
const moveAssetToFolder = vi.fn();
|
||||
render(
|
||||
<AssetPointerDragBridgeHarness
|
||||
initialDrag={{
|
||||
assetId: 'asset-1',
|
||||
pointerId: 7,
|
||||
startClientX: 10,
|
||||
startClientY: 10,
|
||||
currentClientX: 30,
|
||||
currentClientY: 30,
|
||||
active: true,
|
||||
dropFolderId: 'folder-1',
|
||||
}}
|
||||
resolveAssetFolderId={() => 'folder-1'}
|
||||
moveAssetToFolder={moveAssetToFolder}
|
||||
/>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
dispatchPointerEvent('pointerup', {
|
||||
pointerId: 7,
|
||||
clientX: 30,
|
||||
clientY: 30,
|
||||
});
|
||||
});
|
||||
|
||||
expect(moveAssetToFolder).toHaveBeenCalledWith('asset-1', 'folder-1');
|
||||
expect(screen.getByTestId('drag-active').textContent).toBe('none');
|
||||
expect(screen.getByTestId('drop-target').textContent).toBe('-');
|
||||
expect(screen.getByTestId('drop-folder').textContent).toBe('-');
|
||||
expect(screen.getByTestId('suppressed').textContent).toBe('true');
|
||||
});
|
||||
|
||||
it('adds an active dragged asset to the canvas when released over the canvas', () => {
|
||||
const addAssetLayer = vi.fn();
|
||||
render(
|
||||
<AssetPointerDragBridgeHarness
|
||||
initialDrag={{
|
||||
assetId: 'asset-1',
|
||||
pointerId: 7,
|
||||
startClientX: 10,
|
||||
startClientY: 10,
|
||||
currentClientX: 48,
|
||||
currentClientY: 64,
|
||||
active: true,
|
||||
dropFolderId: null,
|
||||
}}
|
||||
resolveCanvasPoint={() => ({ x: 480, y: 640 })}
|
||||
addAssetLayer={addAssetLayer}
|
||||
/>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
dispatchPointerEvent('pointerup', {
|
||||
pointerId: 7,
|
||||
clientX: 48,
|
||||
clientY: 64,
|
||||
});
|
||||
});
|
||||
|
||||
expect(addAssetLayer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'asset-1' }),
|
||||
{ x: 480, y: 640 },
|
||||
);
|
||||
});
|
||||
|
||||
it('cleans up inactive drags without moving or adding assets', () => {
|
||||
const moveAssetToFolder = vi.fn();
|
||||
const addAssetLayer = vi.fn();
|
||||
render(
|
||||
<AssetPointerDragBridgeHarness
|
||||
initialDrag={{
|
||||
assetId: 'asset-1',
|
||||
pointerId: 7,
|
||||
startClientX: 10,
|
||||
startClientY: 10,
|
||||
currentClientX: 10,
|
||||
currentClientY: 10,
|
||||
active: false,
|
||||
dropFolderId: 'folder-1',
|
||||
}}
|
||||
moveAssetToFolder={moveAssetToFolder}
|
||||
addAssetLayer={addAssetLayer}
|
||||
/>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
dispatchPointerEvent('pointerup', {
|
||||
pointerId: 7,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
});
|
||||
});
|
||||
|
||||
expect(moveAssetToFolder).not.toHaveBeenCalled();
|
||||
expect(addAssetLayer).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('drag-active').textContent).toBe('none');
|
||||
});
|
||||
|
||||
it('uses the same finish path for pointer cancellation', () => {
|
||||
const moveAssetToFolder = vi.fn();
|
||||
const addAssetLayer = vi.fn();
|
||||
render(
|
||||
<AssetPointerDragBridgeHarness
|
||||
initialDrag={{
|
||||
assetId: 'asset-1',
|
||||
pointerId: 8,
|
||||
startClientX: 10,
|
||||
startClientY: 10,
|
||||
currentClientX: 24,
|
||||
currentClientY: 24,
|
||||
active: true,
|
||||
dropFolderId: 'folder-1',
|
||||
}}
|
||||
moveAssetToFolder={moveAssetToFolder}
|
||||
addAssetLayer={addAssetLayer}
|
||||
/>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
dispatchPointerEvent('pointercancel', {
|
||||
pointerId: 8,
|
||||
clientX: 24,
|
||||
clientY: 24,
|
||||
});
|
||||
});
|
||||
|
||||
expect(moveAssetToFolder).toHaveBeenCalledWith('asset-1', 'folder-1');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user