/** * Tests for ChangeColumn's handleDragEnd + reorderFn branches that * require driving the @dnd-kit/core onDragEnd callback. Uses the shared * `tests/mocks/dnd-kit` harness so the DnD-kit pointer machinery is * replaced with a passthrough that exposes the onDragEnd prop directly. * * Complements the existing change-column.test.tsx which covers render + * menu + tool-registration but doesn't drive drag events. */ import { describe, test, expect, beforeEach, vi } from 'vitest' import { render } from '@testing-library/react' import { dndKitCoreMock, dndKitSortableMock, lastDndContextProps, resetDndKitMocks, } from '../../../../tests/mocks/dnd-kit' vi.mock('@dnd-kit/core', () => dndKitCoreMock()) vi.mock('@dnd-kit/sortable', () => dndKitSortableMock()) import { ChangeColumn, CHANGE_COLUMN_TOOL_ID } from './change-column' import { useWidgetStore } from '../../stores/widget-store' import type { TableColumn } from '../../table/types' const widgetId = 'change-column-dnd' const columns: TableColumn[] = [ { id: 'name', label: 'Name' }, { id: 'country', label: 'Country' }, { id: 'population', label: 'Population' }, ] function seedColumns(extra: Partial<{ columns: TableColumn[] }> = {}) { useWidgetStore.getState().setWidget(widgetId, { columns, ...extra, }) } beforeEach(() => { useWidgetStore.getState().clearWidgets() resetDndKitMocks() }) describe('handleDragEnd branches', () => { test('reorders columns when active and over differ', () => { seedColumns() render() // Drag 'name' (index 0) over 'population' (index 2) lastDndContextProps().onDragEnd?.({ active: { id: 'name' }, over: { id: 'population' }, }) const widget = useWidgetStore.getState().getWidget(widgetId) const result = (widget as unknown as { columns: TableColumn[] }).columns expect(result.map((c) => c.id)).toEqual(['country', 'population', 'name']) }) test('no-op when `over` is missing (drop outside the sortable area)', () => { seedColumns() render() const before = useWidgetStore.getState().getWidget(widgetId) lastDndContextProps().onDragEnd?.({ active: { id: 'name' }, over: null, }) const after = useWidgetStore.getState().getWidget(widgetId) // No reorder; column order unchanged const beforeIds = ( before as unknown as { columns: TableColumn[] } ).columns.map((c) => c.id) const afterIds = ( after as unknown as { columns: TableColumn[] } ).columns.map((c) => c.id) expect(afterIds).toEqual(beforeIds) }) test('no-op when active.id === over.id (no movement)', () => { seedColumns() render() lastDndContextProps().onDragEnd?.({ active: { id: 'name' }, over: { id: 'name' }, }) const widget = useWidgetStore.getState().getWidget(widgetId) const result = (widget as unknown as { columns: TableColumn[] }).columns expect(result.map((c) => c.id)).toEqual(['name', 'country', 'population']) }) test('no-op when widget has no columns (returns early)', () => { // No columns seeded but the component renders for the test we still // want to set up — minimum 2 columns are required for the menu to open. // Render with 2 columns then clear them. seedColumns() render() useWidgetStore.getState().setWidget(widgetId, { columns: undefined }) expect(() => lastDndContextProps().onDragEnd?.({ active: { id: 'name' }, over: { id: 'country' }, }), ).not.toThrow() }) test('no-op when active.id is not found in columns (oldIndex === -1)', () => { seedColumns() render() lastDndContextProps().onDragEnd?.({ active: { id: 'unknown-col' }, over: { id: 'name' }, }) const widget = useWidgetStore.getState().getWidget(widgetId) const result = (widget as unknown as { columns: TableColumn[] }).columns expect(result.map((c) => c.id)).toEqual(['name', 'country', 'population']) }) test('no-op when over.id is not found in columns (newIndex === -1)', () => { seedColumns() render() lastDndContextProps().onDragEnd?.({ active: { id: 'name' }, over: { id: 'unknown-col' }, }) const widget = useWidgetStore.getState().getWidget(widgetId) const result = (widget as unknown as { columns: TableColumn[] }).columns expect(result.map((c) => c.id)).toEqual(['name', 'country', 'population']) }) }) describe('reorderFn branches', () => { test('returns currentConfig when widget has no columns', () => { seedColumns() render() const widget = useWidgetStore.getState().getWidget(widgetId) const tool = widget?.registeredTools?.find( (t) => t.id === CHANGE_COLUMN_TOOL_ID, ) // Clear columns from the widget useWidgetStore.getState().setWidget(widgetId, { columns: [] }) const input = { columns } expect(tool?.fn(input)).toBe(input) }) test('returns currentConfig when config has no columns', () => { seedColumns() render() const widget = useWidgetStore.getState().getWidget(widgetId) const tool = widget?.registeredTools?.find( (t) => t.id === CHANGE_COLUMN_TOOL_ID, ) const input = { columns: [] } expect(tool?.fn(input)).toBe(input) }) test('returns currentConfig when config columns key is undefined', () => { seedColumns() render() const widget = useWidgetStore.getState().getWidget(widgetId) const tool = widget?.registeredTools?.find( (t) => t.id === CHANGE_COLUMN_TOOL_ID, ) const input = { other: 1 } expect(tool?.fn(input)).toBe(input) }) test('drops config columns that have no widget-column counterpart', () => { seedColumns({ columns: [{ id: 'name', label: 'Name' }] }) // widget has only 'name' render() const widget = useWidgetStore.getState().getWidget(widgetId) const tool = widget?.registeredTools?.find( (t) => t.id === CHANGE_COLUMN_TOOL_ID, ) // Config has more columns; the reorder drops the loop's `if (col)` true // path for matches and the trailing "append leftovers" loop for extras. // With widget=['name'], the reordered result should be ['name'] followed // by the remaining config columns in their original order. const input = { columns: [ { id: 'population', label: 'Population' }, { id: 'name', label: 'Name' }, { id: 'country', label: 'Country' }, ], } const result = tool?.fn(input) as { columns: TableColumn[] } expect(result.columns.map((c) => c.id)).toEqual([ 'name', 'population', 'country', ]) }) })