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