/** * Tests for SortableColumnItem — a DnD-kit sortable menu item. * * `useSortable` is mocked so we can drive isDragging / transform independently * and exercise the conditional style branches without spinning up a real * DndContext + SortableContext. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { render } from '@testing-library/react' // Mock @dnd-kit/sortable's useSortable. Tests can override the return value // per-test via setSortableReturn. interface SortableReturn { attributes: Record listeners: Record setNodeRef: (el: HTMLElement | null) => void transform: { x: number; y: number; scaleX: number; scaleY: number } | null transition: string | undefined isDragging: boolean } let sortableReturn: SortableReturn = { attributes: { 'aria-roledescription': 'sortable' }, listeners: {}, setNodeRef: () => undefined, transform: null, transition: undefined, isDragging: false, } const setSortableReturn = (overrides: Partial) => { sortableReturn = { ...sortableReturn, ...overrides } } vi.mock('@dnd-kit/sortable', () => ({ useSortable: () => sortableReturn, })) import { SortableColumnItem } from './sortable-column-item' import type { TableColumn } from '../../table/types' const baseColumn: TableColumn = { id: 'col-1', label: 'Name', } beforeEach(() => { // Reset to defaults so each test starts clean. sortableReturn = { attributes: { 'aria-roledescription': 'sortable' }, listeners: {}, setNodeRef: () => undefined, transform: null, transition: undefined, isDragging: false, } }) describe('SortableColumnItem', () => { it('renders the column label', () => { const { getByText } = render() expect(getByText('Name')).toBeDefined() }) it('renders as a menuitem role', () => { const { getByRole } = render() expect(getByRole('menuitem')).toBeDefined() }) it('applies grab cursor + opacity=1 when not dragging', () => { setSortableReturn({ isDragging: false }) const { getByRole } = render() const item = getByRole('menuitem') expect(item.style.cursor).toBe('grab') expect(item.style.opacity).toBe('1') }) it('applies grabbing cursor + opacity=0.5 when dragging', () => { setSortableReturn({ isDragging: true }) const { getByRole } = render() const item = getByRole('menuitem') expect(item.style.cursor).toBe('grabbing') expect(item.style.opacity).toBe('0.5') }) it('forces transform.x to 0 when transform is non-null (vertical-only drag)', () => { setSortableReturn({ transform: { x: 50, y: 20, scaleX: 1, scaleY: 1 }, transition: 'transform 200ms ease', }) const { getByRole } = render() const item = getByRole('menuitem') // CSS.Transform.toString({x: 0, y: 20, ...}) → 'translate3d(0px, 20px, 0)' expect(item.style.transform).toContain('0px') expect(item.style.transform).toContain('20px') }) it('handles a null transform (no drag offset applied)', () => { setSortableReturn({ transform: null }) const { getByRole } = render() const item = getByRole('menuitem') // CSS.Transform.toString(null) is undefined → no transform attr expect(item.style.transform).toBe('') }) it('threads dnd-kit attributes onto the rendered element', () => { setSortableReturn({ attributes: { 'aria-roledescription': 'draggable column' }, }) const { getByRole } = render() expect(getByRole('menuitem').getAttribute('aria-roledescription')).toBe( 'draggable column', ) }) it('renders a column with a ReactNode label', () => { const column: TableColumn = { id: 'col-x', label: Custom, } const { getByTestId } = render() expect(getByTestId('custom-label')).toBeDefined() }) })