170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
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);
|
||
});
|
||
});
|