Files
Genarrative/src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx
kdletters cdc823611b 拆分图片画布素材拖拽桥接
新增素材拖拽桥接 hook,承接素材拖向画布或文件夹的全局 pointer 监听

恢复认证弹窗 portal 渲染,避免全屏画布遮住账号入口

优化画布背景设置面板,补回当前色、色域、色相、预设、HEX 和恢复默认

补充素材拖拽、认证弹窗和背景面板回归测试并更新文档与 TRACKING
2026-06-17 12:20:04 +08:00

264 lines
7.3 KiB
TypeScript

/* @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');
});
});