diff --git a/frontend/src/lib/person/PersonChipRow.svelte.test.ts b/frontend/src/lib/person/PersonChipRow.svelte.test.ts new file mode 100644 index 00000000..cac46ef2 --- /dev/null +++ b/frontend/src/lib/person/PersonChipRow.svelte.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import PersonChipRow from './PersonChipRow.svelte'; + +afterEach(cleanup); + +const sender = { id: 's-1', firstName: 'Anna', lastName: 'Schmidt', displayName: 'Anna Schmidt' }; +const r1 = { id: 'r-1', firstName: 'Bert', lastName: 'Meier', displayName: 'Bert Meier' }; +const r2 = { id: 'r-2', firstName: 'Clara', lastName: 'Weiss', displayName: 'Clara Weiss' }; +const r3 = { id: 'r-3', firstName: 'Doris', lastName: 'Lang', displayName: 'Doris Lang' }; + +describe('PersonChipRow', () => { + it('renders only the sender when there are no receivers', async () => { + render(PersonChipRow, { + props: { sender, receivers: [], abbreviated: false, extraCount: 0 } + }); + + await expect.element(page.getByText('Anna Schmidt')).toBeVisible(); + await expect.element(page.getByRole('img', { name: '' })).not.toBeInTheDocument(); + }); + + it('renders the arrow image when sender and at least one receiver are present', async () => { + render(PersonChipRow, { + props: { sender, receivers: [r1], abbreviated: false, extraCount: 0 } + }); + + const arrow = document.querySelector('img[aria-hidden="true"]'); + expect(arrow).not.toBeNull(); + }); + + it('renders both sender and visible receivers with abbreviated=false', async () => { + render(PersonChipRow, { + props: { sender, receivers: [r1, r2], abbreviated: false, extraCount: 0 } + }); + + await expect.element(page.getByText('Anna Schmidt')).toBeVisible(); + await expect.element(page.getByText('Bert Meier')).toBeVisible(); + }); + + it('uses abbreviated names when abbreviated=true', async () => { + render(PersonChipRow, { + props: { sender, receivers: [r1], abbreviated: true, extraCount: 0 } + }); + + await expect.element(page.getByText('A. Schmidt')).toBeVisible(); + await expect.element(page.getByText('B. Meier')).toBeVisible(); + }); + + it('limits the visible receivers to the first two', async () => { + render(PersonChipRow, { + props: { sender, receivers: [r1, r2, r3], abbreviated: false, extraCount: 1 } + }); + + await expect.element(page.getByText('Bert Meier')).toBeVisible(); + await expect.element(page.getByText('Clara Weiss')).toBeVisible(); + await expect.element(page.getByText('Doris Lang')).not.toBeInTheDocument(); + }); + + it('renders the OverflowPillDisplay when extraCount > 0', async () => { + render(PersonChipRow, { + props: { sender, receivers: [r1, r2, r3], abbreviated: false, extraCount: 1 } + }); + + await expect.element(page.getByText(/\+1/)).toBeVisible(); + }); + + it('omits the OverflowPillDisplay when extraCount is 0', async () => { + render(PersonChipRow, { + props: { sender, receivers: [r1], abbreviated: false, extraCount: 0 } + }); + + await expect.element(page.getByText(/\+\d/)).not.toBeInTheDocument(); + }); + + it('renders only receivers when there is no sender', async () => { + render(PersonChipRow, { + props: { sender: null, receivers: [r1], abbreviated: false, extraCount: 0 } + }); + + await expect.element(page.getByText('Bert Meier')).toBeVisible(); + const arrow = document.querySelector('img[aria-hidden="true"]'); + expect(arrow).toBeNull(); + }); +}); diff --git a/frontend/src/lib/person/PersonTypeBadge.svelte.test.ts b/frontend/src/lib/person/PersonTypeBadge.svelte.test.ts new file mode 100644 index 00000000..45fcf2a7 --- /dev/null +++ b/frontend/src/lib/person/PersonTypeBadge.svelte.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import PersonTypeBadge from './PersonTypeBadge.svelte'; + +afterEach(cleanup); + +describe('PersonTypeBadge', () => { + it('renders the institution label and badge-institution class for personType="INSTITUTION"', async () => { + render(PersonTypeBadge, { props: { personType: 'INSTITUTION' } }); + + await expect.element(page.getByText('Institution')).toBeVisible(); + const badge = await page.getByText('Institution').element(); + expect(badge.classList.contains('badge-institution')).toBe(true); + }); + + it('renders the group label and badge-group class for personType="GROUP"', async () => { + render(PersonTypeBadge, { props: { personType: 'GROUP' } }); + + const badge = await page.getByText('Gruppe').element(); + expect(badge.classList.contains('badge-group')).toBe(true); + }); + + it('renders the unknown label and badge-unknown class for personType="UNKNOWN"', async () => { + render(PersonTypeBadge, { props: { personType: 'UNKNOWN' } }); + + const badge = await page.getByText('Unbekannt').element(); + expect(badge.classList.contains('badge-unknown')).toBe(true); + }); + + it('renders nothing when personType does not match a known kind', async () => { + render(PersonTypeBadge, { props: { personType: 'INDIVIDUAL' } }); + + expect(document.querySelector('.badge')).toBeNull(); + }); + + it('renders nothing for empty personType', async () => { + render(PersonTypeBadge, { props: { personType: '' } }); + + expect(document.querySelector('.badge')).toBeNull(); + }); +}); diff --git a/frontend/src/lib/shared/primitives/ExpandableText.svelte.test.ts b/frontend/src/lib/shared/primitives/ExpandableText.svelte.test.ts new file mode 100644 index 00000000..00a604ec --- /dev/null +++ b/frontend/src/lib/shared/primitives/ExpandableText.svelte.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import ExpandableText from './ExpandableText.svelte'; + +afterEach(cleanup); + +const longText = Array.from({ length: 60 }, (_, i) => `Zeile ${i + 1}.`).join('\n'); +const shortText = 'Zeile 1'; + +describe('ExpandableText', () => { + it('renders the supplied text inside the clamped block', async () => { + render(ExpandableText, { props: { text: shortText, maxLines: 2 } }); + + await expect.element(page.getByText('Zeile 1')).toBeVisible(); + }); + + it('does not show a toggle button when the content fits inside maxLines', async () => { + render(ExpandableText, { props: { text: shortText, maxLines: 100 } }); + + await expect + .element(page.getByRole('button', { name: /mehr anzeigen/i })) + .not.toBeInTheDocument(); + await expect + .element(page.getByRole('button', { name: /weniger anzeigen/i })) + .not.toBeInTheDocument(); + }); + + it('shows the "Mehr anzeigen" button when the content overflows the line clamp', async () => { + render(ExpandableText, { props: { text: longText, maxLines: 2 } }); + + await expect.element(page.getByRole('button', { name: /mehr anzeigen/i })).toBeVisible(); + }); + + it('switches the toggle label to "Weniger anzeigen" after expanding', async () => { + render(ExpandableText, { props: { text: longText, maxLines: 2 } }); + + await page.getByRole('button', { name: /mehr anzeigen/i }).click(); + + await expect.element(page.getByRole('button', { name: /weniger anzeigen/i })).toBeVisible(); + }); + + it('collapses again when the toggle is clicked while expanded', async () => { + render(ExpandableText, { props: { text: longText, maxLines: 2 } }); + + await page.getByRole('button', { name: /mehr anzeigen/i }).click(); + await page.getByRole('button', { name: /weniger anzeigen/i }).click(); + + await expect.element(page.getByRole('button', { name: /mehr anzeigen/i })).toBeVisible(); + }); + + it('uses the default maxLines (10) when the prop is omitted', async () => { + render(ExpandableText, { props: { text: shortText } }); + + await expect.element(page.getByText('Zeile 1')).toBeVisible(); + }); +});