Files
RedditBot/tests/dashboard/notifications.test.tsx

94 lines
3.2 KiB
TypeScript

// @vitest-environment jsdom
import '@testing-library/jest-dom/vitest';
import { act, cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { DashboardEvent } from '../../packages/api-contracts/src/index.js';
import {
NotificationBell,
NotificationProvider,
NotificationViewport,
notificationForEvent,
} from '../../apps/dashboard/src/app/notifications.js';
class FakeRealtime {
private listener?: (event: DashboardEvent) => void;
subscribe(listener: (event: DashboardEvent) => void): () => void {
this.listener = listener;
return () => { this.listener = undefined; };
}
emit(event: DashboardEvent): void {
this.listener?.(event);
}
}
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe('dashboard notifications', () => {
it('turns actionable realtime events into useful notification copy', () => {
expect(notificationForEvent({
id: 'reply-1',
occurredAt: '2026-08-09T09:30:00.000Z',
type: 'reply.received',
payload: { conversationId: 'room/one', replyId: 'message-1', sender: 'alice', preview: 'Sounds good!' },
})).toMatchObject({
title: 'New reply from u/alice',
message: 'Sounds good!',
tone: 'success',
href: '/conversations/room%2Fone',
});
expect(notificationForEvent({
id: 'log-1',
occurredAt: '2026-08-09T09:30:00.000Z',
type: 'log.created',
payload: { level: 'info', message: 'Routine detail' },
})).toBeNull();
});
it('shows new events as toasts and keeps them in a readable notification centre', async () => {
const realtime = new FakeRealtime();
const user = userEvent.setup();
render(
<MemoryRouter>
<NotificationProvider realtime={realtime}>
<NotificationBell />
<NotificationViewport />
</NotificationProvider>
</MemoryRouter>,
);
expect(screen.getByRole('button', { name: 'Notifications' })).toBeInTheDocument();
act(() => realtime.emit({
id: 'reply-1',
occurredAt: '2026-08-09T09:30:00.000Z',
type: 'reply.received',
payload: { conversationId: 'conversation-1', replyId: 'message-1', sender: 'alice', preview: 'Sounds good!' },
}));
const toast = screen.getByRole('status');
expect(toast).toHaveTextContent('New reply from u/alice');
expect(screen.getByRole('button', { name: 'Notifications, 1 unread' })).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Notifications, 1 unread' }));
const centre = screen.getByLabelText('Notifications', { selector: 'aside' });
expect(centre).toHaveClass('is-open');
expect(within(centre).getByText('Sounds good!')).toBeInTheDocument();
expect(within(centre).getByRole('link', { name: /New reply from u\/alice/ })).toHaveAttribute(
'href',
'/conversations/conversation-1',
);
expect(screen.getByRole('button', { name: 'Notifications' })).toBeInTheDocument();
await user.click(within(centre).getByRole('button', { name: 'Clear all' }));
expect(within(centre).getByText('All quiet')).toBeInTheDocument();
});
});