mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 22:46:32 +00:00
251 lines
11 KiB
TypeScript
251 lines
11 KiB
TypeScript
// FE-COMP-BELL-001 to FE-COMP-BELL-021
|
|
import { render, screen, waitFor } from '../../../tests/helpers/render';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { vi } from 'vitest';
|
|
import { useAuthStore } from '../../store/authStore';
|
|
import { useInAppNotificationStore } from '../../store/inAppNotificationStore';
|
|
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
|
import { buildUser } from '../../../tests/helpers/factories';
|
|
import InAppNotificationBell from './InAppNotificationBell';
|
|
import type { InAppNotification } from '../../store/inAppNotificationStore';
|
|
|
|
let _notifId = 1;
|
|
function buildNotification(overrides: Partial<InAppNotification> = {}): InAppNotification {
|
|
return {
|
|
id: _notifId++,
|
|
type: 'simple',
|
|
scope: 'trip',
|
|
target: 1,
|
|
sender_id: 2,
|
|
sender_username: 'alice',
|
|
sender_avatar: null,
|
|
recipient_id: 1,
|
|
title_key: 'test',
|
|
title_params: {},
|
|
text_key: 'test.text',
|
|
text_params: {},
|
|
positive_text_key: null,
|
|
negative_text_key: null,
|
|
response: null,
|
|
navigate_text_key: null,
|
|
navigate_target: null,
|
|
is_read: false,
|
|
created_at: '2025-01-01T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
beforeAll(() => {
|
|
_notifId = 1;
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
|
|
});
|
|
|
|
describe('InAppNotificationBell', () => {
|
|
it('FE-COMP-BELL-001: renders without crashing', () => {
|
|
render(<InAppNotificationBell />);
|
|
expect(document.body).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-002: shows bell button', () => {
|
|
render(<InAppNotificationBell />);
|
|
const buttons = screen.getAllByRole('button');
|
|
expect(buttons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('FE-COMP-BELL-003: clicking bell opens notification panel', async () => {
|
|
const user = userEvent.setup();
|
|
render(<InAppNotificationBell />);
|
|
const bell = screen.getAllByRole('button')[0];
|
|
await user.click(bell);
|
|
// Panel shows "Notifications" title
|
|
expect(await screen.findByText('Notifications')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-004: notification panel shows empty state when no notifications', async () => {
|
|
const { http, HttpResponse } = await import('msw');
|
|
const { server } = await import('../../../tests/helpers/msw/server');
|
|
server.use(
|
|
http.get('/api/notifications/in-app', () => HttpResponse.json({ notifications: [], total: 0, unread_count: 0 })),
|
|
http.get('/api/notifications/in-app/unread-count', () => HttpResponse.json({ count: 0 })),
|
|
);
|
|
const user = userEvent.setup();
|
|
render(<InAppNotificationBell />);
|
|
const bell = screen.getAllByRole('button')[0];
|
|
await user.click(bell);
|
|
expect(await screen.findByText('No notifications')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-005: shows unread badge count when there are unread notifications', async () => {
|
|
seedStore(useInAppNotificationStore, { notifications: [], unreadCount: 5, isLoading: false });
|
|
render(<InAppNotificationBell />);
|
|
expect(screen.getByText('5')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-006: does not show badge when unread count is 0', () => {
|
|
seedStore(useInAppNotificationStore, { notifications: [], unreadCount: 0, isLoading: false });
|
|
render(<InAppNotificationBell />);
|
|
expect(screen.queryByText('0')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-007: panel shows Mark all read button when panel is open', async () => {
|
|
const user = userEvent.setup();
|
|
const notification = buildNotification({ id: 1, title_key: 'test', text_key: 'test.text' });
|
|
seedStore(useInAppNotificationStore, { notifications: [notification], unreadCount: 1, isLoading: false });
|
|
render(<InAppNotificationBell />);
|
|
const bell = screen.getAllByRole('button')[0];
|
|
await user.click(bell);
|
|
expect(await screen.findByTitle('Mark all read')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-008: panel shows empty description when no notifications', async () => {
|
|
const { http, HttpResponse } = await import('msw');
|
|
const { server } = await import('../../../tests/helpers/msw/server');
|
|
server.use(
|
|
http.get('/api/notifications/in-app', () => HttpResponse.json({ notifications: [], total: 0, unread_count: 0 })),
|
|
http.get('/api/notifications/in-app/unread-count', () => HttpResponse.json({ count: 0 })),
|
|
);
|
|
const user = userEvent.setup();
|
|
render(<InAppNotificationBell />);
|
|
await user.click(screen.getAllByRole('button')[0]);
|
|
expect(await screen.findByText("You're all caught up!")).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-009: bell is accessible as a button', () => {
|
|
render(<InAppNotificationBell />);
|
|
const bell = screen.getAllByRole('button')[0];
|
|
expect(bell).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-010: unread count greater than 99 shows 99+', () => {
|
|
seedStore(useInAppNotificationStore, { notifications: [], unreadCount: 150, isLoading: false });
|
|
render(<InAppNotificationBell />);
|
|
// Should show "99+" not "150"
|
|
expect(screen.queryByText('150')).not.toBeInTheDocument();
|
|
expect(screen.getByText('99+')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-011: Delete all button shown when notifications exist', async () => {
|
|
const user = userEvent.setup();
|
|
seedStore(useInAppNotificationStore, { notifications: [buildNotification()], unreadCount: 1, isLoading: false });
|
|
render(<InAppNotificationBell />);
|
|
await user.click(screen.getAllByRole('button')[0]);
|
|
expect(screen.getByTitle('Delete all')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-012: Delete all button NOT shown when no notifications', async () => {
|
|
const user = userEvent.setup();
|
|
seedStore(useInAppNotificationStore, { notifications: [], unreadCount: 0, isLoading: false, fetchNotifications: vi.fn() });
|
|
render(<InAppNotificationBell />);
|
|
await user.click(screen.getAllByRole('button')[0]);
|
|
await screen.findByText('Notifications');
|
|
expect(screen.queryByTitle('Delete all')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-013: Mark all read button NOT shown when unreadCount is 0', async () => {
|
|
const user = userEvent.setup();
|
|
seedStore(useInAppNotificationStore, { notifications: [buildNotification({ is_read: true })], unreadCount: 0, isLoading: false, fetchNotifications: vi.fn(), fetchUnreadCount: vi.fn() });
|
|
render(<InAppNotificationBell />);
|
|
await user.click(screen.getAllByRole('button')[0]);
|
|
await screen.findByText('Notifications');
|
|
expect(screen.queryByTitle('Mark all read')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-014: clicking Mark all read calls store action', async () => {
|
|
const user = userEvent.setup();
|
|
const markAllRead = vi.fn();
|
|
seedStore(useInAppNotificationStore, { notifications: [buildNotification()], unreadCount: 1, isLoading: false, markAllRead });
|
|
render(<InAppNotificationBell />);
|
|
await user.click(screen.getAllByRole('button')[0]);
|
|
await user.click(screen.getByTitle('Mark all read'));
|
|
expect(markAllRead).toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-COMP-BELL-015: clicking Delete all calls store action', async () => {
|
|
const user = userEvent.setup();
|
|
const deleteAll = vi.fn();
|
|
seedStore(useInAppNotificationStore, { notifications: [buildNotification()], unreadCount: 1, isLoading: false, deleteAll });
|
|
render(<InAppNotificationBell />);
|
|
await user.click(screen.getAllByRole('button')[0]);
|
|
await user.click(screen.getByTitle('Delete all'));
|
|
expect(deleteAll).toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-COMP-BELL-016: Show all notifications navigates to /notifications', async () => {
|
|
const user = userEvent.setup();
|
|
seedStore(useInAppNotificationStore, { notifications: [], unreadCount: 0, isLoading: false });
|
|
render(<InAppNotificationBell />);
|
|
await user.click(screen.getAllByRole('button')[0]);
|
|
await screen.findByText('Notifications');
|
|
const showAllBtn = screen.getByText('Show all notifications');
|
|
await user.click(showAllBtn);
|
|
// Panel should close after clicking show all
|
|
expect(screen.queryByText('No notifications')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-017: loading spinner shown when isLoading=true and notifications empty', async () => {
|
|
const user = userEvent.setup();
|
|
seedStore(useInAppNotificationStore, { notifications: [], unreadCount: 0, isLoading: true, fetchNotifications: vi.fn() });
|
|
render(<InAppNotificationBell />);
|
|
await user.click(screen.getAllByRole('button')[0]);
|
|
const spinner = document.querySelector('.animate-spin');
|
|
expect(spinner).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-BELL-018: notification items rendered up to 10', async () => {
|
|
const user = userEvent.setup();
|
|
const notifications = Array.from({ length: 12 }, (_, i) => buildNotification({ id: i + 1 }));
|
|
seedStore(useInAppNotificationStore, { notifications, unreadCount: 12, isLoading: false });
|
|
render(<InAppNotificationBell />);
|
|
await user.click(screen.getAllByRole('button')[0]);
|
|
await screen.findByText('Notifications');
|
|
// Each InAppNotificationItem renders with py-3 px-4 pattern; count rendered items
|
|
const items = document.querySelectorAll('.relative.px-4.py-3');
|
|
expect(items.length).toBeLessThanOrEqual(10);
|
|
});
|
|
|
|
it('FE-COMP-BELL-019: clicking outside the panel closes it', async () => {
|
|
const user = userEvent.setup();
|
|
seedStore(useInAppNotificationStore, { notifications: [], unreadCount: 0, isLoading: false });
|
|
render(<InAppNotificationBell />);
|
|
await user.click(screen.getAllByRole('button')[0]);
|
|
await screen.findByText('Notifications');
|
|
// The backdrop div is the fixed overlay — click it to close
|
|
const backdrop = document.querySelector('div[style*="position: fixed"][style*="inset: 0"]') as HTMLElement;
|
|
expect(backdrop).toBeInTheDocument();
|
|
await user.click(backdrop);
|
|
// Panel should be gone — "No notifications" text no longer visible
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('No notifications')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('FE-COMP-BELL-020: panel does not fetch again when already open and clicked again', async () => {
|
|
const user = userEvent.setup();
|
|
const fetchNotifications = vi.fn();
|
|
seedStore(useInAppNotificationStore, { notifications: [], unreadCount: 0, isLoading: false, fetchNotifications });
|
|
render(<InAppNotificationBell />);
|
|
const bell = screen.getAllByRole('button')[0];
|
|
// Open
|
|
await user.click(bell);
|
|
// Close
|
|
await user.click(bell);
|
|
// Re-open
|
|
await user.click(bell);
|
|
// fetchNotifications should be called once per open (2 total)
|
|
expect(fetchNotifications).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('FE-COMP-BELL-021: unread badge is large enough that the count is not squeezed', () => {
|
|
seedStore(useInAppNotificationStore, { notifications: [], unreadCount: 150, isLoading: false });
|
|
render(<InAppNotificationBell />);
|
|
const badge = screen.getByTestId('notification-badge');
|
|
expect(badge).toHaveTextContent('99+');
|
|
expect(badge.style.height).toBe('20px');
|
|
expect(badge.style.minWidth).toBe('20px');
|
|
});
|
|
});
|