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 = {}) => ({ id: 'p-1', displayName: 'Anna Schmidt', familyMember: true, ...overrides }); describe('StammbaumSidePanel', () => { let fetchSpy: ReturnType; 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); }); });