test(front): add test suite frontend (WIP)

This commit is contained in:
jubnl
2026-04-07 12:31:09 +02:00
parent 96080e8a03
commit 3c31902885
97 changed files with 16973 additions and 4 deletions
@@ -0,0 +1,105 @@
// FE-COMP-BELL-001 to FE-COMP-BELL-010
import { render, screen, waitFor } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
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';
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
await screen.findByText('Notifications');
});
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);
await screen.findByText('No notifications');
});
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 = {
id: 1, 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: 0,
created_at: '2025-01-01T00:00:00.000Z',
};
seedStore(useInAppNotificationStore, { notifications: [notification], unreadCount: 1, isLoading: false });
render(<InAppNotificationBell />);
const bell = screen.getAllByRole('button')[0];
await user.click(bell);
await screen.findByTitle('Mark all read');
});
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]);
await screen.findByText("You're all caught up!");
});
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();
});
});
@@ -0,0 +1,131 @@
// FE-COMP-NAVBAR-001 to FE-COMP-NAVBAR-015
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 { useAuthStore } from '../../store/authStore';
import { useSettingsStore } from '../../store/settingsStore';
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { buildUser, buildSettings } from '../../../tests/helpers/factories';
import Navbar from './Navbar';
beforeEach(() => {
resetAllStores();
server.use(
http.get('/api/auth/app-config', () => HttpResponse.json({ version: '2.9.10' })),
);
seedStore(useAuthStore, { user: buildUser({ username: 'testuser', role: 'user' }), isAuthenticated: true });
seedStore(useSettingsStore, { settings: buildSettings() });
});
describe('Navbar', () => {
it('FE-COMP-NAVBAR-001: renders without crashing', () => {
render(<Navbar />);
expect(document.body).toBeInTheDocument();
});
it('FE-COMP-NAVBAR-002: shows TREK logo/brand', () => {
render(<Navbar />);
// The Navbar shows the app icon — check for presence of the nav element
expect(document.querySelector('nav') || document.body).toBeTruthy();
});
it('FE-COMP-NAVBAR-003: shows username in user menu trigger', () => {
render(<Navbar />);
expect(screen.getByText('testuser')).toBeInTheDocument();
});
it('FE-COMP-NAVBAR-004: user menu opens on click', async () => {
const user = userEvent.setup();
render(<Navbar />);
// Click the username to open dropdown
await user.click(screen.getByText('testuser'));
// Settings option appears
expect(screen.getByText('Settings')).toBeInTheDocument();
});
it('FE-COMP-NAVBAR-005: user menu shows Log out option', async () => {
const user = userEvent.setup();
render(<Navbar />);
await user.click(screen.getByText('testuser'));
expect(screen.getByText('Log out')).toBeInTheDocument();
});
it('FE-COMP-NAVBAR-006: shows Settings link in user menu', async () => {
const user = userEvent.setup();
render(<Navbar />);
await user.click(screen.getByText('testuser'));
expect(screen.getByText('Settings')).toBeInTheDocument();
});
it('FE-COMP-NAVBAR-007: shows My Trips link in navbar', () => {
render(<Navbar />);
// nav.myTrips = "My Trips" is in the main navbar (hidden on mobile via CSS, but CSS is not processed in tests)
// The link to /dashboard is present regardless
const dashboardLinks = document.querySelectorAll('a[href="/dashboard"]');
expect(dashboardLinks.length).toBeGreaterThan(0);
});
it('FE-COMP-NAVBAR-008: clicking Log out calls logout', async () => {
const user = userEvent.setup();
const logout = vi.fn();
seedStore(useAuthStore, { user: buildUser({ username: 'testuser' }), isAuthenticated: true, logout });
render(<Navbar />);
await user.click(screen.getByText('testuser'));
await user.click(screen.getByText('Log out'));
expect(logout).toHaveBeenCalled();
});
it('FE-COMP-NAVBAR-009: admin user sees Admin option', async () => {
const user = userEvent.setup();
seedStore(useAuthStore, { user: buildUser({ username: 'admin', role: 'admin' }), isAuthenticated: true });
render(<Navbar />);
await user.click(screen.getByText('admin'));
expect(screen.getByText('Admin')).toBeInTheDocument();
});
it('FE-COMP-NAVBAR-010: regular user does not see Admin option', async () => {
const user = userEvent.setup();
render(<Navbar />);
await user.click(screen.getByText('testuser'));
expect(screen.queryByText('Admin')).not.toBeInTheDocument();
});
it('FE-COMP-NAVBAR-011: shows tripTitle when provided', () => {
render(<Navbar tripTitle="Paris 2026" />);
expect(screen.getByText('Paris 2026')).toBeInTheDocument();
});
it('FE-COMP-NAVBAR-012: shows back button when showBack is true', () => {
render(<Navbar showBack={true} onBack={vi.fn()} />);
// Back button is a button element
const backBtns = screen.getAllByRole('button');
expect(backBtns.length).toBeGreaterThan(0);
});
it('FE-COMP-NAVBAR-013: clicking back button calls onBack', async () => {
const user = userEvent.setup();
const onBack = vi.fn();
render(<Navbar showBack={true} onBack={onBack} />);
// Find the back button (ArrowLeft icon)
const buttons = screen.getAllByRole('button');
// First button should be the back button
await user.click(buttons[0]);
expect(onBack).toHaveBeenCalled();
});
it('FE-COMP-NAVBAR-014: notification bell is rendered when user is logged in', () => {
render(<Navbar />);
// InAppNotificationBell is rendered — check that body has some content
expect(document.body.children.length).toBeGreaterThan(0);
});
it('FE-COMP-NAVBAR-015: dark mode toggle is accessible in user menu', async () => {
const user = userEvent.setup();
render(<Navbar />);
await user.click(screen.getByText('testuser'));
// Dark mode / Light mode / Auto mode options
const darkModeEls = screen.getAllByRole('button');
expect(darkModeEls.length).toBeGreaterThan(0);
});
});