import { afterEach, describe, expect, it, vi } from 'vitest'; import { render } from 'svelte/server'; import { writable } from 'svelte/store'; import { createRawSnippet } from 'svelte'; import Navigation from '../../src/lib/ui/components/Navigation.svelte'; import Layout from '../../src/routes/+layout.svelte'; import { fleet } from '../../src/lib/ui/stores/fleet.js'; import type { AuthUser } from '../../src/lib/auth.js'; const currentPage = writable({ url: new URL('http://localhost/campaigns/42'), data: {} }); vi.mock('$app/stores', () => ({ page: { subscribe: (run: Parameters[0]) => currentPage.subscribe(run) } })); vi.mock('$app/navigation', () => ({ goto: vi.fn(), beforeNavigate: vi.fn(), afterNavigate: vi.fn(), onNavigate: vi.fn() })); afterEach(() => { fleet.set({ servers: [], operations: [], receivedAt: 0 }); currentPage.set({ url: new URL('http://localhost/campaigns/42'), data: {} }); }); describe('sidebar navigation', () => { it('exposes every allowed page in one navigation and marks the current page', () => { const body = render(Navigation, { props: { role: 'operator' } }).body; expect(body.match(/]*href="\/campaigns"[^>]*aria-current="page"/); expect(body).toContain('href="/betas"'); expect(body).not.toContain('href="/activity"'); expect(body).toContain('href="/provisioning"'); expect(body).toContain('href="/maintenance"'); expect(body).toContain('href="/metrics"'); expect(body).toContain('href="/bank"'); expect(body).not.toContain('href="/settings"'); expect(body).not.toContain('href="/server-keys"'); }); it('does not show invented attention counts before the fleet snapshot loads', () => { expect(render(Navigation, { props: { role: 'admin' } }).body).not.toMatch( /needs? attention/ ); fleet.set({ receivedAt: Date.now(), operations: [], servers: [ { name: 't5_zombie_theater_1', game: 't5', map: 'zombie_theater', last_players: null, last_map: null, last_gametype: null, last_seen_at: null, last_error: null, roster: [], health: 'offline', held: [], active_since: null, inactive_since: null, game_started_at: null, game_ended_at: null } ] }); expect(render(Navigation, { props: { role: 'admin' } }).body).toContain( 'aria-label="Servers, 1 needs attention"' ); }); it('counts hosts without servers and clears the badge when the next host summary is healthy', () => { fleet.set({ receivedAt: Date.now(), operations: [], servers: [], hostAttention: 2 }); expect(render(Navigation, { props: { role: 'admin' } }).body).toContain( 'aria-label="Hosts, 2 need attention"' ); fleet.update((value) => ({ ...value, hostAttention: 0 })); expect(render(Navigation, { props: { role: 'admin' } }).body).not.toMatch( /aria-label="Hosts, \d/ ); }); }); const user: AuthUser = { id: '1', username: 'operator', displayName: 'Operator', role: 'operator', disabled: false, mustChangePassword: false, createdAt: '', updatedAt: '', lastLoginAt: null }; const children = createRawSnippet(() => ({ render: () => '

Page content

' })); const shell = (overrides: Partial = {}) => render(Layout, { props: { children, data: { user: { ...user, ...overrides }, setupRequired: false } } }).body; describe('application shell', () => { it('explains Super Viewer access while exposing administrator navigation and shortcuts', () => { const body = shell({ role: 'super_viewer' }); expect(body).toContain('Super Viewer access'); expect(body).toContain('href="/settings/users"'); expect(body).toContain('href="/server-keys"'); expect(body).toContain('href="/settings"'); expect(body).toContain('g h'); expect(body).toContain('g k'); }); it('offers collapsible desktop navigation, a labelled mobile drawer, search and help', () => { const body = shell(); expect(body).toContain('id="desktop-navigation"'); expect(body).toContain('aria-label="Collapse navigation"'); expect(body).toContain('aria-label="Open navigation"'); expect(body).toContain('id="mobile-navigation"'); expect(body).toContain('aria-keyshortcuts="Meta+k Control+k"'); expect(body).toContain('aria-label="Keyboard shortcuts"'); expect(body).not.toContain('class="primary-navigation"'); expect(body).not.toContain('variant="secondary"'); expect(body).not.toContain('g d'); expect(body).not.toContain('Activity log'); }); it('preserves the viewer notice and hides navigation while a password change is required', () => { const viewer = shell({ role: 'viewer' }); expect(viewer).toContain('Viewer access'); expect(viewer).not.toContain('href="/server-keys"'); const password = shell({ mustChangePassword: true }); expect(password).not.toContain('id="desktop-navigation"'); expect(password).not.toContain('id="mobile-navigation"'); expect(password).not.toContain('aria-keyshortcuts='); }); });