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