Files
smoothschedule/frontend/src/components/dashboard/__tests__/ChartWidget.test.tsx
poduck 8c52d6a275 refactor: Extract reusable UI components and add TDD documentation
- Add comprehensive TDD documentation to CLAUDE.md with coverage requirements and examples
- Extract reusable UI components to frontend/src/components/ui/ (Modal, FormInput, Button, Alert, etc.)
- Add shared constants (schedulePresets) and utility hooks (useCrudMutation, useFormValidation)
- Update frontend/CLAUDE.md with component documentation and usage examples
- Refactor CreateTaskModal to use shared components and constants
- Fix test assertions to be more robust and accurate across all test files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-10 15:27:27 -05:00

896 lines
22 KiB
TypeScript

/**
* 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 }) => (
<div data-testid="responsive-container">{children}</div>
),
BarChart: ({ data, children }: { data: any[]; children: React.ReactNode }) => (
<div data-testid="bar-chart" data-chart-data={JSON.stringify(data)}>
{children}
</div>
),
LineChart: ({ data, children }: { data: any[]; children: React.ReactNode }) => (
<div data-testid="line-chart" data-chart-data={JSON.stringify(data)}>
{children}
</div>
),
Bar: ({ dataKey, fill }: { dataKey: string; fill: string }) => (
<div data-testid="bar" data-key={dataKey} data-fill={fill} />
),
Line: ({ dataKey, stroke }: { dataKey: string; stroke: string }) => (
<div data-testid="line" data-key={dataKey} data-stroke={stroke} />
),
XAxis: ({ dataKey }: { dataKey: string }) => (
<div data-testid="x-axis" data-key={dataKey} />
),
YAxis: () => <div data-testid="y-axis" />,
CartesianGrid: () => <div data-testid="cartesian-grid" />,
Tooltip: () => <div data-testid="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(
<ChartWidget
title="Revenue Chart"
data={mockChartData}
type="bar"
/>
);
expect(screen.getByText('Revenue Chart')).toBeInTheDocument();
});
it('should render chart container', () => {
render(
<ChartWidget
title="Revenue Chart"
data={mockChartData}
type="bar"
/>
);
const container = screen.getByTestId('responsive-container');
expect(container).toBeInTheDocument();
});
it('should render with different titles', () => {
const { rerender } = render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
expect(screen.getByText('Revenue')).toBeInTheDocument();
rerender(
<ChartWidget
title="Appointments"
data={mockChartData}
type="bar"
/>
);
expect(screen.getByText('Appointments')).toBeInTheDocument();
});
it('should render with empty data array', () => {
render(
<ChartWidget
title="Empty Chart"
data={[]}
type="bar"
/>
);
expect(screen.getByText('Empty Chart')).toBeInTheDocument();
expect(screen.getByTestId('bar-chart')).toBeInTheDocument();
});
});
describe('Title', () => {
it('should display title with correct styling', () => {
render(
<ChartWidget
title="Weekly Revenue"
data={mockChartData}
type="bar"
/>
);
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(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
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(
<ChartWidget
title={longTitle}
data={mockChartData}
type="bar"
/>
);
expect(screen.getByText(longTitle)).toBeInTheDocument();
});
});
describe('Bar Chart', () => {
it('should render bar chart when type is "bar"', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
expect(screen.getByTestId('bar-chart')).toBeInTheDocument();
expect(screen.queryByTestId('line-chart')).not.toBeInTheDocument();
});
it('should pass data to bar chart', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
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(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
const bar = screen.getByTestId('bar');
expect(bar).toHaveAttribute('data-key', 'value');
});
it('should render bar with default color', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
const bar = screen.getByTestId('bar');
expect(bar).toHaveAttribute('data-fill', '#3b82f6');
});
it('should render bar with custom color', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
color="#10b981"
/>
);
const bar = screen.getByTestId('bar');
expect(bar).toHaveAttribute('data-fill', '#10b981');
});
it('should render CartesianGrid for bar chart', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
expect(screen.getByTestId('cartesian-grid')).toBeInTheDocument();
});
it('should render XAxis with name dataKey', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
const xAxis = screen.getByTestId('x-axis');
expect(xAxis).toHaveAttribute('data-key', 'name');
});
it('should render YAxis', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
expect(screen.getByTestId('y-axis')).toBeInTheDocument();
});
it('should render Tooltip', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
expect(screen.getByTestId('tooltip')).toBeInTheDocument();
});
});
describe('Line Chart', () => {
it('should render line chart when type is "line"', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="line"
/>
);
expect(screen.getByTestId('line-chart')).toBeInTheDocument();
expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument();
});
it('should pass data to line chart', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="line"
/>
);
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(
<ChartWidget
title="Revenue"
data={mockChartData}
type="line"
/>
);
const line = screen.getByTestId('line');
expect(line).toHaveAttribute('data-key', 'value');
});
it('should render line with default color', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="line"
/>
);
const line = screen.getByTestId('line');
expect(line).toHaveAttribute('data-stroke', '#3b82f6');
});
it('should render line with custom color', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="line"
color="#ef4444"
/>
);
const line = screen.getByTestId('line');
expect(line).toHaveAttribute('data-stroke', '#ef4444');
});
it('should render CartesianGrid for line chart', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="line"
/>
);
expect(screen.getByTestId('cartesian-grid')).toBeInTheDocument();
});
it('should switch between chart types', () => {
const { rerender } = render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
expect(screen.getByTestId('bar-chart')).toBeInTheDocument();
rerender(
<ChartWidget
title="Revenue"
data={mockChartData}
type="line"
/>
);
expect(screen.getByTestId('line-chart')).toBeInTheDocument();
expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument();
});
});
describe('Value Prefix', () => {
it('should use empty prefix by default', () => {
render(
<ChartWidget
title="Appointments"
data={mockChartData}
type="bar"
/>
);
// Component renders successfully without prefix
expect(screen.getByText('Appointments')).toBeInTheDocument();
});
it('should accept custom value prefix', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
valuePrefix="$"
/>
);
// Component renders successfully with prefix
expect(screen.getByText('Revenue')).toBeInTheDocument();
});
it('should accept different prefixes', () => {
const { rerender } = render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
valuePrefix="$"
/>
);
expect(screen.getByText('Revenue')).toBeInTheDocument();
rerender(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
valuePrefix="€"
/>
);
expect(screen.getByText('Revenue')).toBeInTheDocument();
});
});
describe('Edit Mode', () => {
it('should not show edit controls when isEditing is false', () => {
const { container } = render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
isEditing={false}
/>
);
const dragHandle = container.querySelector('.drag-handle');
expect(dragHandle).not.toBeInTheDocument();
});
it('should show drag handle when in edit mode', () => {
const { container } = render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
isEditing={true}
/>
);
const dragHandle = container.querySelector('.drag-handle');
expect(dragHandle).toBeInTheDocument();
});
it('should show remove button when in edit mode', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
isEditing={true}
onRemove={vi.fn()}
/>
);
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(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
isEditing={true}
onRemove={handleRemove}
/>
);
const removeButton = screen.getByRole('button');
await user.click(removeButton);
expect(handleRemove).toHaveBeenCalledTimes(1);
});
it('should apply padding to title when in edit mode', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
isEditing={true}
/>
);
const title = screen.getByText('Revenue');
expect(title).toHaveClass('pl-5');
});
it('should not apply padding to title when not in edit mode', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
isEditing={false}
/>
);
const title = screen.getByText('Revenue');
expect(title).not.toHaveClass('pl-5');
});
it('should have grab cursor on drag handle', () => {
const { container } = render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
isEditing={true}
/>
);
const dragHandle = container.querySelector('.drag-handle');
expect(dragHandle).toHaveClass('cursor-grab', 'active:cursor-grabbing');
});
});
describe('Responsive Container', () => {
it('should render ResponsiveContainer', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
expect(screen.getByTestId('responsive-container')).toBeInTheDocument();
});
it('should wrap chart in responsive container', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
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(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
const widget = container.firstChild;
expect(widget).toHaveClass('flex', 'flex-col');
});
});
describe('Styling', () => {
it('should apply container styles', () => {
const { container } = render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
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(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
const widget = container.firstChild;
expect(widget).toHaveClass('dark:bg-gray-800', 'dark:border-gray-700');
});
it('should have proper spacing for title', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
const title = screen.getByText('Revenue');
expect(title).toHaveClass('mb-4');
});
it('should use flex-1 for chart container', () => {
const { container } = render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
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(
<ChartWidget
title="Revenue"
data={singlePoint}
type="bar"
/>
);
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(
<ChartWidget
title="Revenue"
data={largeData}
type="line"
/>
);
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(
<ChartWidget
title="Revenue"
data={zeroData}
type="bar"
/>
);
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(
<ChartWidget
title="Profit/Loss"
data={negativeData}
type="line"
/>
);
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(
<ChartWidget
title="Revenue Chart"
data={mockChartData}
type="bar"
/>
);
const heading = screen.getByRole('heading', { level: 3 });
expect(heading).toHaveTextContent('Revenue Chart');
});
it('should be keyboard accessible in edit mode', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
isEditing={true}
onRemove={vi.fn()}
/>
);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
});
it('should have proper color contrast', () => {
render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
/>
);
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(
<ChartWidget
title="Weekly Revenue"
data={mockChartData}
type="bar"
color="#10b981"
valuePrefix="$"
isEditing={true}
onRemove={handleRemove}
/>
);
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(
<ChartWidget
title="Simple Chart"
data={mockChartData}
type="bar"
/>
);
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(
<ChartWidget
title="Data"
data={shortData}
type="bar"
/>
);
expect(screen.getByText('Data')).toBeInTheDocument();
const longData = Array.from({ length: 50 }, (_, i) => ({
name: `Item ${i}`,
value: i * 10,
}));
rerender(
<ChartWidget
title="Data"
data={longData}
type="bar"
/>
);
expect(screen.getByText('Data')).toBeInTheDocument();
});
it('should support different color schemes', () => {
const colors = ['#3b82f6', '#10b981', '#ef4444', '#f59e0b', '#8b5cf6'];
const { rerender } = render(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
color={colors[0]}
/>
);
colors.forEach((color) => {
rerender(
<ChartWidget
title="Revenue"
data={mockChartData}
type="bar"
color={color}
/>
);
const bar = screen.getByTestId('bar');
expect(bar).toHaveAttribute('data-fill', color);
});
});
it('should handle rapid data updates', () => {
const { rerender } = render(
<ChartWidget
title="Live Data"
data={mockChartData}
type="line"
/>
);
for (let i = 0; i < 10; i++) {
const newData = mockChartData.map((item) => ({
...item,
value: item.value + Math.random() * 50,
}));
rerender(
<ChartWidget
title="Live Data"
data={newData}
type="line"
/>
);
expect(screen.getByText('Live Data')).toBeInTheDocument();
}
});
});
});