- Move api.server.ts, errors.ts, types.ts, utils.ts, relativeTime.ts to lib/shared/ - Move person relationship components to lib/person/relationship/ - Move Stammbaum components to lib/person/genealogy/ - Move HelpPopover to lib/shared/primitives/ - Update all import paths across routes, specs, and lib files - Update vi.mock() paths in server-project test files - Remove now-empty legacy directories (components/, hooks/, server/, etc.) - Update vite.config.ts coverage include paths for new structure - Update frontend/CLAUDE.md to reflect domain-based lib/ layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
170 lines
5.8 KiB
TypeScript
170 lines
5.8 KiB
TypeScript
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<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', () => {
|
|
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']);
|
|
});
|
|
});
|