// FE-COMP-NOTIF-001 to FE-COMP-NOTIF-016 import { render, screen, waitFor } from '../../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { useAuthStore } from '../../store/authStore'; import { useSettingsStore } from '../../store/settingsStore'; import { useInAppNotificationStore } from '../../store/inAppNotificationStore'; import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { buildUser, buildSettings } from '../../../tests/helpers/factories'; import InAppNotificationItem from './InAppNotificationItem'; const buildNotification = (overrides = {}) => ({ id: 1, type: 'simple', scope: 'trip', target: 1, sender_id: 2, sender_username: 'alice', sender_avatar: null, recipient_id: 1, title_key: 'notifications.title', title_params: '{}', text_key: 'notifications.empty', text_params: '{}', positive_text_key: null, negative_text_key: null, response: null, navigate_text_key: null, navigate_target: null, is_read: 0, created_at: new Date().toISOString(), ...overrides, }); beforeEach(() => { resetAllStores(); seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true }); seedStore(useSettingsStore, { settings: buildSettings() }); }); describe('InAppNotificationItem', () => { it('FE-COMP-NOTIF-001: renders without crashing', () => { render(); expect(document.body).toBeInTheDocument(); }); it('FE-COMP-NOTIF-002: shows sender avatar initial letter', () => { render(); // Avatar shows first letter uppercase: "B" expect(screen.getByText('B')).toBeInTheDocument(); }); it('FE-COMP-NOTIF-003: shows notification title text', () => { render(); // t('notifications.title') = "Notifications" expect(screen.getByText('Notifications')).toBeInTheDocument(); }); it('FE-COMP-NOTIF-004: shows notification body text', () => { render(); // t('notifications.empty') = "No notifications" expect(screen.getByText('No notifications')).toBeInTheDocument(); }); it('FE-COMP-NOTIF-005: shows Mark as read button for unread notification', () => { render(); expect(screen.getByTitle('Mark as read')).toBeInTheDocument(); }); it('FE-COMP-NOTIF-006: does not show Mark as read button for read notification', () => { render(); expect(screen.queryByTitle('Mark as read')).not.toBeInTheDocument(); }); it('FE-COMP-NOTIF-007: shows Delete button', () => { render(); expect(screen.getByTitle('Delete')).toBeInTheDocument(); }); it('FE-COMP-NOTIF-008: clicking Mark as read calls markRead', async () => { const user = userEvent.setup(); const markRead = vi.fn().mockResolvedValue(undefined); seedStore(useInAppNotificationStore, { markRead }); render(); await user.click(screen.getByTitle('Mark as read')); expect(markRead).toHaveBeenCalledWith(42); }); it('FE-COMP-NOTIF-009: clicking Delete calls deleteNotification', async () => { const user = userEvent.setup(); const deleteNotification = vi.fn().mockResolvedValue(undefined); seedStore(useInAppNotificationStore, { deleteNotification }); render(); await user.click(screen.getByTitle('Delete')); expect(deleteNotification).toHaveBeenCalledWith(99); }); it('FE-COMP-NOTIF-010: shows relative timestamp', () => { render(); // Recent notification shows "just now" expect(screen.getByText('just now')).toBeInTheDocument(); }); it('FE-COMP-NOTIF-011: shows avatar image when sender_avatar is provided', () => { render( ); expect(document.querySelector('img')).toBeInTheDocument(); expect(document.querySelector('img')?.getAttribute('src')).toBe('https://example.com/avatar.png'); }); it('FE-COMP-NOTIF-012: boolean notification shows Accept and Reject buttons', () => { render( ); expect(screen.getByText('Yes')).toBeInTheDocument(); expect(screen.getByText('No')).toBeInTheDocument(); }); it('FE-COMP-NOTIF-013: clicking Accept calls respondToBoolean with positive', async () => { const user = userEvent.setup(); const respondToBoolean = vi.fn().mockResolvedValue(undefined); seedStore(useInAppNotificationStore, { respondToBoolean }); render( ); await user.click(screen.getByText('Yes')); expect(respondToBoolean).toHaveBeenCalledWith(55, 'positive'); }); it('FE-COMP-NOTIF-014: clicking Reject calls respondToBoolean with negative', async () => { const user = userEvent.setup(); const respondToBoolean = vi.fn().mockResolvedValue(undefined); seedStore(useInAppNotificationStore, { respondToBoolean }); render( ); await user.click(screen.getByText('No')); expect(respondToBoolean).toHaveBeenCalledWith(66, 'negative'); }); it('FE-COMP-NOTIF-015: navigate notification shows action button', () => { render( ); // t('notifications.title') = "Notifications" — the navigate button renders this const navigateBtn = document.querySelector('button[style*="pointer"]') ?? Array.from(document.querySelectorAll('button')).find(b => b.textContent?.includes('Notifications')); expect(navigateBtn).toBeInTheDocument(); }); it('FE-COMP-NOTIF-016: clicking navigate button marks read and navigates', async () => { const user = userEvent.setup(); const markRead = vi.fn().mockResolvedValue(undefined); const onClose = vi.fn(); seedStore(useInAppNotificationStore, { markRead }); render( ); // The navigate button renders t('notifications.title') = "Notifications" const btn = Array.from(document.querySelectorAll('button')).find( b => b.textContent?.includes('Notifications') ); expect(btn).toBeTruthy(); await user.click(btn!); expect(markRead).toHaveBeenCalledWith(77); expect(onClose).toHaveBeenCalled(); }); });