import { describe, it, expect, beforeEach, vi } from 'vitest' import { initSortable } from '../../assets/modules/sortable.js' function createList(reorderUrl = '/reorder') { const list = document.createElement('div') list.id = 'categories-list' if (reorderUrl) { list.dataset.reorderUrl = reorderUrl } document.body.appendChild(list) return list } function addItem(list, id) { const el = document.createElement('div') el.dataset.id = String(id) list.appendChild(el) return el } function createDragEvent(type, overrides = {}) { const event = new Event(type, { bubbles: true, cancelable: true }) event.dataTransfer = { effectAllowed: '' } event.preventDefault = vi.fn() Object.assign(event, overrides) return event } describe('initSortable', () => { beforeEach(() => { document.body.innerHTML = '' vi.restoreAllMocks() }) it('does nothing without categories-list element', () => { expect(() => initSortable()).not.toThrow() }) it('does nothing without reorderUrl', () => { const list = document.createElement('div') list.id = 'categories-list' document.body.appendChild(list) expect(() => initSortable()).not.toThrow() expect(list.querySelector('[draggable]')).toBeNull() }) it('sets draggable on all data-id elements', () => { const list = createList() addItem(list, 1) addItem(list, 2) initSortable() list.querySelectorAll('[data-id]').forEach(el => { expect(el.getAttribute('draggable')).toBe('true') }) }) it('adds opacity-50 on dragstart', () => { const list = createList() const item = addItem(list, 1) initSortable() const event = createDragEvent('dragstart') item.dispatchEvent(event) expect(item.classList.contains('opacity-50')).toBe(true) expect(event.dataTransfer.effectAllowed).toBe('move') }) it('dragstart does nothing when no data-id parent', () => { const list = createList() const child = document.createElement('span') list.appendChild(child) initSortable() const event = createDragEvent('dragstart') child.dispatchEvent(event) expect(child.classList.contains('opacity-50')).toBe(false) }) it('removes opacity-50 on dragend', () => { const list = createList() const item = addItem(list, 1) initSortable() const startEvent = createDragEvent('dragstart') item.dispatchEvent(startEvent) expect(item.classList.contains('opacity-50')).toBe(true) const endEvent = createDragEvent('dragend') item.dispatchEvent(endEvent) expect(item.classList.contains('opacity-50')).toBe(false) }) it('dragend does nothing when dragEl is null', () => { const list = createList() addItem(list, 1) initSortable() const item = list.querySelector('[data-id]') const endEvent = createDragEvent('dragend') list.dispatchEvent(endEvent) expect(item.classList.contains('opacity-50')).toBe(false) }) it('moves element before target on dragover above midpoint', () => { const list = createList() const item1 = addItem(list, 1) const item2 = addItem(list, 2) initSortable() // Start dragging item2 const startEvent = createDragEvent('dragstart') item2.dispatchEvent(startEvent) // Dragover item1 above midpoint vi.spyOn(item1, 'getBoundingClientRect').mockReturnValue({ top: 100, height: 50, left: 0, right: 100, bottom: 150, width: 100, }) const overEvent = createDragEvent('dragover', { clientY: 110 }) item1.dispatchEvent(overEvent) expect(overEvent.preventDefault).toHaveBeenCalled() // item2 should be before item1 const items = list.querySelectorAll('[data-id]') expect(items[0].dataset.id).toBe('2') expect(items[1].dataset.id).toBe('1') }) it('moves element after target on dragover below midpoint', () => { const list = createList() const item1 = addItem(list, 1) const item2 = addItem(list, 2) const item3 = addItem(list, 3) initSortable() // Start dragging item1 const startEvent = createDragEvent('dragstart') item1.dispatchEvent(startEvent) // Dragover item2 below midpoint vi.spyOn(item2, 'getBoundingClientRect').mockReturnValue({ top: 100, height: 50, left: 0, right: 100, bottom: 150, width: 100, }) const overEvent = createDragEvent('dragover', { clientY: 140 }) item2.dispatchEvent(overEvent) // item1 should be after item2 (between item2 and item3) const items = list.querySelectorAll('[data-id]') expect(items[0].dataset.id).toBe('2') expect(items[1].dataset.id).toBe('1') expect(items[2].dataset.id).toBe('3') }) it('dragover does nothing when target is dragEl', () => { const list = createList() const item = addItem(list, 1) initSortable() const startEvent = createDragEvent('dragstart') item.dispatchEvent(startEvent) const overEvent = createDragEvent('dragover', { clientY: 100 }) item.dispatchEvent(overEvent) // Should still just have one item, no error expect(list.querySelectorAll('[data-id]').length).toBe(1) }) it('dragover does nothing when target has no data-id', () => { const list = createList() addItem(list, 1) const span = document.createElement('span') list.appendChild(span) initSortable() const startEvent = createDragEvent('dragstart') list.querySelector('[data-id]').dispatchEvent(startEvent) const overEvent = createDragEvent('dragover', { clientY: 100 }) span.dispatchEvent(overEvent) expect(overEvent.preventDefault).toHaveBeenCalled() }) it('sends reorder request on drop', () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true }) globalThis.fetch = fetchMock const list = createList('/api/reorder') addItem(list, 3) addItem(list, 1) addItem(list, 2) initSortable() const dropEvent = createDragEvent('drop') list.dispatchEvent(dropEvent) expect(dropEvent.preventDefault).toHaveBeenCalled() expect(fetchMock).toHaveBeenCalledWith('/api/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify([3, 1, 2]), }) }) it('reloads page on fetch error during drop', async () => { const fetchMock = vi.fn().mockRejectedValue(new Error('Network')) globalThis.fetch = fetchMock const reloadMock = vi.fn() globalThis.location = { reload: reloadMock } const list = createList('/api/reorder') addItem(list, 1) initSortable() const dropEvent = createDragEvent('drop') list.dispatchEvent(dropEvent) await new Promise(r => setTimeout(r, 10)) expect(reloadMock).toHaveBeenCalled() }) it('initializes billets-list sortable', () => { const list = document.createElement('div') list.classList.add('billets-list') list.dataset.reorderUrl = '/billet-reorder' document.body.appendChild(list) const el1 = document.createElement('div') el1.dataset.billetId = '10' list.appendChild(el1) const el2 = document.createElement('div') el2.dataset.billetId = '20' list.appendChild(el2) initSortable() expect(el1.getAttribute('draggable')).toBe('true') expect(el2.getAttribute('draggable')).toBe('true') }) })