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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<typeof vi.fn> {
|
||||||
|
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', () => {
|
describe('createBlockDragDrop', () => {
|
||||||
it('initial state — no drag in progress', () => {
|
it('initial state — no drag in progress', () => {
|
||||||
const dd = createBlockDragDrop({ getSortedBlocks: () => [], onReorder: vi.fn() });
|
const dd = createBlockDragDrop({ getSortedBlocks: () => [], onReorder: vi.fn() });
|
||||||
@@ -55,8 +127,6 @@ describe('createBlockDragDrop', () => {
|
|||||||
const blocks = [makeBlock('b1', 0), makeBlock('b2', 1)];
|
const blocks = [makeBlock('b1', 0), makeBlock('b2', 1)];
|
||||||
const dd = createBlockDragDrop({ getSortedBlocks: () => blocks, onReorder });
|
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');
|
const grip = document.createElement('div');
|
||||||
grip.setAttribute('data-drag-handle', '');
|
grip.setAttribute('data-drag-handle', '');
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
@@ -76,4 +146,23 @@ describe('createBlockDragDrop', () => {
|
|||||||
|
|
||||||
document.body.removeChild(wrapper);
|
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']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user