/* @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( 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(assetPointerDragRef.current); const [uploadDropTarget, setUploadDropTarget] = useState< 'canvas' | 'assets' | null >(null); const [dropFolderId, updateAssetMoveDropFolder] = useState( null, ); useImageCanvasAssetPointerDragBridge({ assetPointerDragRef, suppressAssetClickRef, assets, resolveAssetFolderId, resolveCanvasPoint, setAssetPointerDrag, setUploadDropTarget, updateAssetMoveDropFolder, moveAssetToFolder, addAssetLayer, }); return (
{assetPointerDrag ? String(assetPointerDrag.active) : 'none'} {assetPointerDrag ? assetPointerDrag.currentClientX : 'none'} {uploadDropTarget ?? '-'} {dropFolderId ?? '-'} {String(suppressAssetClickRef.current)}
); } 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( , ); 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( '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( ({ 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( , ); 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( , ); act(() => { dispatchPointerEvent('pointercancel', { pointerId: 8, clientX: 24, clientY: 24, }); }); expect(moveAssetToFolder).toHaveBeenCalledWith('asset-1', 'folder-1'); }); });