import { afterEach, describe, expect, test, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/svelte'; vi.mock('$app/state', () => ({ page: { url: new URL('http://localhost/') } })); vi.mock('$app/navigation', () => ({ goto: vi.fn() })); // Mutable handle so individual tests can flip the user between admin / // non-admin without re-importing the module. const userState = vi.hoisted(() => ({ current: { id: '1', username: 'alice', is_admin: false } as { id: string; username: string; is_admin: boolean; } | null })); vi.mock('$lib/auth/store.svelte', () => ({ user: { get value() { return userState.current; } }, logout: vi.fn().mockResolvedValue(undefined) })); import Shell from './Shell.svelte'; import { logout } from '$lib/auth/store.svelte'; import { goto } from '$app/navigation'; afterEach(() => { vi.clearAllMocks(); userState.current = { id: '1', username: 'alice', is_admin: false }; }); describe('Shell', () => { test('renders the username in the header', () => { render(Shell); expect(screen.getByText('alice')).toBeInTheDocument(); }); test('main nav renders the six primary surfaces in order', () => { render(Shell); const labels = ['Home', 'Artists', 'Albums', 'Liked', 'Discover', 'Playlists']; for (const label of labels) { expect(screen.getByRole('link', { name: label })).toBeInTheDocument(); } expect(screen.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/'); expect(screen.getByRole('link', { name: 'Artists' })).toHaveAttribute('href', '/library/artists'); expect(screen.getByRole('link', { name: 'Albums' })).toHaveAttribute('href', '/library/albums'); expect(screen.getByRole('link', { name: 'Discover' })).toHaveAttribute('href', '/discover'); expect(screen.getByRole('link', { name: 'Playlists' })).toHaveAttribute('href', '/playlists'); }); test('main nav omits Search, Requests, Hidden, Settings, and Admin', () => { userState.current = { id: '1', username: 'alice', is_admin: true }; render(Shell); // Search reaches /search via the global SearchInput, Requests is a // tab inside Discover, Hidden lives under Settings, Settings + Admin // live in the username dropdown — none belong on the main nav. const navLinks = screen .getAllByRole('link') .filter((el) => el.closest('nav') !== null) .map((el) => el.textContent?.trim()); for (const label of ['Search', 'Requests', 'Hidden', 'Settings', 'Admin']) { expect(navLinks).not.toContain(label); } }); test('user-menu shows Settings + Log out for non-admin users', async () => { userState.current = { id: '1', username: 'alice', is_admin: false }; render(Shell); await fireEvent.click(screen.getByRole('button', { name: /alice/i })); expect(screen.getByRole('menuitem', { name: 'Settings' })).toHaveAttribute('href', '/settings'); expect(screen.getByRole('menuitem', { name: /log out/i })).toBeInTheDocument(); expect(screen.queryByRole('menuitem', { name: 'Admin' })).not.toBeInTheDocument(); }); test('user-menu adds Admin between Settings and Log out for admin users', async () => { userState.current = { id: '1', username: 'alice', is_admin: true }; render(Shell); await fireEvent.click(screen.getByRole('button', { name: /alice/i })); const items = screen.getAllByRole('menuitem').map((el) => el.textContent?.trim()); expect(items).toEqual(['Settings', 'Admin', 'Log out']); expect(screen.getByRole('menuitem', { name: 'Admin' })).toHaveAttribute('href', '/admin'); }); test('user-menu "Log out" calls logout() and navigates to /login', async () => { render(Shell); await fireEvent.click(screen.getByRole('button', { name: /alice/i })); await fireEvent.click(screen.getByRole('menuitem', { name: /log out/i })); expect(logout).toHaveBeenCalledTimes(1); expect(goto).toHaveBeenCalledWith('/login', { replaceState: true }); }); });