test: cover CorrespondentSuggestionsDropdown and PersonCard branches

CorrespondentSuggestionsDropdown: empty list still renders the static
heading and 'Alle Korrespondenten' row, populated rows when not loading,
loading hides correspondent rows, initials fallback (lastName-only when
firstName is null), click + keyboard selection, Escape closes.

PersonCard: full matrix of conditional UI — title visibility for PERSON
vs non-PERSON, avatar initials path (firstName+lastName vs lastName-only
fallback), PersonTypeBadge presence for non-PERSON types, alias, life
dates, notes, and the canWrite=true/false branches that gate the edit
link (Nora's authorization-rendering rule).

21 tests covering ~50 branches.

Refs #496.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-09 20:14:16 +02:00
committed by marcel
parent 2e5a9bd36c
commit fb52db1253
2 changed files with 275 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import CorrespondentSuggestionsDropdown from './CorrespondentSuggestionsDropdown.svelte';
afterEach(cleanup);
const corrA = { id: 'a', firstName: 'Anna', lastName: 'Schmidt', displayName: 'Anna Schmidt' };
const corrB = { id: 'b', firstName: null, lastName: 'Müller', displayName: 'Müller' };
describe('CorrespondentSuggestionsDropdown', () => {
it('renders the heading and the "all correspondents" row even when the list is empty', async () => {
render(CorrespondentSuggestionsDropdown, {
props: {
correspondents: [],
loading: false,
senderName: 'Anna',
onselect: () => {},
onclose: () => {}
}
});
await expect.element(page.getByText('Häufigste Korrespondenten')).toBeVisible();
await expect.element(page.getByText('Alle Korrespondenten von Anna')).toBeVisible();
});
it('renders one row per correspondent when not loading', async () => {
render(CorrespondentSuggestionsDropdown, {
props: {
correspondents: [corrA, corrB],
loading: false,
senderName: 'Anna',
onselect: () => {},
onclose: () => {}
}
});
await expect.element(page.getByText('Anna Schmidt')).toBeVisible();
await expect.element(page.getByText('Müller')).toBeVisible();
});
it('hides correspondent rows while loading is true', async () => {
render(CorrespondentSuggestionsDropdown, {
props: {
correspondents: [corrA],
loading: true,
senderName: 'Anna',
onselect: () => {},
onclose: () => {}
}
});
await expect.element(page.getByText('Anna Schmidt')).not.toBeInTheDocument();
await expect.element(page.getByText('Häufigste Korrespondenten')).toBeVisible();
});
it('builds initials from firstName + lastName when available', async () => {
render(CorrespondentSuggestionsDropdown, {
props: {
correspondents: [corrA],
loading: false,
senderName: 'Anna',
onselect: () => {},
onclose: () => {}
}
});
await expect.element(page.getByText('AS')).toBeVisible();
});
it('falls back to the first two letters of lastName when firstName is missing', async () => {
render(CorrespondentSuggestionsDropdown, {
props: {
correspondents: [corrB],
loading: false,
senderName: 'Anna',
onselect: () => {},
onclose: () => {}
}
});
await expect.element(page.getByText('MÜ')).toBeVisible();
});
it('calls onselect with the correspondent id when a row is clicked', async () => {
const onselect = vi.fn();
render(CorrespondentSuggestionsDropdown, {
props: {
correspondents: [corrA],
loading: false,
senderName: 'Anna',
onselect,
onclose: () => {}
}
});
await page.getByText('Anna Schmidt').click();
expect(onselect).toHaveBeenCalledWith('a');
});
it('calls onselect with an empty string when the "all correspondents" row is clicked', async () => {
const onselect = vi.fn();
render(CorrespondentSuggestionsDropdown, {
props: {
correspondents: [],
loading: false,
senderName: 'Anna',
onselect,
onclose: () => {}
}
});
await page.getByText('Alle Korrespondenten von Anna').click();
expect(onselect).toHaveBeenCalledWith('');
});
it('calls onselect via Enter key on a focused row', async () => {
const onselect = vi.fn();
render(CorrespondentSuggestionsDropdown, {
props: {
correspondents: [corrA],
loading: false,
senderName: 'Anna',
onselect,
onclose: () => {}
}
});
const row = (await page.getByText('Anna Schmidt').element()) as HTMLElement;
row.focus();
row.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
expect(onselect).toHaveBeenCalledWith('a');
});
it('calls onclose when the Escape key is pressed', async () => {
const onclose = vi.fn();
render(CorrespondentSuggestionsDropdown, {
props: {
correspondents: [corrA],
loading: false,
senderName: 'Anna',
onselect: () => {},
onclose
}
});
const list = (await page.getByRole('listbox').element()) as HTMLElement;
list.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
expect(onclose).toHaveBeenCalledOnce();
});
});

View File

@@ -0,0 +1,120 @@
import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import PersonCard from './PersonCard.svelte';
afterEach(cleanup);
const basePerson = {
id: 'p-1',
firstName: 'Anna',
lastName: 'Schmidt',
displayName: 'Anna Schmidt',
personType: 'PERSON' as const
};
describe('PersonCard', () => {
it('renders the displayName as the primary heading', async () => {
render(PersonCard, { props: { person: basePerson, canWrite: false } });
await expect.element(page.getByRole('heading', { name: 'Anna Schmidt' })).toBeVisible();
});
it('renders the title above the name when personType is PERSON and title is set', async () => {
render(PersonCard, {
props: { person: { ...basePerson, title: 'Frau Dr.' }, canWrite: false }
});
await expect.element(page.getByText('Frau Dr.')).toBeVisible();
});
it('omits the title for non-PERSON types even if title is set', async () => {
render(PersonCard, {
props: {
person: { ...basePerson, personType: 'INSTITUTION', title: 'Frau Dr.' },
canWrite: false
}
});
await expect.element(page.getByText('Frau Dr.')).not.toBeInTheDocument();
});
it('renders the firstName/lastName initials inside the avatar for PERSON type', async () => {
render(PersonCard, { props: { person: basePerson, canWrite: false } });
await expect.element(page.getByText('AS')).toBeVisible();
});
it('falls back to lastName-only initials when firstName is missing', async () => {
render(PersonCard, {
props: {
person: { ...basePerson, firstName: null, displayName: 'Schmidt' },
canWrite: false
}
});
await expect.element(page.getByText('SS')).toBeVisible();
});
it('renders the PersonTypeBadge for non-PERSON types', async () => {
render(PersonCard, {
props: {
person: { ...basePerson, personType: 'INSTITUTION', displayName: 'Acme Inc.' },
canWrite: false
}
});
await expect.element(page.getByText('Institution')).toBeVisible();
});
it('omits the PersonTypeBadge for PERSON type', async () => {
render(PersonCard, { props: { person: basePerson, canWrite: false } });
await expect.element(page.getByText('Institution')).not.toBeInTheDocument();
await expect.element(page.getByText('Gruppe')).not.toBeInTheDocument();
});
it('renders the alias in italic typography when alias is provided', async () => {
render(PersonCard, {
props: { person: { ...basePerson, alias: 'Annerl' }, canWrite: false }
});
await expect.element(page.getByText(/Annerl/)).toBeVisible();
});
it('renders the life-date range when birthYear or deathYear are present', async () => {
render(PersonCard, {
props: {
person: { ...basePerson, birthYear: 1899, deathYear: 1972 },
canWrite: false
}
});
await expect.element(page.getByText(/1899/)).toBeVisible();
});
it('renders the notes section when notes are provided', async () => {
render(PersonCard, {
props: {
person: { ...basePerson, notes: 'Wohnte in Berlin.' },
canWrite: false
}
});
await expect.element(page.getByText('Wohnte in Berlin.')).toBeVisible();
});
it('renders the edit link when canWrite is true', async () => {
render(PersonCard, { props: { person: basePerson, canWrite: true } });
await expect
.element(page.getByRole('link', { name: /bearbeiten/i }))
.toHaveAttribute('href', '/persons/p-1/edit');
});
it('does not render the edit link when canWrite is false', async () => {
render(PersonCard, { props: { person: basePerson, canWrite: false } });
await expect.element(page.getByRole('link', { name: /bearbeiten/i })).not.toBeInTheDocument();
});
});