Spaces:
Sleeping
Sleeping
| import { describe, it, expect, beforeEach, vi } from 'vitest'; | |
| import { render, screen, waitFor } from '../../tests/helpers/render'; | |
| import userEvent from '@testing-library/user-event'; | |
| import { http, HttpResponse } from 'msw'; | |
| import { server } from '../../tests/helpers/msw/server'; | |
| import { resetAllStores, seedStore } from '../../tests/helpers/store'; | |
| import { buildUser } from '../../tests/helpers/factories'; | |
| import { useAuthStore } from '../store/authStore'; | |
| import { useInAppNotificationStore } from '../store/inAppNotificationStore'; | |
| import InAppNotificationsPage from './InAppNotificationsPage'; | |
| // Mock InAppNotificationItem to simplify rendering | |
| vi.mock('../components/Notifications/InAppNotificationItem', () => ({ | |
| default: ({ notification }: { notification: { id: number; is_read: number } }) => ( | |
| <div | |
| data-testid={`notification-${notification.id}`} | |
| data-read={notification.is_read} | |
| > | |
| Notification {notification.id} | |
| </div> | |
| ), | |
| })); | |
| beforeEach(() => { | |
| resetAllStores(); | |
| seedStore(useAuthStore, { isAuthenticated: true, user: buildUser() }); | |
| }); | |
| describe('InAppNotificationsPage', () => { | |
| describe('FE-PAGE-NOTIFPAGE-001: Notification list loads on mount', () => { | |
| it('fetches and displays notifications on mount', async () => { | |
| render(<InAppNotificationsPage />); | |
| // Default handler returns 20 notifications (offset 0..19 from 25 total) | |
| await waitFor(() => { | |
| expect(screen.getByTestId('notification-1')).toBeInTheDocument(); | |
| }); | |
| }); | |
| }); | |
| describe('FE-PAGE-NOTIFPAGE-002: Unread notifications shown with indicator', () => { | |
| it('shows unread count badge when there are unread notifications', async () => { | |
| render(<InAppNotificationsPage />); | |
| // Default handler returns unread_count: 5 | |
| // The badge shows the count as a span inside the heading | |
| await waitFor(() => { | |
| // The "5" badge appears next to the Notifications heading | |
| const badges = screen.getAllByText('5'); | |
| expect(badges.length).toBeGreaterThan(0); | |
| }); | |
| }); | |
| }); | |
| describe('FE-PAGE-NOTIFPAGE-003: Mark all read button', () => { | |
| it('shows "Mark all read" button when there are unread notifications', async () => { | |
| render(<InAppNotificationsPage />); | |
| await waitFor(() => { | |
| // Button has "Mark all read" text (possibly hidden on mobile via CSS class) | |
| // In jsdom, CSS "hidden" class doesn't actually hide elements | |
| expect(screen.getByRole('button', { name: /mark all read/i })).toBeInTheDocument(); | |
| }); | |
| }); | |
| }); | |
| describe('FE-PAGE-NOTIFPAGE-004: Delete all button', () => { | |
| it('shows "Delete all" button when there are notifications', async () => { | |
| render(<InAppNotificationsPage />); | |
| await waitFor(() => { | |
| expect(screen.getByRole('button', { name: /delete all/i })).toBeInTheDocument(); | |
| }); | |
| }); | |
| }); | |
| describe('FE-PAGE-NOTIFPAGE-005: Empty state when no notifications', () => { | |
| it('shows empty state when API returns no notifications', async () => { | |
| server.use( | |
| http.get('/api/notifications/in-app', () => { | |
| return HttpResponse.json({ | |
| notifications: [], | |
| total: 0, | |
| unread_count: 0, | |
| }); | |
| }), | |
| ); | |
| render(<InAppNotificationsPage />); | |
| await waitFor(() => { | |
| expect(screen.getByText(/no notifications/i)).toBeInTheDocument(); | |
| }); | |
| }); | |
| }); | |
| describe('FE-PAGE-NOTIFPAGE-006: Filter toggle', () => { | |
| it('renders "All" and "Unread" filter buttons', async () => { | |
| render(<InAppNotificationsPage />); | |
| await waitFor(() => { | |
| expect(screen.getByRole('button', { name: /^all$/i })).toBeInTheDocument(); | |
| }); | |
| // The unread filter button uses t('notifications.unreadOnly') = 'Unread' | |
| expect(screen.getByRole('button', { name: /^unread$/i })).toBeInTheDocument(); | |
| }); | |
| }); | |
| describe('FE-PAGE-NOTIFPAGE-007: Unread only filter hides read notifications', () => { | |
| it('clicking Unread filter shows only unread notifications', async () => { | |
| const user = userEvent.setup(); | |
| // Seed store with known mix of read/unread | |
| const unreadNotif = { | |
| id: 100, is_read: 0, type: 'simple', | |
| scope: 'trip', target: 1, sender_id: 2, | |
| sender_username: 'alice', sender_avatar: null, | |
| recipient_id: 1, title_key: 'n', title_params: '{}', | |
| text_key: 'n', text_params: '{}', | |
| positive_text_key: null, negative_text_key: null, | |
| response: null, navigate_text_key: null, navigate_target: null, | |
| created_at: '2025-01-01T00:00:00Z', | |
| }; | |
| const readNotif = { | |
| id: 101, is_read: 1, type: 'simple', | |
| scope: 'trip', target: 1, sender_id: 2, | |
| sender_username: 'alice', sender_avatar: null, | |
| recipient_id: 1, title_key: 'n', title_params: '{}', | |
| text_key: 'n', text_params: '{}', | |
| positive_text_key: null, negative_text_key: null, | |
| response: null, navigate_text_key: null, navigate_target: null, | |
| created_at: '2025-01-01T00:00:00Z', | |
| }; | |
| seedStore(useInAppNotificationStore, { | |
| notifications: [unreadNotif, readNotif], | |
| unreadCount: 1, | |
| total: 2, | |
| isLoading: false, | |
| hasMore: false, | |
| fetchNotifications: vi.fn(), | |
| markAllRead: vi.fn(), | |
| deleteAll: vi.fn(), | |
| } as any); | |
| render(<InAppNotificationsPage />); | |
| // Both notifications start visible | |
| await waitFor(() => { | |
| expect(screen.getByTestId('notification-100')).toBeInTheDocument(); | |
| expect(screen.getByTestId('notification-101')).toBeInTheDocument(); | |
| }); | |
| // Click "Unread" filter | |
| await user.click(screen.getByRole('button', { name: /^unread$/i })); | |
| // Only unread notification should be visible | |
| await waitFor(() => { | |
| expect(screen.getByTestId('notification-100')).toBeInTheDocument(); | |
| expect(screen.queryByTestId('notification-101')).not.toBeInTheDocument(); | |
| }); | |
| }); | |
| }); | |
| describe('FE-PAGE-NOTIFPAGE-008: Page title', () => { | |
| it('shows "Notifications" heading', async () => { | |
| render(<InAppNotificationsPage />); | |
| await waitFor(() => { | |
| expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); | |
| }); | |
| expect(screen.getByRole('heading', { level: 1 }).textContent).toMatch(/notifications/i); | |
| }); | |
| }); | |
| describe('FE-PAGE-NOTIFPAGE-009: Notification total count', () => { | |
| it('shows total notification count in the subtitle', async () => { | |
| render(<InAppNotificationsPage />); | |
| await waitFor(() => { | |
| // "25 notifications" (total from default handler) | |
| expect(screen.getByText(/25 notifications/i)).toBeInTheDocument(); | |
| }); | |
| }); | |
| }); | |
| }); | |