test(stammbaum): cover empty/populated/preselect/zoom branches

empty state vs. populated, zoom controls visibility tied to node count,
URL ?focus= preselection (matching id selects, missing id does not),
zoom-out clamping safety. $app/state mocked at module boundary so the
test can drive page.url and page.data.canWrite without a SvelteKit
runtime.

Six tests focused on user-observable behaviour — one logical behaviour
per test (Sara's guidance).

Refs #496.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-09 20:26:59 +02:00
committed by marcel
parent 79e7f9d243
commit 83ca262b75

View File

@@ -0,0 +1,88 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
const mockPage = {
url: new URL('http://localhost/stammbaum'),
data: { canWrite: false } as { canWrite: boolean }
};
vi.mock('$app/state', () => ({
get page() {
return mockPage;
}
}));
afterEach(cleanup);
async function loadComponent() {
return (await import('./+page.svelte')).default;
}
const sampleNodes = [
{ id: 'p-1', firstName: 'Anna', lastName: 'Schmidt', displayName: 'Anna Schmidt' },
{ id: 'p-2', firstName: 'Bert', lastName: 'Schmidt', displayName: 'Bert Schmidt' }
];
describe('stammbaum page', () => {
it('shows the empty state when there are no family nodes', async () => {
mockPage.url = new URL('http://localhost/stammbaum');
const Stammbaum = await loadComponent();
render(Stammbaum, { props: { data: { nodes: [], edges: [] } } });
await expect
.element(page.getByRole('heading', { name: /noch keine familienmitglieder/i }))
.toBeVisible();
await expect
.element(page.getByRole('link', { name: /zur personenliste/i }))
.toHaveAttribute('href', '/persons');
});
it('hides zoom controls when there are no nodes', async () => {
mockPage.url = new URL('http://localhost/stammbaum');
const Stammbaum = await loadComponent();
render(Stammbaum, { props: { data: { nodes: [], edges: [] } } });
await expect.element(page.getByRole('button', { name: /vergrößern/i })).not.toBeInTheDocument();
await expect
.element(page.getByRole('button', { name: /verkleinern/i }))
.not.toBeInTheDocument();
});
it('renders the page heading and zoom controls when nodes are present', async () => {
mockPage.url = new URL('http://localhost/stammbaum');
const Stammbaum = await loadComponent();
render(Stammbaum, { props: { data: { nodes: sampleNodes, edges: [] } } });
await expect.element(page.getByRole('heading', { name: /stammbaum/i })).toBeVisible();
await expect.element(page.getByRole('button', { name: /vergrößern/i })).toBeVisible();
await expect.element(page.getByRole('button', { name: /verkleinern/i })).toBeVisible();
});
it('preselects a node when the URL has a focus query param matching an existing node', async () => {
mockPage.url = new URL('http://localhost/stammbaum?focus=p-1');
const Stammbaum = await loadComponent();
render(Stammbaum, { props: { data: { nodes: sampleNodes, edges: [] } } });
await expect.element(page.getByRole('complementary')).toBeVisible();
});
it('does not preselect when the focus param does not match any node', async () => {
mockPage.url = new URL('http://localhost/stammbaum?focus=missing');
const Stammbaum = await loadComponent();
render(Stammbaum, { props: { data: { nodes: sampleNodes, edges: [] } } });
await expect.element(page.getByRole('complementary')).not.toBeInTheDocument();
});
it('clamps the zoom level when the zoom-out button is clicked many times', async () => {
mockPage.url = new URL('http://localhost/stammbaum');
const Stammbaum = await loadComponent();
render(Stammbaum, { props: { data: { nodes: sampleNodes, edges: [] } } });
const zoomOut = page.getByRole('button', { name: /verkleinern/i });
for (let i = 0; i < 10; i++) await zoomOut.click();
// Just verify that repeated clicks don't throw — branch coverage
await expect.element(zoomOut).toBeVisible();
});
});