import { describe, test, expect, beforeEach, vi } from 'vitest'
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
import { ChangeColumn, CHANGE_COLUMN_TOOL_ID } from './change-column'
import { useWidgetStore } from '../../stores/widget-store'
import type { TableColumn } from '../../table/types'
// Mock @dnd-kit with minimal implementation for testing
vi.mock('@dnd-kit/core', async () => {
const actual = await vi.importActual('@dnd-kit/core')
return {
...actual,
useSensor: () => ({}),
useSensors: () => [],
}
})
describe('ChangeColumn', () => {
const widgetId = 'test-change-column-widget'
const mockColumns: TableColumn[] = [
{ id: 'name', label: 'Name' },
{ id: 'country', label: 'Country' },
{ id: 'population', label: 'Population' },
]
beforeEach(() => {
useWidgetStore.getState().clearWidgets()
})
test('returns null when widget has no columns', () => {
const { container } = render()
expect(container.firstChild).toBeNull()
})
test('returns null when widget has fewer than 2 columns', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: [{ id: 'name', label: 'Name' }],
})
const { container } = render()
expect(container.firstChild).toBeNull()
})
test('renders change column button when widget has 2 or more columns', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
const button = screen.getByRole('button')
expect(button).toBeTruthy()
})
test('shows default tooltip label', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
const button = screen.getByRole('button', { name: 'Change column' })
expect(button).toBeTruthy()
})
test('opens menu on button click', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
const button = screen.getByRole('button')
fireEvent.click(button)
// Menu should be open with all columns for reordering
expect(screen.getByRole('menu')).toBeTruthy()
expect(screen.getByRole('menuitem', { name: /Name/i })).toBeTruthy()
expect(screen.getByRole('menuitem', { name: /Country/i })).toBeTruthy()
expect(screen.getByRole('menuitem', { name: /Population/i })).toBeTruthy()
})
test('displays all columns in menu for drag-and-drop reordering', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
const button = screen.getByRole('button')
fireEvent.click(button)
// All three columns should be visible
const menuItems = screen.getAllByRole('menuitem')
expect(menuItems).toHaveLength(3)
})
test('uses custom labels when provided', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
const customLabels = {
tooltip: 'Reorder columns',
ariaLabel: 'Reorder table columns',
}
render()
const button = screen.getByRole('button', { name: 'Reorder table columns' })
expect(button).toBeTruthy()
})
test('works with exactly 2 columns', () => {
const twoColumns: TableColumn[] = [
{ id: 'first', label: 'First' },
{ id: 'second', label: 'Second' },
]
useWidgetStore.getState().setWidget(widgetId, {
columns: twoColumns,
})
render()
const button = screen.getByRole('button')
fireEvent.click(button)
// Both columns should be available for reordering
expect(screen.getByRole('menuitem', { name: /First/i })).toBeTruthy()
expect(screen.getByRole('menuitem', { name: /Second/i })).toBeTruthy()
})
test('sets data-active attribute when menu is open', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
const button = screen.getByRole('button')
expect(button.getAttribute('data-active')).toBe('false')
fireEvent.click(button)
expect(button.getAttribute('data-active')).toBe('true')
})
test('menu items are keyboard focusable', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
const button = screen.getByRole('button')
fireEvent.click(button)
const menuItems = screen.getAllByRole('menuitem')
menuItems.forEach((item) => {
expect(item.getAttribute('tabindex')).toBe('0')
})
})
describe('config tool registration', () => {
test('registers config tool on mount', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
const widget = useWidgetStore.getState().getWidget(widgetId)
const tool = widget?.registeredTools?.find(
(t) => t.id === CHANGE_COLUMN_TOOL_ID,
)
expect(tool).toBeDefined()
expect(tool?.type).toBe('config')
expect(tool?.order).toBe(100)
expect(tool?.enabled).toBe(true)
})
test('unregisters config tool on unmount', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
// Verify tool is registered
let widget = useWidgetStore.getState().getWidget(widgetId)
expect(
widget?.registeredTools?.find((t) => t.id === CHANGE_COLUMN_TOOL_ID),
).toBeDefined()
// Unmount
cleanup()
// Verify tool is unregistered
widget = useWidgetStore.getState().getWidget(widgetId)
expect(
widget?.registeredTools?.find((t) => t.id === CHANGE_COLUMN_TOOL_ID),
).toBeUndefined()
})
test('config tool returns currentConfig unchanged when columns match widget state', () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
const widget = useWidgetStore.getState().getWidget(widgetId)
const tool = widget?.registeredTools?.find(
(t) => t.id === CHANGE_COLUMN_TOOL_ID,
)
// When config columns match widget columns, fn returns the same reference
const input = { columns: mockColumns }
const result = tool?.fn(input)
expect(result).toBe(input)
})
test('column order persists when config pipeline re-runs with original config', async () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
// Simulate a drag that reorders columns via setWidget
const reorderedColumns: TableColumn[] = [
{ id: 'population', label: 'Population' },
{ id: 'name', label: 'Name' },
{ id: 'country', label: 'Country' },
]
useWidgetStore.getState().setWidget(widgetId, {
columns: reorderedColumns,
})
// Simulate config pipeline re-running with original column order
const originalConfig = { columns: mockColumns }
await useWidgetStore
.getState()
.executeConfigPipeline(widgetId, originalConfig)
// Verify the columns are in the reordered order
const widget = useWidgetStore.getState().getWidget(widgetId)
const resultColumns = (widget as unknown as Record)
?.columns as TableColumn[] | undefined
expect(resultColumns?.map((c) => c.id)).toEqual([
'population',
'name',
'country',
])
})
test('new columns appended at the end when not in widget order', async () => {
useWidgetStore.getState().setWidget(widgetId, {
columns: mockColumns,
})
render()
// Simulate widget columns having only a partial order (e.g., user dragged 2 of 3)
const partialOrder: TableColumn[] = [
{ id: 'country', label: 'Country' },
{ id: 'name', label: 'Name' },
]
useWidgetStore.getState().setWidget(widgetId, {
columns: partialOrder,
})
// Config pipeline runs with all columns (includes 'population' not in widget order)
const originalConfig = { columns: mockColumns }
await useWidgetStore
.getState()
.executeConfigPipeline(widgetId, originalConfig)
const widget = useWidgetStore.getState().getWidget(widgetId)
const resultColumns = (widget as unknown as Record)
?.columns as TableColumn[] | undefined
expect(resultColumns?.map((c) => c.id)).toEqual([
'country',
'name',
'population',
])
})
})
})