Files
familienarchiv/frontend/src/lib/person/genealogy/StammbaumSidePanel.svelte.test.ts
2026-05-11 21:50:28 +02:00

170 lines
4.9 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 { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import StammbaumSidePanel from './StammbaumSidePanel.svelte';
vi.mock('$app/navigation', () => ({
beforeNavigate: () => {},
afterNavigate: () => {},
goto: vi.fn(),
invalidate: vi.fn(),
invalidateAll: vi.fn(),
preloadCode: vi.fn(),
preloadData: vi.fn(),
pushState: vi.fn(),
replaceState: vi.fn(),
disableScrollHandling: vi.fn(),
onNavigate: () => () => {}
}));
afterEach(cleanup);
const baseNode = (overrides: Record<string, unknown> = {}) => ({
id: 'p-1',
displayName: 'Anna Schmidt',
familyMember: true,
...overrides
});
describe('StammbaumSidePanel', () => {
let fetchSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url: RequestInfo | URL) => {
const u = url.toString();
if (u.includes('/relationships') && !u.includes('/inferred')) {
return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
}
if (u.includes('/inferred-relationships')) {
return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
}
return new Response('[]', { status: 200 });
});
});
afterEach(() => {
fetchSpy?.mockRestore();
});
it('renders the heading from node displayName', async () => {
render(StammbaumSidePanel, {
props: { node: baseNode(), onClose: () => {} }
});
await expect.element(page.getByRole('heading', { name: 'Anna Schmidt' })).toBeVisible();
});
it('shows birth/death years when set', async () => {
render(StammbaumSidePanel, {
props: {
node: baseNode({ birthYear: 1899, deathYear: 1950 }),
onClose: () => {}
}
});
expect(document.body.textContent).toContain('1899');
expect(document.body.textContent).toContain('1950');
});
it('renders ? when birthYear is missing but deathYear is set', async () => {
render(StammbaumSidePanel, {
props: { node: baseNode({ deathYear: 1950 }), onClose: () => {} }
});
expect(document.body.textContent).toMatch(/\?/);
});
it('does not render the years line when both are missing', async () => {
render(StammbaumSidePanel, {
props: { node: baseNode(), onClose: () => {} }
});
expect(document.body.textContent).not.toMatch(/\?\?/);
});
it('calls onClose when the close button is clicked', async () => {
const onClose = vi.fn();
render(StammbaumSidePanel, {
props: { node: baseNode(), onClose }
});
const closeBtn = document.querySelector('button[aria-label]') as HTMLButtonElement;
closeBtn.click();
expect(onClose).toHaveBeenCalledOnce();
});
it('calls onClose when Escape is pressed on window', async () => {
const onClose = vi.fn();
render(StammbaumSidePanel, {
props: { node: baseNode(), onClose }
});
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
expect(onClose).toHaveBeenCalledOnce();
});
it('does not call onClose for non-Escape keys', async () => {
const onClose = vi.fn();
render(StammbaumSidePanel, {
props: { node: baseNode(), onClose }
});
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
expect(onClose).not.toHaveBeenCalled();
});
it('renders the person detail link', async () => {
render(StammbaumSidePanel, {
props: { node: baseNode(), onClose: () => {} }
});
const link = document.querySelector('a[href="/persons/p-1"]');
expect(link).not.toBeNull();
});
it('shows the empty placeholder when there are no direct relationships', async () => {
render(StammbaumSidePanel, {
props: { node: baseNode(), onClose: () => {} }
});
await expect.element(page.getByText(/keine beziehungen bekannt/i)).toBeVisible();
});
it('shows the error banner when both fetch calls fail', async () => {
fetchSpy.mockImplementation(async () => new Response('error', { status: 500 }));
render(StammbaumSidePanel, {
props: { node: baseNode(), onClose: () => {} }
});
await vi.waitFor(() => {
expect(document.querySelector('[role="alert"]')).not.toBeNull();
});
});
it('shows the AddRelationshipForm only when canWrite is true', async () => {
render(StammbaumSidePanel, {
props: { node: baseNode(), onClose: () => {}, canWrite: true }
});
await vi.waitFor(() => {
const addButtons = Array.from(document.querySelectorAll('button')).filter((b) =>
b.textContent?.toLowerCase().includes('hinzufügen')
);
expect(addButtons.length).toBeGreaterThan(0);
});
});
it('hides the AddRelationshipForm when canWrite is false', async () => {
render(StammbaumSidePanel, {
props: { node: baseNode(), onClose: () => {}, canWrite: false }
});
// canWrite=false hides the form — assert the toggle button is absent in the rendered DOM.
const addButtons = Array.from(document.querySelectorAll('button')).filter((b) =>
b.textContent?.toLowerCase().includes('hinzufügen')
);
expect(addButtons.length).toBe(0);
});
});