import { nextTick, ref } from 'vue' import { describe, expect, it } from 'vitest' import type { SampleGroup } from '../../types' import { useSampleSelectorDrag } from '../../components/SampleSelector.drag' type TestDataTransfer = DataTransfer & { data: Record } type TestDragEvent = Omit & { dataTransfer: TestDataTransfer defaultPrevented: boolean } function keyboardEvent(key: string, altKey = true): KeyboardEvent { return { key, altKey, preventDefault: () => {} } as unknown as KeyboardEvent } function createDragEvent(options: { clientY?: number; top?: number; height?: number } = {}) { const data: Record = {} const transfer = { effectAllowed: '', dropEffect: '', data, setData(type: string, value: string) { data[type] = value }, } as unknown as TestDataTransfer let prevented = false const target = { getBoundingClientRect: () => ({ top: options.top ?? 0, height: options.height ?? 20, }), contains: () => false, } return { dataTransfer: transfer, preventDefault: () => { prevented = true }, get defaultPrevented() { return prevented }, clientY: options.clientY ?? 0, currentTarget: target, relatedTarget: null, } as unknown as TestDragEvent } const groups: SampleGroup[] = [ { name: 'Treatment/A', color: '#111111', samples: ['S1'] }, { name: 'Treatment/B', color: '#222222', samples: ['S2'] }, { name: 'Control/A', color: '#333333', samples: ['S3'] }, { name: 'Control/B', color: '#444444', samples: ['S4'] }, ] describe('SampleSelector drag helpers', () => { it('moves dragged samples between groups and resets sample drag state', () => { const state = ref([ { name: 'A', color: '#111111', samples: ['S1'] }, { name: 'B', color: '#222222', samples: ['S2'] }, ]) const drag = useSampleSelectorDrag(state) const startEvent = createDragEvent() const overEvent = createDragEvent() const dropEvent = createDragEvent() drag.handleDragStart('S1', 'A', startEvent) drag.handleDragOver('B', overEvent) drag.handleDrop('B', dropEvent) expect(startEvent.dataTransfer.data['text/plain']).toBe('S1') expect(overEvent.dataTransfer.dropEffect).toBe('move') expect(dropEvent.defaultPrevented).toBe(true) expect(state.value.map(group => group.samples)).toEqual([[], ['S2', 'S1']]) expect(drag.draggingSample.value).toBeNull() expect(drag.dragOverGroup.value).toBeNull() }) it('reorders sibling subgroups without allowing cross-major drops', () => { const state = ref(groups) const drag = useSampleSelectorDrag(state) drag.handleGroupDragStart('Control/A', 'sub', createDragEvent()) drag.handleGroupDragOver('Treatment/A', 'sub', createDragEvent({ clientY: 2 })) expect(drag.reorderTarget.value).toBeNull() const overEvent = createDragEvent({ clientY: 18 }) const dropEvent = createDragEvent() drag.handleGroupDragOver('Control/B', 'sub', overEvent) drag.handleGroupDrop('Control/B', 'sub', dropEvent) expect(overEvent.dataTransfer.dropEffect).toBe('move') expect(dropEvent.defaultPrevented).toBe(true) expect(state.value.map(group => group.name)).toEqual([ 'Treatment/A', 'Treatment/B', 'Control/B', 'Control/A', ]) expect(drag.draggingGroup.value).toBeNull() expect(drag.reorderTarget.value).toBeNull() }) it('rejects leaf reordering across different immediate parents', () => { const state = ref([ { name: 'Liver / High / 5min', color: '#111111', samples: ['L1'] }, { name: 'Liver / Low / 10min', color: '#222222', samples: ['L2'] }, ]) const drag = useSampleSelectorDrag(state) drag.handleGroupDragStart('Liver / High / 5min', 'sub', createDragEvent()) drag.handleGroupDragOver('Liver / Low / 10min', 'sub', createDragEvent({ clientY: 2 })) expect(drag.reorderTarget.value).toBeNull() }) it('accepts leaf reordering within the same immediate parent', () => { const state = ref([ { name: 'Liver / High / 5min', color: '#111111', samples: ['L1'] }, { name: 'Liver / High / 10min', color: '#222222', samples: ['L2'] }, ]) const drag = useSampleSelectorDrag(state) drag.handleGroupDragStart('Liver / High / 5min', 'sub', createDragEvent()) drag.handleGroupDragOver('Liver / High / 10min', 'sub', createDragEvent({ clientY: 2 })) expect(drag.reorderTarget.value).toBe('Liver / High / 10min') }) it('reorders sibling groups with Alt and the arrow keys', async () => { const state = ref([ { name: 'Liver / High / 5min', color: '#111111', samples: ['L1'] }, { name: 'Liver / High / 10min', color: '#222222', samples: ['L2'] }, { name: 'Liver / High / 20min', color: '#333333', samples: ['L3'] }, ]) const drag = useSampleSelectorDrag(state) drag.handleGroupReorderKeydown('Liver / High / 5min', 'sub', keyboardEvent('ArrowDown')) expect(state.value.map(group => group.name)).toEqual([ 'Liver / High / 10min', 'Liver / High / 5min', 'Liver / High / 20min', ]) await nextTick() expect(drag.announcement.value).toBe('Liver / High / 5min moved down to position 2 of 3.') }) it('keeps a group inside its own family and ignores arrows without Alt', () => { const state = ref([ { name: 'Liver / High / 5min', color: '#111111', samples: ['L1'] }, { name: 'Muscle / High / 5min', color: '#222222', samples: ['M1'] }, ]) const drag = useSampleSelectorDrag(state) const before = state.value drag.handleGroupReorderKeydown('Liver / High / 5min', 'sub', keyboardEvent('ArrowDown')) drag.handleGroupReorderKeydown('Liver / High / 5min', 'sub', keyboardEvent('ArrowDown', false)) expect(state.value).toBe(before) }) it('reorders whole major families with Alt and the arrow keys', () => { const state = ref([ { name: 'Liver / High', color: '#111111', samples: ['L1'] }, { name: 'Muscle / High', color: '#222222', samples: ['M1'] }, ]) const drag = useSampleSelectorDrag(state) drag.handleGroupReorderKeydown('Muscle', 'major', keyboardEvent('ArrowUp')) expect(state.value.map(group => group.name)).toEqual(['Muscle / High', 'Liver / High']) }) })