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