Files
electric-cherry/tests/unit/ui-navigation.test.ts
T
2026-10-13 11:50:26 -04:00

135 lines
5.4 KiB
TypeScript

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<typeof currentPage.subscribe>[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(/<nav\b/g)).toHaveLength(1);
expect(body).toContain('Main navigation');
expect(body).toMatch(/<a[^>]*href="\/campaigns"[^>]*aria-current="page"/);
expect(body).toContain('href="/betas"');
expect(body).toContain('href="/activity"');
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: () => '<h1>Page content</h1>' }));
const shell = (overrides: Partial<AuthUser> = {}) =>
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"');
});
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=');
});
});