Files
familienarchiv/frontend/src/routes/layout.svelte.spec.ts
Marcel e268e2dbca
Some checks failed
CI / Compose Bucket Idempotency (push) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / fail2ban Regex (push) Has been cancelled
fix(tests): use native element clicks in layout dropdown spec
CDP-based Playwright clicks (locator.click()) do not reliably trigger
Svelte 5 onclick handlers — documented in commit 0c765d81 which fixed
13 other specs. The layout dropdown tests were missed in that pass.

Applies the same pattern: ((await locator.element()) as HTMLElement).click()
for button interactions, and native KeyboardEvent dispatch for the Escape
test (dispatched on the button so it bubbles to the parent div's onkeydown).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:07:22 +02:00

127 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import { createRawSnippet } from 'svelte';
vi.mock('$env/static/public', () => ({ PUBLIC_NOTIFICATION_POLL_MS: '60000' }));
// NotificationBell calls notificationStore.init() on mount, which creates an
// EventSource and immediately fetches the unread count. In the test environment
// the SSE connection fails (no backend), triggering onerror → another fetch.
// If that fetch returns 401, notifications.svelte.ts calls
// window.location.href = '/login', navigating the test iframe and breaking the
// Playwright connection. Stubbing fetch to return 200 keeps it in-check.
beforeEach(() => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({ count: 0, content: [] })
})
);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
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',
firstName: 'Max',
lastName: 'Müller',
email: 'max@example.com',
groups: [],
enabled: true,
createdAt: ''
},
canWrite: true,
canAnnotate: false,
canBlogWrite: false,
...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', email: 'fallback@example.com', 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();
});
});
// ─── Upload link ──────────────────────────────────────────────────────────────
describe('Layout upload link', () => {
it('has aria-label for screen reader access', async () => {
render(Layout, { data: makeData(), children: emptySnippet });
const link = page.getByRole('link', { name: /Hochladen|Upload|Subir/i });
await expect.element(link).toHaveAttribute('aria-label');
});
it('navigates to /documents/new', async () => {
render(Layout, { data: makeData(), children: emptySnippet });
const link = page.getByRole('link', { name: /Hochladen|Upload|Subir/i });
await expect.element(link).toHaveAttribute('href', '/documents/new');
});
});
// ─── 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/ }).element()) as HTMLElement).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/ }).element()) as HTMLElement).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/ }).element()) as HTMLElement).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 btnEl = (await page.getByRole('button', { name: /MM/ }).element()) as HTMLElement;
btnEl.click();
await expect.element(page.getByRole('link', { name: /Profil/i })).toBeInTheDocument();
btnEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
await tick();
await expect.element(page.getByRole('link', { name: /Profil/i })).not.toBeInTheDocument();
});
});