From ed2c0231dba8a021355b20bedfc7d4431d0198ca Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 15 Apr 2026 15:20:43 +0200 Subject: [PATCH] test(drag-drop): add reorder logic tests for useBlockDragDrop Adds simulateDragDrop helper and three tests covering the splice/insertAt index arithmetic in handlePointerUp: - move-to-end (insertAt path where target > fromIdx) - move-to-start (insertAt path where target <= fromIdx) - move-down-by-one (verifies the off-by-one dropTargetIdx - 1 branch) Fixes @saraholt: "reorder calculation in handlePointerUp is untested" Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/useBlockDragDrop.svelte.test.ts | 93 ++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/hooks/__tests__/useBlockDragDrop.svelte.test.ts b/frontend/src/lib/hooks/__tests__/useBlockDragDrop.svelte.test.ts index 7291e19f..a0c541f3 100644 --- a/frontend/src/lib/hooks/__tests__/useBlockDragDrop.svelte.test.ts +++ b/frontend/src/lib/hooks/__tests__/useBlockDragDrop.svelte.test.ts @@ -16,6 +16,78 @@ function makeBlock(id: string, sortOrder: number): TranscriptionBlockData { }; } +/** + * 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() }); @@ -55,8 +127,6 @@ describe('createBlockDragDrop', () => { const blocks = [makeBlock('b1', 0), makeBlock('b2', 1)]; const dd = createBlockDragDrop({ getSortedBlocks: () => blocks, onReorder }); - // Simulate a drag start without going through handleGripDown internals - // by checking that handlePointerUp without a drop target is a no-op for reorder const grip = document.createElement('div'); grip.setAttribute('data-drag-handle', ''); const wrapper = document.createElement('div'); @@ -76,4 +146,23 @@ describe('createBlockDragDrop', () => { 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']); + }); });