/**
* 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',
])
})
})