mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
ad27c5f6be
- Export __clearVersionCacheForTests() from adminService; call in
versionNotification beforeEach to reset module-scoped cache between
tests (VNOTIF-002..006 failed because VNOTIF-001 cached
update_available:false, short-circuiting all subsequent test fetches)
- Seed appVersion:'2.9.10' in Navbar test authStore; appVersion moved
from local useEffect state to authStore in last commit so the test
render no longer fetches it independently (FE-COMP-NAVBAR-016)
- Add data-testid="weekend-days" to VacaySettings weekend-days
container; use within() in tests to scope button count to that
section, fixing false positives from the week-start buttons which
share the same inline styles (FE-COMP-VACAYSETTINGS-003/004)
- Pass isPrerelease={true} in GitHubPanel FE-ADMIN-GH-007; component
filters out prerelease releases when isPrerelease=false so the badge
was never rendered (pre-existing, unrelated to last commit)
308 lines
12 KiB
TypeScript
308 lines
12 KiB
TypeScript
// FE-COMP-NAVBAR-001 to FE-COMP-NAVBAR-028
|
|
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 { useAddonStore } from '../../store/addonStore';
|
|
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' })),
|
|
http.get('/api/addons', () => HttpResponse.json({ addons: [] })),
|
|
);
|
|
seedStore(useAuthStore, { user: buildUser({ username: 'testuser', role: 'user' }), isAuthenticated: true, appVersion: '2.9.10' });
|
|
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);
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-016: app version shown in user menu', async () => {
|
|
const user = userEvent.setup();
|
|
render(<Navbar />);
|
|
await user.click(screen.getByText('testuser'));
|
|
await waitFor(() => {
|
|
expect(screen.getByText('v2.9.10')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-017: Settings link navigates to /settings', async () => {
|
|
const user = userEvent.setup();
|
|
render(<Navbar />);
|
|
await user.click(screen.getByText('testuser'));
|
|
const settingsLink = screen.getByRole('link', { name: /settings/i });
|
|
expect(settingsLink).toHaveAttribute('href', '/settings');
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-018: Admin link navigates to /admin for admin user', async () => {
|
|
const user = userEvent.setup();
|
|
seedStore(useAuthStore, { user: buildUser({ username: 'adminuser', role: 'admin' }), isAuthenticated: true });
|
|
render(<Navbar />);
|
|
await user.click(screen.getByText('adminuser'));
|
|
const adminLink = screen.getByRole('link', { name: /admin/i });
|
|
expect(adminLink).toHaveAttribute('href', '/admin');
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-019: share button rendered when onShare prop provided', () => {
|
|
render(<Navbar onShare={vi.fn()} />);
|
|
const shareBtn = screen.getByRole('button', { name: /share/i });
|
|
expect(shareBtn).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-020: share button click calls onShare', async () => {
|
|
const user = userEvent.setup();
|
|
const onShare = vi.fn();
|
|
render(<Navbar onShare={onShare} />);
|
|
const shareBtn = screen.getByRole('button', { name: /share/i });
|
|
await user.click(shareBtn);
|
|
expect(onShare).toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-021: share button NOT rendered when onShare prop omitted', () => {
|
|
render(<Navbar />);
|
|
expect(screen.queryByRole('button', { name: /share/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-022: dark mode toggle shows Moon when light, Sun when dark', () => {
|
|
seedStore(useSettingsStore, { settings: buildSettings({ dark_mode: false }) });
|
|
const { unmount } = render(<Navbar />);
|
|
// Moon icon button should be present (title = 'nav.darkMode' i.e. 'Dark mode')
|
|
expect(document.querySelector('[title]')).toBeTruthy();
|
|
unmount();
|
|
|
|
seedStore(useSettingsStore, { settings: buildSettings({ dark_mode: 'dark' }) });
|
|
render(<Navbar />);
|
|
// Sun icon button should be present when dark mode is on
|
|
const buttons = screen.getAllByRole('button');
|
|
expect(buttons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-023: dark mode toggle calls updateSetting', async () => {
|
|
const user = userEvent.setup();
|
|
const updateSetting = vi.fn().mockResolvedValue(undefined);
|
|
seedStore(useSettingsStore, { settings: buildSettings({ dark_mode: false }), updateSetting });
|
|
render(<Navbar />);
|
|
// Find the dark mode toggle button by title attribute
|
|
const toggleBtn = document.querySelector('button[title]') as HTMLElement;
|
|
expect(toggleBtn).toBeTruthy();
|
|
await user.click(toggleBtn);
|
|
expect(updateSetting).toHaveBeenCalledWith('dark_mode', 'dark');
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-024: global addon nav links appear when addons enabled', () => {
|
|
server.use(
|
|
http.get('/api/addons', () => HttpResponse.json({
|
|
addons: [{ id: 'vacay', name: 'Vacay', icon: 'CalendarDays', type: 'global', enabled: true }],
|
|
})),
|
|
);
|
|
seedStore(useAddonStore, {
|
|
addons: [{ id: 'vacay', name: 'Vacay', icon: 'CalendarDays', type: 'global', enabled: true }],
|
|
});
|
|
render(<Navbar />);
|
|
expect(screen.getByRole('link', { name: /vacay/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-025: global addon links hidden when in trip view (tripTitle set)', () => {
|
|
seedStore(useAddonStore, {
|
|
addons: [{ id: 'vacay', name: 'Vacay', icon: 'CalendarDays', type: 'global', enabled: true }],
|
|
});
|
|
render(<Navbar tripTitle="Japan 2025" />);
|
|
expect(screen.queryByRole('link', { name: /vacay/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-026: notification bell visible when tripId provided', () => {
|
|
render(<Navbar tripId="1" />);
|
|
// InAppNotificationBell renders a button — check it is present
|
|
const buttons = screen.getAllByRole('button');
|
|
expect(buttons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-027: user avatar image shown when avatar_url set', () => {
|
|
seedStore(useAuthStore, {
|
|
user: buildUser({ username: 'testuser', avatar_url: 'https://example.com/av.jpg' }),
|
|
isAuthenticated: true,
|
|
});
|
|
render(<Navbar />);
|
|
const avatarImg = document.querySelector('img[src="https://example.com/av.jpg"]');
|
|
expect(avatarImg).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-028: user initial shown when no avatar_url', () => {
|
|
seedStore(useAuthStore, {
|
|
user: buildUser({ username: 'testuser', avatar_url: null }),
|
|
isAuthenticated: true,
|
|
});
|
|
render(<Navbar />);
|
|
// The initial is rendered as the first char uppercased in a div
|
|
expect(screen.getAllByText('T')[0]).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-029: clicking backdrop overlay closes user menu', async () => {
|
|
const user = userEvent.setup();
|
|
render(<Navbar />);
|
|
await user.click(screen.getByText('testuser'));
|
|
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
// The backdrop overlay is a fixed-inset div rendered in the portal
|
|
const backdrop = document.querySelector('[style*="inset: 0"]') as HTMLElement;
|
|
if (backdrop) {
|
|
await user.click(backdrop);
|
|
expect(screen.queryByText('Settings')).not.toBeInTheDocument();
|
|
}
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-030: dark mode auto uses system preference', () => {
|
|
// 'auto' dark_mode relies on matchMedia — seed with auto and render
|
|
seedStore(useSettingsStore, { settings: buildSettings({ dark_mode: 'auto' }) });
|
|
render(<Navbar />);
|
|
// Component should render without errors regardless of system preference
|
|
expect(document.querySelector('nav')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-031: dark mode toggle calls updateSetting with light when currently dark', async () => {
|
|
const user = userEvent.setup();
|
|
const updateSetting = vi.fn().mockResolvedValue(undefined);
|
|
seedStore(useSettingsStore, { settings: buildSettings({ dark_mode: 'dark' }), updateSetting });
|
|
render(<Navbar />);
|
|
const toggleBtn = document.querySelector('button[title]') as HTMLElement;
|
|
expect(toggleBtn).toBeTruthy();
|
|
await user.click(toggleBtn);
|
|
expect(updateSetting).toHaveBeenCalledWith('dark_mode', 'light');
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-032: user email shown in open user menu', async () => {
|
|
const user = userEvent.setup();
|
|
seedStore(useAuthStore, {
|
|
user: buildUser({ username: 'testuser', email: 'testuser@example.com' }),
|
|
isAuthenticated: true,
|
|
});
|
|
render(<Navbar />);
|
|
await user.click(screen.getByText('testuser'));
|
|
expect(screen.getByText('testuser@example.com')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NAVBAR-033: administrator badge shown for admin user in open menu', async () => {
|
|
const user = userEvent.setup();
|
|
seedStore(useAuthStore, {
|
|
user: buildUser({ username: 'adminuser', role: 'admin' }),
|
|
isAuthenticated: true,
|
|
});
|
|
render(<Navbar />);
|
|
await user.click(screen.getByText('adminuser'));
|
|
expect(screen.getByText('Administrator')).toBeInTheDocument();
|
|
});
|
|
});
|