// @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, shouldToastForEvent, } 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(); globalThis.localStorage.clear(); vi.restoreAllMocks(); vi.useRealTimers(); }); 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( , ); 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(); }); it('keeps routine sync completions in the notification centre without showing a toast', async () => { const realtime = new FakeRealtime(); const user = userEvent.setup(); render( , ); const syncEvent: DashboardEvent = { id: 'sync-1', occurredAt: '2026-08-09T09:30:00.000Z', type: 'sync.completed', payload: { syncId: 'conversation-sync-1', kind: 'conversations', processed: 12 }, }; expect(shouldToastForEvent(syncEvent)).toBe(false); act(() => realtime.emit(syncEvent)); expect(screen.queryByRole('status')).not.toBeInTheDocument(); await user.click(screen.getByRole('button', { name: 'Notifications, 1 unread' })); expect(within(screen.getByLabelText('Notifications', { selector: 'aside' })).getByText('Conversations sync complete')) .toBeInTheDocument(); }); it('keeps important event toasts visible until they are dismissed', async () => { vi.useFakeTimers(); const realtime = new FakeRealtime(); render( , ); act(() => realtime.emit({ id: 'media-1', occurredAt: '2026-08-09T09:30:00.000Z', type: 'media.received', payload: { conversationId: 'conversation-1', sender: 'alice', count: 1 }, })); expect(screen.getByRole('status')).toHaveTextContent('Image received from u/alice'); act(() => vi.advanceTimersByTime(60_000)); expect(screen.getByRole('status')).toBeInTheDocument(); act(() => screen.getByRole('button', { name: 'Dismiss Image received from u/alice' }).click()); expect(screen.queryByRole('status')).not.toBeInTheDocument(); }); });