/** * Frontend Component Tests - Dashboard Components * * Tests for dashboard-related components: * - DashboardLayout * - StatsCard * - ChartComponent * - ActivityFeed * - NotificationPanel * * Author: Claude */ import React from 'react'; import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import '@testing-library/jest-dom'; import { BrowserRouter } from 'react-router-dom'; // Mock components for testing const StatsCard = ({ title, value, change, changeType, icon }) => (
{title}
{value}
{change && (
{change > 0 ? '+' : ''}{change}%
)} {icon &&
{icon}
}
); const ChartComponent = ({ data, type, height = 300 }) => (
{type}
{height}
{data?.length || 0}
); const ActivityFeed = ({ activities, loading }) => (
{loading ? (
Loading...
) : (
{activities?.map((activity, index) => (
{activity.type}
{activity.description}
{activity.timestamp}
))}
)}
); const NotificationPanel = ({ notifications, onMarkAsRead, onDismiss }) => (
Notifications
{notifications?.map((notification, index) => (
{notification.title}
{notification.message}
{notification.type}
{notification.read ? 'Read' : 'Unread'}
{!notification.read && ( )}
))}
{(!notifications || notifications.length === 0) && (
No notifications
)}
); // Test data const mockActivities = [ { id: 1, type: 'user_created', description: 'New user registered: John Doe', timestamp: '2024-01-15T10:30:00Z', user: { name: 'John Doe' } }, { id: 2, type: 'payment_processed', description: 'Payment of RM299.00 processed', timestamp: '2024-01-15T09:15:00Z', amount: 299.00 }, { id: 3, type: 'login_attempt', description: 'Failed login attempt from unknown device', timestamp: '2024-01-15T08:00:00Z', successful: false } ]; const mockNotifications = [ { id: 1, title: 'Welcome!', message: 'Welcome to the platform', type: 'info', read: false, timestamp: '2024-01-15T10:00:00Z' }, { id: 2, title: 'Payment Received', message: 'Your subscription payment was successful', type: 'success', read: true, timestamp: '2024-01-14T15:30:00Z' }, { id: 3, title: 'Security Alert', message: 'New login detected from unknown device', type: 'warning', read: false, timestamp: '2024-01-14T12:00:00Z' } ]; const mockChartData = [ { month: 'Jan', value: 1000 }, { month: 'Feb', value: 1200 }, { month: 'Mar', value: 1100 }, { month: 'Apr', value: 1400 }, { month: 'May', value: 1300 }, { month: 'Jun', value: 1500 } ]; describe('Dashboard Components', () => { describe('StatsCard', () => { test('renders stats card with basic props', () => { render( ); expect(screen.getByTestId('stats-card')).toBeInTheDocument(); expect(screen.getByTestId('card-title')).toHaveTextContent('Total Users'); expect(screen.getByTestId('card-value')).toHaveTextContent('1,234'); expect(screen.getByTestId('card-change-positive')).toHaveTextContent('+12.5%'); expect(screen.getByTestId('card-icon')).toHaveTextContent('👥'); }); test('renders stats card with negative change', () => { render( ); expect(screen.getByTestId('card-change-negative')).toHaveTextContent('-3.2%'); }); test('renders stats card without change', () => { render( ); expect(screen.queryByTestId(/card-change-/)).not.toBeInTheDocument(); }); test('formats large numbers correctly', () => { const testCases = [ { input: '1000000', expected: '1,000,000' }, { input: '2500000.50', expected: '2,500,000.50' }, { input: '1234567890', expected: '1,234,567,890' } ]; testCases.forEach(({ input, expected }) => { render(); expect(screen.getByTestId('card-value')).toHaveTextContent(expected); }); }); }); describe('ChartComponent', () => { test('renders chart with data', () => { render( ); expect(screen.getByTestId('chart-component')).toBeInTheDocument(); expect(screen.getByTestId('chart-type')).toHaveTextContent('line'); expect(screen.getByTestId('chart-height')).toHaveTextContent('400'); expect(screen.getByTestId('chart-data-points')).toHaveTextContent('6'); }); test('renders chart without data', () => { render( ); expect(screen.getByTestId('chart-data-points')).toHaveTextContent('0'); }); test('renders chart with null data', () => { render( ); expect(screen.getByTestId('chart-data-points')).toHaveTextContent('0'); }); test('supports different chart types', () => { const chartTypes = ['line', 'bar', 'pie', 'area', 'scatter']; chartTypes.forEach(type => { const { unmount } = render( ); expect(screen.getByTestId('chart-type')).toHaveTextContent(type); unmount(); }); }); }); describe('ActivityFeed', () => { test('renders activity feed with activities', () => { const mockOnActivityClick = jest.fn(); render( ); expect(screen.getByTestId('activity-feed')).toBeInTheDocument(); expect(screen.getByTestId('activities-list')).toBeInTheDocument(); expect(screen.getAllByTestId(/activity-/)).toHaveLength(3); // Check first activity expect(screen.getByTestId('activity-0')).toBeInTheDocument(); expect(screen.getByTestId('activity-type')).toHaveTextContent('user_created'); expect(screen.getByTestId('activity-description')).toHaveTextContent('New user registered: John Doe'); }); test('renders loading state', () => { render( ); expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); expect(screen.queryByTestId('activities-list')).not.toBeInTheDocument(); }); test('renders empty state', () => { render( ); expect(screen.getByTestId('activities-list')).toBeInTheDocument(); expect(screen.queryByTestId(/activity-/)).not.toBeInTheDocument(); }); test('formats timestamps correctly', () => { const formatTimestamp = (timestamp) => { const date = new Date(timestamp); const now = new Date(); const diffMs = now - date; const diffMinutes = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMinutes < 1) return 'Just now'; if (diffMinutes < 60) return `${diffMinutes}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); }; const recent = new Date().toISOString(); const hourAgo = new Date(Date.now() - 3600000).toISOString(); const dayAgo = new Date(Date.now() - 86400000).toISOString(); expect(formatTimestamp(recent)).toMatch(/Just now|\d+m ago/); expect(formatTimestamp(hourAgo)).toMatch(/\d+h ago/); expect(formatTimestamp(dayAgo)).toMatch(/\d+d ago/); }); test('handles activity click', () => { const mockOnActivityClick = jest.fn(); render( ); const firstActivity = screen.getByTestId('activity-0'); fireEvent.click(firstActivity); expect(mockOnActivityClick).toHaveBeenCalledWith(mockActivities[0]); }); }); describe('NotificationPanel', () => { const mockOnMarkAsRead = jest.fn(); const mockOnDismiss = jest.fn(); beforeEach(() => { mockOnMarkAsRead.mockClear(); mockOnDismiss.mockClear(); }); test('renders notification panel with notifications', () => { render( ); expect(screen.getByTestId('notification-panel')).toBeInTheDocument(); expect(screen.getByTestId('notifications-header')).toHaveTextContent('Notifications'); expect(screen.getAllByTestId(/notification-/)).toHaveLength(3); }); test('renders empty state', () => { render( ); expect(screen.getByTestId('no-notifications')).toBeInTheDocument(); expect(screen.getByTestId('no-notifications')).toHaveTextContent('No notifications'); }); test('handles mark as read action', async () => { render( ); const firstNotification = screen.getByTestId('notification-0'); const markReadButton = screen.getByTestId('mark-read-0'); await act(async () => { fireEvent.click(markReadButton); }); expect(mockOnMarkAsRead).toHaveBeenCalledWith(mockNotifications[0].id); }); test('handles dismiss action', async () => { render( ); const dismissButton = screen.getByTestId('dismiss-0'); await act(async () => { fireEvent.click(dismissButton); }); expect(mockOnDismiss).toHaveBeenCalledWith(mockNotifications[0].id); }); test('shows read/unread status correctly', () => { render( ); // First notification should be unread expect(screen.getByTestId('notification-0')).toHaveTextContent('Unread'); expect(screen.getByTestId('mark-read-0')).toBeInTheDocument(); // Second notification should be read expect(screen.getByTestId('notification-1')).toHaveTextContent('Read'); expect(screen.queryByTestId('mark-read-1')).not.toBeInTheDocument(); }); test('shows different notification types', () => { const typeNotifications = [ { id: 1, title: 'Info', message: 'Info message', type: 'info', read: false }, { id: 2, title: 'Success', message: 'Success message', type: 'success', read: false }, { id: 3, title: 'Warning', message: 'Warning message', type: 'warning', read: false }, { id: 4, title: 'Error', message: 'Error message', type: 'error', read: false } ]; render( ); expect(screen.getAllByTestId(/notification-/)).toHaveLength(4); expect(screen.getByTestId('notification-0')).toHaveTextContent('info'); expect(screen.getByTestId('notification-1')).toHaveTextContent('success'); expect(screen.getByTestId('notification-2')).toHaveTextContent('warning'); expect(screen.getByTestId('notification-3')).toHaveTextContent('error'); }); }); describe('Dashboard Integration', () => { test('components work together in dashboard layout', () => { const DashboardLayout = ({ children }) => (
Sidebar
Header
{children}
); render( ); expect(screen.getByTestId('dashboard-layout')).toBeInTheDocument(); expect(screen.getByTestId('sidebar')).toBeInTheDocument(); expect(screen.getByTestId('main-content')).toBeInTheDocument(); expect(screen.getByTestId('header')).toBeInTheDocument(); expect(screen.getByTestId('dashboard-content')).toBeInTheDocument(); expect(screen.getByTestId('stats-card')).toBeInTheDocument(); expect(screen.getByTestId('chart-component')).toBeInTheDocument(); expect(screen.getByTestId('activity-feed')).toBeInTheDocument(); expect(screen.getByTestId('notification-panel')).toBeInTheDocument(); }); test('handles responsive behavior', () => { // Test mobile viewport Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 375, }); render(
); expect(screen.getByTestId('stats-card')).toBeInTheDocument(); expect(screen.getByTestId('chart-component')).toBeInTheDocument(); expect(screen.getByTestId('chart-height')).toHaveTextContent('200'); }); }); describe('Performance Considerations', () => { test('handles large datasets efficiently', () => { const largeDataset = Array.from({ length: 1000 }, (_, i) => ({ month: `Month ${i}`, value: Math.floor(Math.random() * 1000) })); const startTime = performance.now(); render(); const endTime = performance.now(); // Should render within 100ms for 1000 data points expect(endTime - startTime).toBeLessThan(100); expect(screen.getByTestId('chart-data-points')).toHaveTextContent('1000'); }); test('debounces rapid interactions', () => { jest.useFakeTimers(); const mockOnChange = jest.fn(); const { rerender } = render( ); // Simulate rapid changes for (let i = 0; i < 10; i++) { rerender( ); } // Fast-forward timer act(() => { jest.advanceTimersByTime(500); }); // Should only call once after debouncing expect(mockOnChange).toHaveBeenCalledTimes(1); jest.useRealTimers(); }); }); describe('Error Handling', () => { test('handles missing props gracefully', () => { render(); expect(screen.getByTestId('stats-card')).toBeInTheDocument(); expect(screen.getByTestId('card-title')).toBeInTheDocument(); expect(screen.getByTestId('card-value')).toBeInTheDocument(); }); test('handles invalid data types', () => { const invalidData = [ { month: 'Jan', value: 'invalid' }, { month: 'Feb', value: null }, { month: 'Mar', value: undefined } ]; render(); expect(screen.getByTestId('chart-component')).toBeInTheDocument(); expect(screen.getByTestId('chart-data-points')).toHaveTextContent('3'); }); }); });