Spaces:
Sleeping
Sleeping
File size: 12,784 Bytes
766d85d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | // 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();
});
});
|