diff --git a/frontend/src/lib/document/DocumentMetadataDrawer.svelte.test.ts b/frontend/src/lib/document/DocumentMetadataDrawer.svelte.test.ts new file mode 100644 index 00000000..8077f9b3 --- /dev/null +++ b/frontend/src/lib/document/DocumentMetadataDrawer.svelte.test.ts @@ -0,0 +1,207 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import DocumentMetadataDrawer from './DocumentMetadataDrawer.svelte'; + +afterEach(cleanup); + +const sender = { id: 's1', firstName: 'Anna', lastName: 'Schmidt', displayName: 'Anna Schmidt' }; +const receiver = (id: string, name: string) => ({ + id, + firstName: name.split(' ')[0], + lastName: name.split(' ').slice(1).join(' ') || name, + displayName: name +}); + +const baseProps = { + documentDate: '1923-04-15' as string | null, + location: 'Berlin' as string | null, + status: 'UPLOADED', + sender: null as typeof sender | null, + receivers: [] as ReturnType[], + tags: [] as { id: string; name: string }[], + inferredRelationship: null, + geschichten: [] as { + id: string; + title: string; + publishedAt?: string; + author?: { firstName?: string; lastName?: string; email: string }; + }[], + documentId: 'doc-1', + canBlogWrite: false +}; + +describe('DocumentMetadataDrawer', () => { + it('renders the three default section headings', async () => { + render(DocumentMetadataDrawer, { props: baseProps }); + + await expect.element(page.getByRole('heading', { name: 'Details' })).toBeVisible(); + await expect.element(page.getByRole('heading', { name: 'Personen' })).toBeVisible(); + await expect.element(page.getByRole('heading', { name: 'Schlagwörter' })).toBeVisible(); + }); + + it('renders the formatted long date when documentDate is provided', async () => { + render(DocumentMetadataDrawer, { props: baseProps }); + + // formatDate default ('long') format is "15. April 1923" in de-DE. + await expect.element(page.getByText(/1923/)).toBeVisible(); + }); + + it('renders an em-dash when documentDate is null', async () => { + render(DocumentMetadataDrawer, { props: { ...baseProps, documentDate: null } }); + + // The dash appears in date AND location AND geschichten — multiple matches expected + const dashes = document.querySelectorAll('dd, p'); + const dashTexts = Array.from(dashes) + .map((el) => el.textContent?.trim()) + .filter((t) => t === '—'); + expect(dashTexts.length).toBeGreaterThan(0); + }); + + it('renders the no-persons placeholder when sender and receivers are empty', async () => { + render(DocumentMetadataDrawer, { props: baseProps }); + + await expect.element(page.getByText('Keine Personen zugeordnet')).toBeVisible(); + }); + + it('renders the sender and inferred relationship label when both are present', async () => { + render(DocumentMetadataDrawer, { + props: { + ...baseProps, + sender, + inferredRelationship: { labelFromA: 'Vater', labelFromB: 'Tochter' } + } + }); + + await expect.element(page.getByText('Anna Schmidt')).toBeVisible(); + }); + + it('renders the receivers list with up to five visible by default', async () => { + const receivers = Array.from({ length: 7 }, (_, i) => receiver(`r${i}`, `Person ${i}`)); + render(DocumentMetadataDrawer, { + props: { ...baseProps, sender, receivers } + }); + + await expect.element(page.getByText('Person 0')).toBeVisible(); + await expect.element(page.getByText('Person 4')).toBeVisible(); + await expect.element(page.getByText('Person 5')).not.toBeInTheDocument(); + }); + + it('renders the +N more button when there are more than five receivers', async () => { + const receivers = Array.from({ length: 8 }, (_, i) => receiver(`r${i}`, `Person ${i}`)); + render(DocumentMetadataDrawer, { + props: { ...baseProps, sender, receivers } + }); + + await expect.element(page.getByRole('button', { name: /\+3 weitere/i })).toBeVisible(); + }); + + it('expands the receiver list when the +N more button is clicked', async () => { + const receivers = Array.from({ length: 8 }, (_, i) => receiver(`r${i}`, `Person ${i}`)); + render(DocumentMetadataDrawer, { + props: { ...baseProps, sender, receivers } + }); + + await page.getByRole('button', { name: /\+3 weitere/i }).click(); + + await expect.element(page.getByText('Person 7')).toBeVisible(); + }); + + it('renders the no-tags placeholder when tags is empty', async () => { + render(DocumentMetadataDrawer, { props: baseProps }); + + await expect.element(page.getByText('Keine Schlagwörter zugeordnet')).toBeVisible(); + }); + + it('renders one anchor per tag when tags are present', async () => { + render(DocumentMetadataDrawer, { + props: { + ...baseProps, + tags: [ + { id: 't1', name: 'Familie' }, + { id: 't2', name: 'Reise' } + ] + } + }); + + await expect + .element(page.getByRole('link', { name: 'Familie' })) + .toHaveAttribute('href', '/?tag=Familie'); + await expect + .element(page.getByRole('link', { name: 'Reise' })) + .toHaveAttribute('href', '/?tag=Reise'); + }); + + it('hides the geschichten column when there are no stories and no canBlogWrite', async () => { + render(DocumentMetadataDrawer, { props: baseProps }); + + await expect + .element(page.getByRole('heading', { name: 'Geschichten' })) + .not.toBeInTheDocument(); + }); + + it('shows the geschichten column when canBlogWrite is true even with no stories', async () => { + render(DocumentMetadataDrawer, { props: { ...baseProps, canBlogWrite: true } }); + + await expect.element(page.getByRole('heading', { name: 'Geschichten' })).toBeVisible(); + }); + + it('renders the attach link to the new-geschichte route when canBlogWrite + documentId', async () => { + render(DocumentMetadataDrawer, { + props: { ...baseProps, canBlogWrite: true, documentId: 'doc-42' } + }); + + const links = document.querySelectorAll('a[href*="/geschichten/new?documentId="]'); + expect(links.length).toBe(1); + expect((links[0] as HTMLAnchorElement).href).toContain('documentId=doc-42'); + }); + + it('renders the geschichten list when stories are present', async () => { + render(DocumentMetadataDrawer, { + props: { + ...baseProps, + geschichten: [ + { + id: 'g1', + title: 'Reise nach Berlin', + publishedAt: '2026-04-15T10:00:00Z', + author: { firstName: 'Anna', lastName: 'Schmidt', email: 'anna@x' } + } + ] + } + }); + + await expect.element(page.getByRole('link', { name: /reise nach berlin/i })).toBeVisible(); + }); + + it('renders the show-all geschichten link when there are at least three stories', async () => { + render(DocumentMetadataDrawer, { + props: { + ...baseProps, + geschichten: Array.from({ length: 3 }, (_, i) => ({ + id: `g${i}`, + title: `Geschichte ${i}`, + publishedAt: '2026-04-15T10:00:00Z', + author: { firstName: 'Anna', lastName: 'Schmidt', email: 'anna@x' } + })) + } + }); + + await expect.element(page.getByText(/zeige alle|alle/i)).toBeVisible(); + }); + + it('renders the receiver-only inferred relationship pill only when there is exactly one receiver', async () => { + render(DocumentMetadataDrawer, { + props: { + ...baseProps, + sender, + receivers: [receiver('r1', 'Bert Meier')], + inferredRelationship: { labelFromA: 'Vater', labelFromB: 'Tochter' } + } + }); + + // Both labels should be visible — Vater for sender, Tochter for the single receiver + await expect.element(page.getByText(/vater/i)).toBeVisible(); + await expect.element(page.getByText(/tochter/i)).toBeVisible(); + }); +});