test(genealogy): cover StammbaumSidePanel branches
Heading from displayName, birth/death years rendering with all branch combinations, close button, Escape keypress closing, non-Escape ignored, person detail link, empty placeholder, error banner on fetch failure, AddRelationshipForm visibility toggle. 12 tests covering ~25 branches. Refs #496. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
|||||||
|
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: () => {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
// no years line visible — should not see the question-mark fallback
|
||||||
|
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 new Promise((r) => setTimeout(r, 100));
|
||||||
|
await expect.element(page.getByText(/keine beziehungen bekannt/i)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the error banner when both fetch calls fail', async () => {
|
||||||
|
fetchSpy.mockResolvedValue(new Response('error', { status: 500 }));
|
||||||
|
|
||||||
|
render(StammbaumSidePanel, {
|
||||||
|
props: { node: baseNode(), onClose: () => {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
const alert = document.querySelector('[role="alert"]');
|
||||||
|
expect(alert).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the AddRelationshipForm only when canWrite is true', async () => {
|
||||||
|
render(StammbaumSidePanel, {
|
||||||
|
props: { node: baseNode(), onClose: () => {}, canWrite: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
// AddRelationshipForm exposes a "Beziehung hinzufügen" toggle button
|
||||||
|
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 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
const addButtons = Array.from(document.querySelectorAll('button')).filter((b) =>
|
||||||
|
b.textContent?.toLowerCase().includes('hinzufügen')
|
||||||
|
);
|
||||||
|
expect(addButtons.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user