import { describe, test, expect, beforeEach } from 'vitest' import { render, screen } from '@testing-library/react' import { ThemeProvider, createTheme } from '@mui/material/styles' import { CategoryUI } from './category-ui' import { useWidgetStore } from '../stores/widget-store' import type { CategoryWidgetState } from './types' // Create a test theme with qualitative colors const testTheme = createTheme({ palette: { primary: { main: '#1976d2', }, qualitative: { bold: { 0: '#1f77b4', 1: '#ff7f0e', 2: '#2ca02c', 3: '#d62728', 4: '#9467bd', 5: '#8c564b', 6: '#e377c2', 7: '#7f7f7f', }, }, }, }) // Helper function to render with theme function renderWithTheme(ui: React.ReactElement) { return render({ui}) } describe('CategoryUI', () => { beforeEach(() => { useWidgetStore.getState().clearWidgets() }) test('returns null when widget does not exist', () => { const { container } = renderWithTheme() expect(container.firstChild).toBeNull() }) test('returns null when data is empty', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [[]], }) const { container } = renderWithTheme() expect(container.firstChild).toBeNull() }) test('renders basic category data', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [ [ { name: 'Category A', value: 100 }, { name: 'Category B', value: 50 }, ], ], }) renderWithTheme() expect(screen.getByText('Category A')).toBeTruthy() expect(screen.getByText('Category B')).toBeTruthy() expect(screen.getByText('100')).toBeTruthy() expect(screen.getByText('50')).toBeTruthy() }) test('auto-calculates max value from data when max prop not provided', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [ [ { name: 'Category A', value: 100 }, { name: 'Category B', value: 150 }, { name: 'Category C', value: 75 }, ], ], }) renderWithTheme() // Verify all categories are rendered expect(screen.getByText('Category A')).toBeTruthy() expect(screen.getByText('Category B')).toBeTruthy() expect(screen.getByText('Category C')).toBeTruthy() expect(screen.getByText('100')).toBeTruthy() expect(screen.getByText('150')).toBeTruthy() expect(screen.getByText('75')).toBeTruthy() }) test('uses custom max value when provided', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [ [ { name: 'Category A', value: 100 }, { name: 'Category B', value: 150 }, ], ], max: 200, }) renderWithTheme() // Verify categories are rendered with custom max expect(screen.getByText('Category A')).toBeTruthy() expect(screen.getByText('Category B')).toBeTruthy() expect(screen.getByText('100')).toBeTruthy() expect(screen.getByText('150')).toBeTruthy() }) test('handles max value of 0', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [ [ { name: 'Category A', value: 100 }, { name: 'Category B', value: 50 }, ], ], max: 0, }) renderWithTheme() // Verify categories are rendered even with max=0 expect(screen.getByText('Category A')).toBeTruthy() expect(screen.getByText('Category B')).toBeTruthy() expect(screen.getByText('100')).toBeTruthy() expect(screen.getByText('50')).toBeTruthy() }) test('handles max value less than actual data values', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [ [ { name: 'Category A', value: 150 }, { name: 'Category B', value: 75 }, ], ], max: 100, }) renderWithTheme() // Verify categories are rendered when values exceed max expect(screen.getByText('Category A')).toBeTruthy() expect(screen.getByText('Category B')).toBeTruthy() expect(screen.getByText('150')).toBeTruthy() expect(screen.getByText('75')).toBeTruthy() }) test('uses custom max with multi-series data', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [ [ { name: 'Product A', value: 80 }, { name: 'Product B', value: 60 }, ], [ { name: 'Product A', value: 90 }, { name: 'Product B', value: 70 }, ], ], max: 100, series: [{ name: 'Q1' }, { name: 'Q2' }], }) renderWithTheme() // Multi-series should show legend expect(screen.getByText('Q1')).toBeTruthy() expect(screen.getByText('Q2')).toBeTruthy() // Verify product names are displayed expect(screen.getByText('Product A')).toBeTruthy() expect(screen.getByText('Product B')).toBeTruthy() }) test('updates when max value changes', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [[{ name: 'Category A', value: 100 }]], max: 200, }) renderWithTheme() expect(screen.getByText('Category A')).toBeTruthy() expect(screen.getByText('100')).toBeTruthy() // Update max value useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [[{ name: 'Category A', value: 100 }]], max: 100, }) // Widget should still render after update expect(screen.getByText('Category A')).toBeTruthy() expect(screen.getByText('100')).toBeTruthy() }) test('uses formatter when provided', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [[{ name: 'Category A', value: 1234 }]], formatter: (value: number) => `$${value}`, }) renderWithTheme() expect(screen.getByText('$1234')).toBeTruthy() }) test('uses formatter with custom max', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [[{ name: 'Sales', value: 125000 }]], max: 200000, formatter: (value: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }).format(value), }) renderWithTheme() // Should show formatted value expect(screen.getByText('$125,000')).toBeTruthy() expect(screen.getByText('Sales')).toBeTruthy() }) test('max prop works with maxItems configuration', () => { useWidgetStore.getState().setWidget('test-category', { type: 'category', data: [ [ { name: 'Item 1', value: 80 }, { name: 'Item 2', value: 60 }, { name: 'Item 3', value: 40 }, { name: 'Item 4', value: 20 }, ], ], max: 100, maxItems: 2, }) renderWithTheme() // Should show only first 2 items expect(screen.getByText('Item 1')).toBeTruthy() expect(screen.getByText('Item 2')).toBeTruthy() // Should show values expect(screen.getByText('80')).toBeTruthy() expect(screen.getByText('60')).toBeTruthy() }) describe('Overflow behavior', () => { test('applies overflow auto when hiddenCount is 0 (no maxItems)', () => { useWidgetStore .getState() .setWidget('test-category', { type: 'category', data: [ [ { name: 'Item 1', value: 100 }, { name: 'Item 2', value: 50 }, ], ], // No maxItems set, so hiddenCount will be 0 }) const { container } = renderWithTheme() // The list Box has inline style for overflow const listBox = container.querySelector( '[style*="overflow"]', )! expect(listBox).toBeTruthy() expect(listBox.style.overflow).toBe('auto') }) test('applies overflow hidden when hiddenCount is greater than 0', () => { useWidgetStore .getState() .setWidget('test-category', { type: 'category', data: [ [ { name: 'Item 1', value: 100 }, { name: 'Item 2', value: 80 }, { name: 'Item 3', value: 60 }, { name: 'Item 4', value: 40 }, ], ], maxItems: 2, // This creates hiddenCount = 2 }) const { container } = renderWithTheme() // The list Box has inline style for overflow const listBox = container.querySelector( '[style*="overflow"]', )! expect(listBox).toBeTruthy() expect(listBox.style.overflow).toBe('hidden') }) test('maintains overflow auto when maxItems equals data length', () => { useWidgetStore .getState() .setWidget('test-category', { type: 'category', data: [ [ { name: 'Item 1', value: 100 }, { name: 'Item 2', value: 50 }, ], ], maxItems: 2, // Same as data length, hiddenCount = 0 }) const { container } = renderWithTheme() // The list Box has inline style for overflow const listBox = container.querySelector( '[style*="overflow"]', )! expect(listBox).toBeTruthy() expect(listBox.style.overflow).toBe('auto') }) test('applies overflow auto when maxItems exceeds data length', () => { useWidgetStore .getState() .setWidget('test-category', { type: 'category', data: [ [ { name: 'Item 1', value: 100 }, { name: 'Item 2', value: 50 }, ], ], maxItems: 5, // More than data length, hiddenCount = 0 }) const { container } = renderWithTheme() // The list Box has inline style for overflow const listBox = container.querySelector( '[style*="overflow"]', )! expect(listBox).toBeTruthy() expect(listBox.style.overflow).toBe('auto') }) test('works correctly with multi-series data', () => { useWidgetStore .getState() .setWidget('test-category', { type: 'category', data: [ [ { name: 'Product A', value: 100 }, { name: 'Product B', value: 80 }, { name: 'Product C', value: 60 }, ], [ { name: 'Product A', value: 90 }, { name: 'Product B', value: 70 }, { name: 'Product C', value: 50 }, ], ], series: [{ name: 'Q1' }, { name: 'Q2' }], maxItems: 2, // hiddenCount = 1 }) const { container } = renderWithTheme() // The list Box has inline style for overflow const listBox = container.querySelector( '[style*="overflow"]', )! expect(listBox).toBeTruthy() expect(listBox.style.overflow).toBe('hidden') }) }) })