import { renderHook, act } from '@testing-library/react' import { useDragDrop } from '../useDragDrop' import { createSection, createRow, createBlock, Section } from '../../types' // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** * Build a single-section, single-row, single-column state with the given * number of text blocks. */ function makeOneColSections(blockCount: number): Section[] { const section = createSection() section.rows[0].columns[0] = Array.from({ length: blockCount }, () => createBlock('text')) return [section] } /** * Build a single-section, single-row, two-column state. * blocksPerCol controls how many text blocks each column starts with. */ function makeTwoColSections(col0Count = 1, col1Count = 1): Section[] { const section = createSection() section.rows[0] = createRow('2') section.rows[0].columns[0] = Array.from({ length: col0Count }, () => createBlock('text')) section.rows[0].columns[1] = Array.from({ length: col1Count }, () => createBlock('text')) return [section] } /** Create a minimal synthetic DragEvent mock with preventDefault() */ function fakeDragEvent(): React.DragEvent { return { preventDefault: jest.fn() } as unknown as React.DragEvent } /** Default setup with shared mocks */ function setup(sections: Section[] = makeOneColSections(2)) { const commitChange = jest.fn() const setSelection = jest.fn() const { result, rerender } = renderHook( ({ secs }: { secs: Section[] }) => useDragDrop({ sections: secs, commitChange, setSelection }), { initialProps: { secs: sections } }, ) return { result, commitChange, setSelection, rerender, sections } } // --------------------------------------------------------------------------- // dragStart // --------------------------------------------------------------------------- describe('useDragDrop — handleDragStart', () => { it('sets dragState to the source coordinates', () => { const { result } = setup() act(() => { result.current.handleDragStart(0, 0, 0, 1) }) expect(result.current.dragState).toEqual({ sourceSection: 0, sourceRow: 0, sourceCol: 0, sourceBlock: 1, }) }) it('dragState is null before any drag starts', () => { const { result } = setup() expect(result.current.dragState).toBeNull() }) it('overwriting dragState with a new dragStart replaces previous source', () => { const { result } = setup(makeOneColSections(3)) act(() => { result.current.handleDragStart(0, 0, 0, 0) }) act(() => { result.current.handleDragStart(0, 0, 0, 2) }) expect(result.current.dragState?.sourceBlock).toBe(2) }) }) // --------------------------------------------------------------------------- // dragOver // --------------------------------------------------------------------------- describe('useDragDrop — handleDragOver', () => { it('sets dragOverTarget to the hovered position', () => { const { result } = setup() const evt = fakeDragEvent() act(() => { result.current.handleDragOver(evt, 0, 0, 0, 1) }) expect(result.current.dragOverTarget).toEqual({ sectionIndex: 0, rowIndex: 0, colIndex: 0, blockIndex: 1, }) }) it('calls event.preventDefault', () => { const { result } = setup() const evt = fakeDragEvent() act(() => { result.current.handleDragOver(evt, 0, 0, 0, 0) }) expect(evt.preventDefault).toHaveBeenCalled() }) it('dragOverTarget is null before any dragOver', () => { const { result } = setup() expect(result.current.dragOverTarget).toBeNull() }) }) // --------------------------------------------------------------------------- // dragEnd // --------------------------------------------------------------------------- describe('useDragDrop — handleDragEnd', () => { it('resets dragState and dragOverTarget to null', () => { const { result } = setup() const evt = fakeDragEvent() act(() => { result.current.handleDragStart(0, 0, 0, 0) }) act(() => { result.current.handleDragOver(evt, 0, 0, 0, 1) }) act(() => { result.current.handleDragEnd() }) expect(result.current.dragState).toBeNull() expect(result.current.dragOverTarget).toBeNull() }) it('handleDragEnd is safe to call without a prior drag', () => { const { result } = setup() expect(() => { act(() => { result.current.handleDragEnd() }) }).not.toThrow() }) }) // --------------------------------------------------------------------------- // drop — within the same column // --------------------------------------------------------------------------- describe('useDragDrop — handleDrop within same column', () => { it('moves block forward (index 0 → 2) past a middle block in the same column', () => { // With 3 blocks [A, B, C], drag A from index 0 to target index 2. // After removing A the column is [B, C]. // adjustedTargetBlock = 2 - 1 = 1 (source 0 < target 2). // Insert A at index 1 → [B, A, C]. const sections = makeOneColSections(3) const col0 = sections[0].rows[0].columns[0] const idA = col0[0].id const idB = col0[1].id const idC = col0[2].id const { result, commitChange } = setup(sections) const evt = fakeDragEvent() act(() => { result.current.handleDragStart(0, 0, 0, 0) }) act(() => { result.current.handleDrop(evt, 0, 0, 0, 2) }) expect(commitChange).toHaveBeenCalledTimes(1) const committed: Section[] = commitChange.mock.calls[0][0] const col = committed[0].rows[0].columns[0] expect(col[0].id).toBe(idB) expect(col[1].id).toBe(idA) expect(col[2].id).toBe(idC) }) it('moves block backward (index 1 → 0) in the same column', () => { const sections = makeOneColSections(2) const firstId = sections[0].rows[0].columns[0][0].id const secondId = sections[0].rows[0].columns[0][1].id const { result, commitChange } = setup(sections) const evt = fakeDragEvent() act(() => { result.current.handleDragStart(0, 0, 0, 1) }) act(() => { result.current.handleDrop(evt, 0, 0, 0, 0) }) expect(commitChange).toHaveBeenCalledTimes(1) const committed: Section[] = commitChange.mock.calls[0][0] const col = committed[0].rows[0].columns[0] expect(col[0].id).toBe(secondId) expect(col[1].id).toBe(firstId) }) it('calls event.preventDefault on drop', () => { const { result } = setup(makeOneColSections(2)) const evt = fakeDragEvent() act(() => { result.current.handleDragStart(0, 0, 0, 0) }) act(() => { result.current.handleDrop(evt, 0, 0, 0, 1) }) expect(evt.preventDefault).toHaveBeenCalled() }) it('resets dragState and dragOverTarget after a successful drop', () => { const { result } = setup(makeOneColSections(2)) const evt = fakeDragEvent() act(() => { result.current.handleDragStart(0, 0, 0, 0) }) act(() => { result.current.handleDrop(evt, 0, 0, 0, 1) }) expect(result.current.dragState).toBeNull() expect(result.current.dragOverTarget).toBeNull() }) it('updates selection to the adjusted drop target position', () => { // 3 blocks, drag index 0 to target 2 → adjustedTargetBlock = 1 const { result, setSelection } = setup(makeOneColSections(3)) const evt = fakeDragEvent() act(() => { result.current.handleDragStart(0, 0, 0, 0) }) act(() => { result.current.handleDrop(evt, 0, 0, 0, 2) }) expect(setSelection).toHaveBeenCalledWith({ sectionIndex: 0, rowIndex: 0, columnIndex: 0, blockIndex: 1, // adjusted: source (0) < target (2) so 2 - 1 = 1 }) }) }) // --------------------------------------------------------------------------- // drop — across columns // --------------------------------------------------------------------------- describe('useDragDrop — handleDrop across columns', () => { it('moves block from col 0 to col 1', () => { const sections = makeTwoColSections(1, 1) const movedId = sections[0].rows[0].columns[0][0].id const { result, commitChange } = setup(sections) const evt = fakeDragEvent() act(() => { result.current.handleDragStart(0, 0, 0, 0) }) act(() => { result.current.handleDrop(evt, 0, 0, 1, 0) }) const committed: Section[] = commitChange.mock.calls[0][0] expect(committed[0].rows[0].columns[0]).toHaveLength(0) // source col empty expect(committed[0].rows[0].columns[1]).toHaveLength(2) expect(committed[0].rows[0].columns[1][0].id).toBe(movedId) }) it('moves block from col 1 to col 0', () => { const sections = makeTwoColSections(1, 1) const movedId = sections[0].rows[0].columns[1][0].id const { result, commitChange } = setup(sections) const evt = fakeDragEvent() act(() => { result.current.handleDragStart(0, 0, 1, 0) }) act(() => { result.current.handleDrop(evt, 0, 0, 0, 0) }) const committed: Section[] = commitChange.mock.calls[0][0] expect(committed[0].rows[0].columns[1]).toHaveLength(0) // source col empty expect(committed[0].rows[0].columns[0]).toHaveLength(2) expect(committed[0].rows[0].columns[0][0].id).toBe(movedId) }) it('does not adjust target index when moving between different columns', () => { // Source col 0 index 0, target col 1 index 1: no adjustment expected const sections = makeTwoColSections(1, 2) const movedId = sections[0].rows[0].columns[0][0].id const { result, commitChange } = setup(sections) const evt = fakeDragEvent() act(() => { result.current.handleDragStart(0, 0, 0, 0) }) act(() => { result.current.handleDrop(evt, 0, 0, 1, 1) }) const committed: Section[] = commitChange.mock.calls[0][0] const col1 = committed[0].rows[0].columns[1] // Target index 1 — block should land at position 1 expect(col1[1].id).toBe(movedId) }) }) // --------------------------------------------------------------------------- // drop — guard: no dragState // --------------------------------------------------------------------------- describe('useDragDrop — handleDrop guards', () => { it('does nothing if dragState is null when drop fires', () => { const { result, commitChange } = setup(makeOneColSections(2)) const evt = fakeDragEvent() // Drop without a prior dragStart act(() => { result.current.handleDrop(evt, 0, 0, 0, 1) }) expect(commitChange).not.toHaveBeenCalled() }) it('does nothing if the source block is missing in sections', () => { const sections = makeOneColSections(1) const { result, commitChange } = setup(sections) const evt = fakeDragEvent() // Start drag at an index that doesn't exist act(() => { result.current.handleDragStart(0, 0, 0, 99) }) act(() => { result.current.handleDrop(evt, 0, 0, 0, 0) }) expect(commitChange).not.toHaveBeenCalled() }) }) // --------------------------------------------------------------------------- // setDragOverTarget // --------------------------------------------------------------------------- describe('useDragDrop — setDragOverTarget', () => { it('is exposed and can be called directly to set the target', () => { const { result } = setup() act(() => { result.current.setDragOverTarget({ sectionIndex: 0, rowIndex: 0, colIndex: 0, blockIndex: 2, }) }) expect(result.current.dragOverTarget).toEqual({ sectionIndex: 0, rowIndex: 0, colIndex: 0, blockIndex: 2, }) }) it('can be used to clear the target by setting null', () => { const { result } = setup() const evt = fakeDragEvent() act(() => { result.current.handleDragOver(evt, 0, 0, 0, 0) }) act(() => { result.current.setDragOverTarget(null) }) expect(result.current.dragOverTarget).toBeNull() }) })