All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m29s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m52s
CI / fail2ban Regex (pull_request) Successful in 48s
CI / Semgrep Security Scan (pull_request) Successful in 24s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
- useBlockDragDrop: add runtime expect() alongside expectTypeOf so
browser-mode runner counts at least one assertion
- JourneyAddBar: use exact:true on 'Hinzufügen' button — partial match
was hitting '+ Brief hinzufügen' and '+ Zwischentext hinzufügen' too
- JourneyEditor: fix 4 issues — drop wrong not.toBeInTheDocument()
(placeholder creates accessible name); pass title:'' in publish-disabled
test (default was non-empty); use getByPlaceholder for interlude
textarea to avoid 4-element strict-mode violation; exact:true for
'Hinzufügen' button
- DocumentPickerDropdown: use .click({force:true}) on aria-disabled
option — userEvent refuses non-enabled elements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
198 lines
7.2 KiB
TypeScript
198 lines
7.2 KiB
TypeScript
import { describe, it, expect, vi, expectTypeOf } from 'vitest';
|
|
import { createBlockDragDrop } from './useBlockDragDrop.svelte';
|
|
import type { TranscriptionBlockData } from '$lib/shared/types';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Type-regression guard: createBlockDragDrop must accept any T extends {id: string}
|
|
// so JourneyEditor can reuse it without importing TranscriptionBlockData.
|
|
// This test fails with "Expected 0 type arguments, but got 1" via tsc --noEmit
|
|
// until the function is made generic.
|
|
// ---------------------------------------------------------------------------
|
|
describe('createBlockDragDrop — generic type guard', () => {
|
|
it('accepts items shaped as { id: string; position: number } — not only TranscriptionBlockData', () => {
|
|
type SimpleItem = { id: string; position: number };
|
|
const items: SimpleItem[] = [
|
|
{ id: 'item-1', position: 0 },
|
|
{ id: 'item-2', position: 1 }
|
|
];
|
|
const onReorder = vi.fn();
|
|
const dd = createBlockDragDrop<SimpleItem>({ getSortedBlocks: () => items, onReorder });
|
|
// Verify the hook is functional with the new type — state reads must work
|
|
expect(dd.draggedBlockId).toBeNull();
|
|
expect(dd.dragOffsetY).toBe(0);
|
|
});
|
|
|
|
it('TranscriptionBlockData caller still compiles — regression guard for existing transcription editor', () => {
|
|
// If the generic constraint is wrong this line fails tsc --noEmit
|
|
expectTypeOf(createBlockDragDrop<TranscriptionBlockData>).toBeFunction();
|
|
// Runtime assertion so browser-mode doesn't report "no assertions"
|
|
expect(typeof createBlockDragDrop).toBe('function');
|
|
});
|
|
});
|
|
|
|
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']);
|
|
});
|
|
});
|