import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, act, waitFor } from '@testing-library/react'; import { NotificationProvider, useNotifications } from '../NotificationContext'; import { NotificationType } from '../../types/notification'; import React from 'react'; // Mock component to test the context const TestComponent: React.FC = () => { const { notifications, unreadCount, addNotification, markAsRead, markAllAsRead, clearNotification, clearAll, addBatchNotification, } = useNotifications(); return (
{unreadCount}
{notifications.length}
{notifications.map((notification) => (
{notification.title} {notification.message} {notification.type} {notification.read.toString()}
))}
); }; const renderWithProvider = () => { return render( ); }; describe('NotificationContext', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); }); test('should initialize with empty state', () => { renderWithProvider(); expect(screen.getByTestId('notifications-count')).toHaveTextContent('0'); expect(screen.getByTestId('unread-count')).toHaveTextContent('0'); }); test('should add a single notification', async () => { renderWithProvider(); const addButton = screen.getByTestId('add-notification'); act(() => { addButton.click(); }); expect(screen.getByTestId('notifications-count')).toHaveTextContent('1'); expect(screen.getByTestId('unread-count')).toHaveTextContent('1'); // Find the first notification element const notificationsContainer = screen.getByTestId('notifications'); const firstNotification = notificationsContainer.firstElementChild as HTMLElement; expect(firstNotification).toBeTruthy(); const notificationId = firstNotification.getAttribute('data-testid')?.replace('notification-', ''); expect(notificationId).toBeTruthy(); expect(screen.getByTestId(`title-${notificationId}`)).toHaveTextContent('Test'); expect(screen.getByTestId(`message-${notificationId}`)).toHaveTextContent('Test message'); expect(screen.getByTestId(`type-${notificationId}`)).toHaveTextContent('success'); expect(screen.getByTestId(`read-${notificationId}`)).toHaveTextContent('false'); }); test('should mark notification as read', async () => { renderWithProvider(); // Add a notification first act(() => { screen.getByTestId('add-notification').click(); }); expect(screen.getByTestId('unread-count')).toHaveTextContent('1'); // Mark as read act(() => { screen.getByTestId('mark-first-read').click(); }); expect(screen.getByTestId('unread-count')).toHaveTextContent('0'); expect(screen.getByTestId('notifications-count')).toHaveTextContent('1'); }); test('should mark all notifications as read', async () => { renderWithProvider(); // Add multiple notifications act(() => { screen.getByTestId('add-notification').click(); screen.getByTestId('add-notification').click(); }); expect(screen.getByTestId('unread-count')).toHaveTextContent('2'); // Mark all as read act(() => { screen.getByTestId('mark-all-read').click(); }); expect(screen.getByTestId('unread-count')).toHaveTextContent('0'); expect(screen.getByTestId('notifications-count')).toHaveTextContent('2'); }); test('should clear a single notification', async () => { renderWithProvider(); // Add a notification act(() => { screen.getByTestId('add-notification').click(); }); expect(screen.getByTestId('notifications-count')).toHaveTextContent('1'); // Clear the notification act(() => { screen.getByTestId('clear-first').click(); }); expect(screen.getByTestId('notifications-count')).toHaveTextContent('0'); expect(screen.getByTestId('unread-count')).toHaveTextContent('0'); }); test('should clear all notifications', async () => { renderWithProvider(); // Add multiple notifications act(() => { screen.getByTestId('add-notification').click(); screen.getByTestId('add-notification').click(); }); expect(screen.getByTestId('notifications-count')).toHaveTextContent('2'); // Clear all act(() => { screen.getByTestId('clear-all').click(); }); expect(screen.getByTestId('notifications-count')).toHaveTextContent('0'); expect(screen.getByTestId('unread-count')).toHaveTextContent('0'); }); test('should handle batch notifications with batching window', async () => { renderWithProvider(); // Add batch notification act(() => { screen.getByTestId('add-batch').click(); }); // No notification should appear immediately (batching window) expect(screen.getByTestId('notifications-count')).toHaveTextContent('0'); // Fast forward time to trigger batch completion act(() => { vi.advanceTimersByTime(2100); // Slightly more than BATCH_WINDOW_MS (2000ms) }); await waitFor(() => { expect(screen.getByTestId('notifications-count')).toHaveTextContent('1'); }, { timeout: 10000 }); expect(screen.getByTestId('unread-count')).toHaveTextContent('1'); }, 15000); test('should batch multiple operations of same type', async () => { renderWithProvider(); // Add multiple batch notifications quickly act(() => { screen.getByTestId('add-batch').click(); screen.getByTestId('add-batch').click(); }); // No notifications should appear immediately expect(screen.getByTestId('notifications-count')).toHaveTextContent('0'); // Fast forward time act(() => { vi.advanceTimersByTime(2100); }); await waitFor(() => { expect(screen.getByTestId('notifications-count')).toHaveTextContent('1'); }, { timeout: 10000 }); // Should only have one batched notification expect(screen.getByTestId('unread-count')).toHaveTextContent('1'); }, 15000); test('should limit notifications to MAX_NOTIFICATIONS', async () => { renderWithProvider(); // Add 52 notifications (more than MAX_NOTIFICATIONS = 50) act(() => { for (let i = 0; i < 52; i++) { screen.getByTestId('add-notification').click(); } }); expect(screen.getByTestId('notifications-count')).toHaveTextContent('50'); expect(screen.getByTestId('unread-count')).toHaveTextContent('50'); }); test('should throw error when used outside provider', () => { // Mock console.error to avoid noisy test output const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); expect(() => { render(); }).toThrow('useNotifications must be used within NotificationProvider'); consoleSpy.mockRestore(); }); }); // Test different notification types describe('NotificationContext - Notification Types', () => { const notificationTypes: NotificationType[] = ['success', 'error', 'info', 'warning']; beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); test.each(notificationTypes)('should handle %s notification type', (type) => { const TestTypeComponent: React.FC = () => { const { addNotification, notifications } = useNotifications(); return (
{notifications.length}
{notifications.length > 0 ? notifications[0].type : ''}
); }; render( ); act(() => { screen.getByTestId('add-notification').click(); }); expect(screen.getByTestId('notifications-count')).toHaveTextContent('1'); expect(screen.getByTestId('notification-type')).toHaveTextContent(type); }); }); // Test batch notification scenarios describe('NotificationContext - Batch Notifications', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); const BatchTestComponent: React.FC = () => { const { addBatchNotification, notifications } = useNotifications(); return (
{notifications.length}
{notifications.length > 0 ? notifications[0].title : ''}
{notifications.length > 0 ? notifications[0].message : ''}
); }; test('should create single file notification for one file', async () => { render( ); act(() => { screen.getByTestId('single-success').click(); }); act(() => { vi.advanceTimersByTime(2100); }); await waitFor(() => { expect(screen.getByTestId('notifications-count')).toHaveTextContent('1'); }, { timeout: 10000 }); expect(screen.getByTestId('notification-title')).toHaveTextContent('File Uploaded'); expect(screen.getByTestId('notification-message')).toHaveTextContent('document.pdf uploaded successfully'); }, 15000); test('should create batch notification for multiple files', async () => { render( ); act(() => { screen.getByTestId('batch-success').click(); }); act(() => { vi.advanceTimersByTime(2100); }); await waitFor(() => { expect(screen.getByTestId('notifications-count')).toHaveTextContent('1'); }, { timeout: 10000 }); expect(screen.getByTestId('notification-title')).toHaveTextContent('Batch Upload Complete'); expect(screen.getByTestId('notification-message')).toHaveTextContent('3 files uploaded successfully'); }, 15000); test('should handle mixed success/failure batch', async () => { render( ); act(() => { screen.getByTestId('mixed-batch').click(); }); act(() => { vi.advanceTimersByTime(2100); }); await waitFor(() => { expect(screen.getByTestId('notifications-count')).toHaveTextContent('1'); }, { timeout: 10000 }); expect(screen.getByTestId('notification-title')).toHaveTextContent('Batch Upload Complete'); expect(screen.getByTestId('notification-message')).toHaveTextContent('2 files uploaded, 1 failed'); }, 15000); test('should handle all failed batch', async () => { render( ); act(() => { screen.getByTestId('all-failed').click(); }); act(() => { vi.advanceTimersByTime(2100); }); await waitFor(() => { expect(screen.getByTestId('notifications-count')).toHaveTextContent('1'); }, { timeout: 10000 }); expect(screen.getByTestId('notification-title')).toHaveTextContent('Batch Upload Complete'); expect(screen.getByTestId('notification-message')).toHaveTextContent('Failed to upload 2 files'); }, 15000); });