feat(frontend): replace logout button with user avatar dropdown in nav
Show user initials (e.g. MM) in a circular button when name is set, or a fallback person icon. Clicking opens a dropdown with links to /profile and a logout form. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
86
frontend/src/routes/layout.svelte.spec.ts
Normal file
86
frontend/src/routes/layout.svelte.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page, userEvent } from 'vitest/browser';
|
||||
import { createRawSnippet } from 'svelte';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const emptySnippet = createRawSnippet(() => ({ render: () => '<span></span>' }));
|
||||
import Layout from './+layout.svelte';
|
||||
|
||||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
// Minimal data required by the layout
|
||||
const makeData = (overrides = {}) => ({
|
||||
user: {
|
||||
id: '1',
|
||||
username: 'max',
|
||||
firstName: 'Max',
|
||||
lastName: 'Müller',
|
||||
groups: [],
|
||||
enabled: true,
|
||||
createdAt: ''
|
||||
},
|
||||
canWrite: true,
|
||||
...overrides
|
||||
});
|
||||
|
||||
// ─── User avatar ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Layout – user avatar button', () => {
|
||||
it('shows user initials when first and last name are set', async () => {
|
||||
render(Layout, { data: makeData(), children: emptySnippet });
|
||||
await expect.element(page.getByRole('button', { name: /MM/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows fallback icon button when names are not set', async () => {
|
||||
render(Layout, {
|
||||
data: makeData({
|
||||
user: { id: '1', username: 'x', groups: [], enabled: true, createdAt: '' }
|
||||
}),
|
||||
children: emptySnippet
|
||||
});
|
||||
// Button should still exist (with aria-label for accessibility)
|
||||
await expect.element(page.getByRole('button', { name: /Profil/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Dropdown ─────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Layout – user dropdown', () => {
|
||||
it('dropdown is hidden initially', async () => {
|
||||
render(Layout, { data: makeData(), children: emptySnippet });
|
||||
await tick();
|
||||
await expect.element(page.getByRole('link', { name: /Profil/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens dropdown on button click', async () => {
|
||||
render(Layout, { data: makeData(), children: emptySnippet });
|
||||
await page.getByRole('button', { name: /MM/ }).click();
|
||||
await expect.element(page.getByRole('link', { name: /Profil/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('profile link points to /profile', async () => {
|
||||
render(Layout, { data: makeData(), children: emptySnippet });
|
||||
await page.getByRole('button', { name: /MM/ }).click();
|
||||
await expect
|
||||
.element(page.getByRole('link', { name: /Profil/i }))
|
||||
.toHaveAttribute('href', '/profile');
|
||||
});
|
||||
|
||||
it('logout button is in the dropdown', async () => {
|
||||
render(Layout, { data: makeData(), children: emptySnippet });
|
||||
await page.getByRole('button', { name: /MM/ }).click();
|
||||
await expect.element(page.getByRole('button', { name: /Abmelden/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes dropdown when Escape is pressed', async () => {
|
||||
render(Layout, { data: makeData(), children: emptySnippet });
|
||||
const btn = page.getByRole('button', { name: /MM/ });
|
||||
await btn.click();
|
||||
await expect.element(page.getByRole('link', { name: /Profil/i })).toBeInTheDocument();
|
||||
await userEvent.keyboard('{Escape}');
|
||||
await tick();
|
||||
await expect.element(page.getByRole('link', { name: /Profil/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user