/** * Unit tests for ChartWidget component * * Tests cover: * - Chart container rendering * - Title display * - Bar chart rendering * - Line chart rendering * - Data visualization * - Custom colors * - Value prefixes * - Edit mode with drag handle and remove button * - Tooltip formatting * - Responsive container * - Accessibility */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import ChartWidget from '../ChartWidget'; // Mock Recharts components to avoid rendering issues in tests vi.mock('recharts', () => ({ ResponsiveContainer: ({ children }: { children: React.ReactNode }) => (
{children}
), BarChart: ({ data, children }: { data: any[]; children: React.ReactNode }) => (
{children}
), LineChart: ({ data, children }: { data: any[]; children: React.ReactNode }) => (
{children}
), Bar: ({ dataKey, fill }: { dataKey: string; fill: string }) => (
), Line: ({ dataKey, stroke }: { dataKey: string; stroke: string }) => (
), XAxis: ({ dataKey }: { dataKey: string }) => (
), YAxis: () =>
, CartesianGrid: () =>
, Tooltip: () =>
, })); describe('ChartWidget', () => { const mockChartData = [ { name: 'Mon', value: 100 }, { name: 'Tue', value: 150 }, { name: 'Wed', value: 120 }, { name: 'Thu', value: 180 }, { name: 'Fri', value: 200 }, ]; beforeEach(() => { vi.clearAllMocks(); }); describe('Rendering', () => { it('should render the component', () => { render( ); expect(screen.getByText('Revenue Chart')).toBeInTheDocument(); }); it('should render chart container', () => { render( ); const container = screen.getByTestId('responsive-container'); expect(container).toBeInTheDocument(); }); it('should render with different titles', () => { const { rerender } = render( ); expect(screen.getByText('Revenue')).toBeInTheDocument(); rerender( ); expect(screen.getByText('Appointments')).toBeInTheDocument(); }); it('should render with empty data array', () => { render( ); expect(screen.getByText('Empty Chart')).toBeInTheDocument(); expect(screen.getByTestId('bar-chart')).toBeInTheDocument(); }); }); describe('Title', () => { it('should display title with correct styling', () => { render( ); const title = screen.getByText('Weekly Revenue'); expect(title).toBeInTheDocument(); expect(title).toHaveClass('text-lg', 'font-semibold', 'text-gray-900'); }); it('should apply dark mode styles to title', () => { render( ); const title = screen.getByText('Revenue'); expect(title).toHaveClass('dark:text-white'); }); it('should handle long titles', () => { const longTitle = 'Very Long Chart Title That Should Still Display Properly Without Breaking Layout'; render( ); expect(screen.getByText(longTitle)).toBeInTheDocument(); }); }); describe('Bar Chart', () => { it('should render bar chart when type is "bar"', () => { render( ); expect(screen.getByTestId('bar-chart')).toBeInTheDocument(); expect(screen.queryByTestId('line-chart')).not.toBeInTheDocument(); }); it('should pass data to bar chart', () => { render( ); const barChart = screen.getByTestId('bar-chart'); const chartData = JSON.parse(barChart.getAttribute('data-chart-data') || '[]'); expect(chartData).toEqual(mockChartData); }); it('should render bar with correct dataKey', () => { render( ); const bar = screen.getByTestId('bar'); expect(bar).toHaveAttribute('data-key', 'value'); }); it('should render bar with default color', () => { render( ); const bar = screen.getByTestId('bar'); expect(bar).toHaveAttribute('data-fill', '#3b82f6'); }); it('should render bar with custom color', () => { render( ); const bar = screen.getByTestId('bar'); expect(bar).toHaveAttribute('data-fill', '#10b981'); }); it('should render CartesianGrid for bar chart', () => { render( ); expect(screen.getByTestId('cartesian-grid')).toBeInTheDocument(); }); it('should render XAxis with name dataKey', () => { render( ); const xAxis = screen.getByTestId('x-axis'); expect(xAxis).toHaveAttribute('data-key', 'name'); }); it('should render YAxis', () => { render( ); expect(screen.getByTestId('y-axis')).toBeInTheDocument(); }); it('should render Tooltip', () => { render( ); expect(screen.getByTestId('tooltip')).toBeInTheDocument(); }); }); describe('Line Chart', () => { it('should render line chart when type is "line"', () => { render( ); expect(screen.getByTestId('line-chart')).toBeInTheDocument(); expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument(); }); it('should pass data to line chart', () => { render( ); const lineChart = screen.getByTestId('line-chart'); const chartData = JSON.parse(lineChart.getAttribute('data-chart-data') || '[]'); expect(chartData).toEqual(mockChartData); }); it('should render line with correct dataKey', () => { render( ); const line = screen.getByTestId('line'); expect(line).toHaveAttribute('data-key', 'value'); }); it('should render line with default color', () => { render( ); const line = screen.getByTestId('line'); expect(line).toHaveAttribute('data-stroke', '#3b82f6'); }); it('should render line with custom color', () => { render( ); const line = screen.getByTestId('line'); expect(line).toHaveAttribute('data-stroke', '#ef4444'); }); it('should render CartesianGrid for line chart', () => { render( ); expect(screen.getByTestId('cartesian-grid')).toBeInTheDocument(); }); it('should switch between chart types', () => { const { rerender } = render( ); expect(screen.getByTestId('bar-chart')).toBeInTheDocument(); rerender( ); expect(screen.getByTestId('line-chart')).toBeInTheDocument(); expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument(); }); }); describe('Value Prefix', () => { it('should use empty prefix by default', () => { render( ); // Component renders successfully without prefix expect(screen.getByText('Appointments')).toBeInTheDocument(); }); it('should accept custom value prefix', () => { render( ); // Component renders successfully with prefix expect(screen.getByText('Revenue')).toBeInTheDocument(); }); it('should accept different prefixes', () => { const { rerender } = render( ); expect(screen.getByText('Revenue')).toBeInTheDocument(); rerender( ); expect(screen.getByText('Revenue')).toBeInTheDocument(); }); }); describe('Edit Mode', () => { it('should not show edit controls when isEditing is false', () => { const { container } = render( ); const dragHandle = container.querySelector('.drag-handle'); expect(dragHandle).not.toBeInTheDocument(); }); it('should show drag handle when in edit mode', () => { const { container } = render( ); const dragHandle = container.querySelector('.drag-handle'); expect(dragHandle).toBeInTheDocument(); }); it('should show remove button when in edit mode', () => { render( ); const removeButton = screen.getByRole('button'); expect(removeButton).toBeInTheDocument(); }); it('should call onRemove when remove button is clicked', async () => { const user = userEvent.setup(); const handleRemove = vi.fn(); render( ); const removeButton = screen.getByRole('button'); await user.click(removeButton); expect(handleRemove).toHaveBeenCalledTimes(1); }); it('should apply padding to title when in edit mode', () => { render( ); const title = screen.getByText('Revenue'); expect(title).toHaveClass('pl-5'); }); it('should not apply padding to title when not in edit mode', () => { render( ); const title = screen.getByText('Revenue'); expect(title).not.toHaveClass('pl-5'); }); it('should have grab cursor on drag handle', () => { const { container } = render( ); const dragHandle = container.querySelector('.drag-handle'); expect(dragHandle).toHaveClass('cursor-grab', 'active:cursor-grabbing'); }); }); describe('Responsive Container', () => { it('should render ResponsiveContainer', () => { render( ); expect(screen.getByTestId('responsive-container')).toBeInTheDocument(); }); it('should wrap chart in responsive container', () => { render( ); const container = screen.getByTestId('responsive-container'); const barChart = screen.getByTestId('bar-chart'); expect(container).toContainElement(barChart); }); it('should have flex layout for proper sizing', () => { const { container } = render( ); const widget = container.firstChild; expect(widget).toHaveClass('flex', 'flex-col'); }); }); describe('Styling', () => { it('should apply container styles', () => { const { container } = render( ); const widget = container.firstChild; expect(widget).toHaveClass( 'h-full', 'p-4', 'bg-white', 'rounded-xl', 'border', 'border-gray-200', 'shadow-sm', 'relative', 'group' ); }); it('should apply dark mode styles', () => { const { container } = render( ); const widget = container.firstChild; expect(widget).toHaveClass('dark:bg-gray-800', 'dark:border-gray-700'); }); it('should have proper spacing for title', () => { render( ); const title = screen.getByText('Revenue'); expect(title).toHaveClass('mb-4'); }); it('should use flex-1 for chart container', () => { const { container } = render( ); const chartContainer = container.querySelector('.flex-1'); expect(chartContainer).toBeInTheDocument(); expect(chartContainer).toHaveClass('min-h-0'); }); }); describe('Data Handling', () => { it('should handle single data point', () => { const singlePoint = [{ name: 'Mon', value: 100 }]; render( ); const barChart = screen.getByTestId('bar-chart'); const chartData = JSON.parse(barChart.getAttribute('data-chart-data') || '[]'); expect(chartData).toEqual(singlePoint); }); it('should handle large datasets', () => { const largeData = Array.from({ length: 100 }, (_, i) => ({ name: `Day ${i + 1}`, value: Math.random() * 1000, })); render( ); const lineChart = screen.getByTestId('line-chart'); const chartData = JSON.parse(lineChart.getAttribute('data-chart-data') || '[]'); expect(chartData).toHaveLength(100); }); it('should handle zero values', () => { const zeroData = [ { name: 'Mon', value: 0 }, { name: 'Tue', value: 0 }, ]; render( ); const barChart = screen.getByTestId('bar-chart'); const chartData = JSON.parse(barChart.getAttribute('data-chart-data') || '[]'); expect(chartData).toEqual(zeroData); }); it('should handle negative values', () => { const negativeData = [ { name: 'Mon', value: -50 }, { name: 'Tue', value: 100 }, { name: 'Wed', value: -30 }, ]; render( ); const lineChart = screen.getByTestId('line-chart'); const chartData = JSON.parse(lineChart.getAttribute('data-chart-data') || '[]'); expect(chartData).toEqual(negativeData); }); }); describe('Accessibility', () => { it('should have semantic heading for title', () => { render( ); const heading = screen.getByRole('heading', { level: 3 }); expect(heading).toHaveTextContent('Revenue Chart'); }); it('should be keyboard accessible in edit mode', () => { render( ); const button = screen.getByRole('button'); expect(button).toBeInTheDocument(); }); it('should have proper color contrast', () => { render( ); const title = screen.getByText('Revenue'); expect(title).toHaveClass('text-gray-900'); }); }); describe('Integration', () => { it('should render correctly with all props', () => { const handleRemove = vi.fn(); render( ); expect(screen.getByText('Weekly Revenue')).toBeInTheDocument(); expect(screen.getByTestId('bar-chart')).toBeInTheDocument(); expect(screen.getByTestId('bar')).toHaveAttribute('data-fill', '#10b981'); expect(screen.getByRole('button')).toBeInTheDocument(); }); it('should work with minimal props', () => { render( ); expect(screen.getByText('Simple Chart')).toBeInTheDocument(); expect(screen.getByTestId('bar-chart')).toBeInTheDocument(); }); it('should maintain layout with varying data lengths', () => { const shortData = [{ name: 'A', value: 1 }]; const { rerender } = render( ); expect(screen.getByText('Data')).toBeInTheDocument(); const longData = Array.from({ length: 50 }, (_, i) => ({ name: `Item ${i}`, value: i * 10, })); rerender( ); expect(screen.getByText('Data')).toBeInTheDocument(); }); it('should support different color schemes', () => { const colors = ['#3b82f6', '#10b981', '#ef4444', '#f59e0b', '#8b5cf6']; const { rerender } = render( ); colors.forEach((color) => { rerender( ); const bar = screen.getByTestId('bar'); expect(bar).toHaveAttribute('data-fill', color); }); }); it('should handle rapid data updates', () => { const { rerender } = render( ); for (let i = 0; i < 10; i++) { const newData = mockChartData.map((item) => ({ ...item, value: item.value + Math.random() * 50, })); rerender( ); expect(screen.getByText('Live Data')).toBeInTheDocument(); } }); }); });