import { describe, it, expect, vi } from 'vitest'; import { createBlockDragDrop } from './useBlockDragDrop.svelte'; import type { TranscriptionBlockData } from '$lib/shared/types'; function makeBlock(id: string, sortOrder: number): TranscriptionBlockData { return { id, annotationId: `ann-${id}`, documentId: 'doc-1', text: '', label: null, sortOrder, version: 1, source: 'MANUAL', reviewed: false, mentionedPersons: [] }; } /** * Builds a DOM list, mocks getBoundingClientRect (60px per wrapper), * drags `dragId` and drops it so dropTargetIdx === targetIdx, then * triggers handlePointerUp. Returns the onReorder spy. */ function simulateDragDrop( dragId: string, targetIdx: number, blocks: TranscriptionBlockData[] ): ReturnType { const onReorder = vi.fn(); const dd = createBlockDragDrop({ getSortedBlocks: () => blocks, onReorder }); // Build DOM const listEl = document.createElement('div'); const wrappers = blocks.map(() => { const grip = document.createElement('div'); grip.setAttribute('data-drag-handle', ''); const wrapper = document.createElement('div'); wrapper.setAttribute('data-block-wrapper', ''); wrapper.appendChild(grip); listEl.appendChild(wrapper); return { grip, wrapper }; }); document.body.appendChild(listEl); dd.setListElement(listEl); // Mock bounding rects: each wrapper is 60px tall starting at y=0 wrappers.forEach(({ wrapper }, i) => { vi.spyOn(wrapper, 'getBoundingClientRect').mockReturnValue({ top: i * 60, height: 60, bottom: (i + 1) * 60, left: 0, right: 100, width: 100, x: 0, y: i * 60, toJSON: () => ({}) } as DOMRect); }); const dragIdx = blocks.findIndex((b) => b.id === dragId); const { grip, wrapper: dragWrapper } = wrappers[dragIdx]; dragWrapper.setPointerCapture = vi.fn(); // Start drag const downEvent = new PointerEvent('pointerdown', { clientY: dragIdx * 60, cancelable: true }); Object.defineProperty(downEvent, 'target', { value: grip }); dd.handleGripDown(downEvent as PointerEvent, dragId); // Move pointer to achieve the desired targetIdx // midpoint of wrapper[i] = i*60 + 30 // clientY just before midpoint[i] → target = i // clientY past last midpoint → target = wrappers.length let clientY: number; if (targetIdx <= 0) { clientY = 5; // before first midpoint (30) } else if (targetIdx >= wrappers.length) { clientY = wrappers.length * 60 + 10; // past all midpoints } else { clientY = targetIdx * 60 + 5; // just past top of wrapper[targetIdx], before its midpoint } const moveEvent = new PointerEvent('pointermove', { clientY }); dd.handlePointerMove(moveEvent as PointerEvent); dd.handlePointerUp(); document.body.removeChild(listEl); return onReorder; } describe('createBlockDragDrop', () => { it('initial state — no drag in progress', () => { const dd = createBlockDragDrop({ getSortedBlocks: () => [], onReorder: vi.fn() }); expect(dd.draggedBlockId).toBeNull(); expect(dd.dropTargetIdx).toBeNull(); expect(dd.dragOffsetY).toBe(0); }); it('handleGripDown sets draggedBlockId when grip is hit', () => { const dd = createBlockDragDrop({ getSortedBlocks: () => [], onReorder: vi.fn() }); const grip = document.createElement('div'); grip.setAttribute('data-drag-handle', ''); const wrapper = document.createElement('div'); wrapper.setAttribute('data-block-wrapper', ''); wrapper.appendChild(grip); document.body.appendChild(wrapper); const e = new PointerEvent('pointerdown', { clientY: 100, cancelable: true, bubbles: true }); Object.defineProperty(e, 'target', { value: grip }); wrapper.setPointerCapture = vi.fn(); dd.handleGripDown(e as PointerEvent, 'block-1'); expect(dd.draggedBlockId).toBe('block-1'); document.body.removeChild(wrapper); }); it('handlePointerUp without active drag is a no-op', () => { const onReorder = vi.fn(); const dd = createBlockDragDrop({ getSortedBlocks: () => [], onReorder }); dd.handlePointerUp(); expect(onReorder).not.toHaveBeenCalled(); }); it('handlePointerUp with null dropTargetIdx does not call onReorder', () => { const onReorder = vi.fn(); const blocks = [makeBlock('b1', 0), makeBlock('b2', 1)]; const dd = createBlockDragDrop({ getSortedBlocks: () => blocks, onReorder }); const grip = document.createElement('div'); grip.setAttribute('data-drag-handle', ''); const wrapper = document.createElement('div'); wrapper.setAttribute('data-block-wrapper', ''); wrapper.appendChild(grip); document.body.appendChild(wrapper); wrapper.setPointerCapture = vi.fn(); const downEvent = new PointerEvent('pointerdown', { clientY: 50, cancelable: true }); Object.defineProperty(downEvent, 'target', { value: grip }); dd.handleGripDown(downEvent as PointerEvent, 'b1'); // dropTargetIdx is still null (no pointer move happened) dd.handlePointerUp(); expect(onReorder).not.toHaveBeenCalled(); expect(dd.draggedBlockId).toBeNull(); document.body.removeChild(wrapper); }); it('reorder: moves block from index 0 to end', () => { const blocks = [makeBlock('b1', 0), makeBlock('b2', 1), makeBlock('b3', 2)]; const onReorder = simulateDragDrop('b1', 3, blocks); expect(onReorder).toHaveBeenCalledWith(['b2', 'b3', 'b1']); }); it('reorder: moves block from end to index 0', () => { const blocks = [makeBlock('b1', 0), makeBlock('b2', 1), makeBlock('b3', 2)]; const onReorder = simulateDragDrop('b3', 0, blocks); expect(onReorder).toHaveBeenCalledWith(['b3', 'b1', 'b2']); }); it('reorder: moves block down by one position (tests insertAt = dropTargetIdx - 1)', () => { const blocks = [makeBlock('b1', 0), makeBlock('b2', 1), makeBlock('b3', 2)]; // dragId=b1 (idx=0), targetIdx=2 → insertAt = 2-1 = 1 → [b2, b1, b3] const onReorder = simulateDragDrop('b1', 2, blocks); expect(onReorder).toHaveBeenCalledWith(['b2', 'b1', 'b3']); }); });