505 lines
14 KiB
Plaintext
505 lines
14 KiB
Plaintext
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 (
|
|
<div>
|
|
<div data-testid="unread-count">{unreadCount}</div>
|
|
<div data-testid="notifications-count">{notifications.length}</div>
|
|
<div data-testid="notifications">
|
|
{notifications.map((notification) => (
|
|
<div key={notification.id} data-testid={`notification-${notification.id}`}>
|
|
<span data-testid={`title-${notification.id}`}>{notification.title}</span>
|
|
<span data-testid={`message-${notification.id}`}>{notification.message}</span>
|
|
<span data-testid={`type-${notification.id}`}>{notification.type}</span>
|
|
<span data-testid={`read-${notification.id}`}>{notification.read.toString()}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<button
|
|
data-testid="add-notification"
|
|
onClick={() =>
|
|
addNotification({
|
|
type: 'success',
|
|
title: 'Test',
|
|
message: 'Test message',
|
|
})
|
|
}
|
|
>
|
|
Add Notification
|
|
</button>
|
|
<button
|
|
data-testid="add-batch"
|
|
onClick={() =>
|
|
addBatchNotification('success', 'upload', [
|
|
{ name: 'file1.pdf', success: true },
|
|
{ name: 'file2.pdf', success: true },
|
|
])
|
|
}
|
|
>
|
|
Add Batch
|
|
</button>
|
|
<button
|
|
data-testid="mark-first-read"
|
|
onClick={() => {
|
|
if (notifications.length > 0) {
|
|
markAsRead(notifications[0].id);
|
|
}
|
|
}}
|
|
>
|
|
Mark First Read
|
|
</button>
|
|
<button data-testid="mark-all-read" onClick={markAllAsRead}>
|
|
Mark All Read
|
|
</button>
|
|
<button
|
|
data-testid="clear-first"
|
|
onClick={() => {
|
|
if (notifications.length > 0) {
|
|
clearNotification(notifications[0].id);
|
|
}
|
|
}}
|
|
>
|
|
Clear First
|
|
</button>
|
|
<button data-testid="clear-all" onClick={clearAll}>
|
|
Clear All
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderWithProvider = () => {
|
|
return render(
|
|
<NotificationProvider>
|
|
<TestComponent />
|
|
</NotificationProvider>
|
|
);
|
|
};
|
|
|
|
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(<TestComponent />);
|
|
}).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 (
|
|
<div>
|
|
<div data-testid="notifications-count">{notifications.length}</div>
|
|
<div data-testid="notification-type">
|
|
{notifications.length > 0 ? notifications[0].type : ''}
|
|
</div>
|
|
<button
|
|
data-testid="add-notification"
|
|
onClick={() =>
|
|
addNotification({
|
|
type,
|
|
title: `Test ${type}`,
|
|
message: `Test ${type} message`,
|
|
})
|
|
}
|
|
>
|
|
Add {type} Notification
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
render(
|
|
<NotificationProvider>
|
|
<TestTypeComponent />
|
|
</NotificationProvider>
|
|
);
|
|
|
|
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 (
|
|
<div>
|
|
<div data-testid="notifications-count">{notifications.length}</div>
|
|
<div data-testid="notification-title">
|
|
{notifications.length > 0 ? notifications[0].title : ''}
|
|
</div>
|
|
<div data-testid="notification-message">
|
|
{notifications.length > 0 ? notifications[0].message : ''}
|
|
</div>
|
|
<button
|
|
data-testid="single-success"
|
|
onClick={() =>
|
|
addBatchNotification('success', 'upload', [
|
|
{ name: 'document.pdf', success: true },
|
|
])
|
|
}
|
|
>
|
|
Single Success
|
|
</button>
|
|
<button
|
|
data-testid="batch-success"
|
|
onClick={() =>
|
|
addBatchNotification('success', 'upload', [
|
|
{ name: 'doc1.pdf', success: true },
|
|
{ name: 'doc2.pdf', success: true },
|
|
{ name: 'doc3.pdf', success: true },
|
|
])
|
|
}
|
|
>
|
|
Batch Success
|
|
</button>
|
|
<button
|
|
data-testid="mixed-batch"
|
|
onClick={() =>
|
|
addBatchNotification('warning', 'upload', [
|
|
{ name: 'doc1.pdf', success: true },
|
|
{ name: 'doc2.pdf', success: false },
|
|
{ name: 'doc3.pdf', success: true },
|
|
])
|
|
}
|
|
>
|
|
Mixed Batch
|
|
</button>
|
|
<button
|
|
data-testid="all-failed"
|
|
onClick={() =>
|
|
addBatchNotification('error', 'upload', [
|
|
{ name: 'doc1.pdf', success: false },
|
|
{ name: 'doc2.pdf', success: false },
|
|
])
|
|
}
|
|
>
|
|
All Failed
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
test('should create single file notification for one file', async () => {
|
|
render(
|
|
<NotificationProvider>
|
|
<BatchTestComponent />
|
|
</NotificationProvider>
|
|
);
|
|
|
|
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(
|
|
<NotificationProvider>
|
|
<BatchTestComponent />
|
|
</NotificationProvider>
|
|
);
|
|
|
|
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(
|
|
<NotificationProvider>
|
|
<BatchTestComponent />
|
|
</NotificationProvider>
|
|
);
|
|
|
|
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(
|
|
<NotificationProvider>
|
|
<BatchTestComponent />
|
|
</NotificationProvider>
|
|
);
|
|
|
|
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);
|
|
}); |