diff --git a/frontend/src/lib/document/DocumentTopBar.svelte.test.ts b/frontend/src/lib/document/DocumentTopBar.svelte.test.ts new file mode 100644 index 00000000..93d07948 --- /dev/null +++ b/frontend/src/lib/document/DocumentTopBar.svelte.test.ts @@ -0,0 +1,193 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import DocumentTopBar from './DocumentTopBar.svelte'; + +afterEach(cleanup); + +const sender = { id: 's1', firstName: 'Anna', lastName: 'Schmidt', displayName: 'Anna Schmidt' }; +const receiver = { id: 'r1', firstName: 'Bert', lastName: 'Meier', displayName: 'Bert Meier' }; + +const baseDoc = { + id: 'd1', + title: 'Brief an Helene', + originalFilename: 'brief.pdf', + documentDate: '1923-04-15', + sender, + receivers: [receiver], + filePath: null as string | null, + contentType: null as string | null, + location: null, + status: 'UPLOADED', + tags: [] as { id: string; name: string }[] +}; + +const baseProps = (overrides: Record = {}) => ({ + doc: baseDoc, + canWrite: false, + fileUrl: '', + transcribeMode: false, + inferredRelationship: null, + geschichten: [], + canBlogWrite: false, + ...overrides +}); + +describe('DocumentTopBar', () => { + it('renders the document title as the main heading', async () => { + render(DocumentTopBar, { props: baseProps() }); + + await expect.element(page.getByRole('heading', { name: 'Brief an Helene' })).toBeVisible(); + }); + + it('falls back to originalFilename when title is missing', async () => { + render(DocumentTopBar, { props: baseProps({ doc: { ...baseDoc, title: null } }) }); + + await expect.element(page.getByRole('heading', { name: 'brief.pdf' })).toBeVisible(); + }); + + it('renders the short documentDate when one is present', async () => { + render(DocumentTopBar, { props: baseProps() }); + + await expect.element(page.getByText('15.04.1923')).toBeVisible(); + }); + + it('omits the date paragraph entirely when documentDate is null', async () => { + render(DocumentTopBar, { props: baseProps({ doc: { ...baseDoc, documentDate: null } }) }); + + await expect.element(page.getByText(/^\d{2}\.\d{2}\.\d{4}$/)).not.toBeInTheDocument(); + }); + + it('does not render the transcribe button when canWrite is false', async () => { + render(DocumentTopBar, { + props: baseProps({ doc: { ...baseDoc, filePath: 'x', contentType: 'application/pdf' } }) + }); + + await expect + .element(page.getByRole('button', { name: /transkribieren/i })) + .not.toBeInTheDocument(); + }); + + it('does not render the transcribe button when contentType is not PDF', async () => { + render(DocumentTopBar, { + props: baseProps({ + canWrite: true, + doc: { ...baseDoc, filePath: 'x', contentType: 'image/jpeg' } + }) + }); + + await expect + .element(page.getByRole('button', { name: /transkribieren/i })) + .not.toBeInTheDocument(); + }); + + it('renders the transcribe button when canWrite is true and the file is a PDF', async () => { + render(DocumentTopBar, { + props: baseProps({ + canWrite: true, + doc: { ...baseDoc, filePath: 'x', contentType: 'application/pdf' } + }) + }); + + await expect.element(page.getByRole('button', { name: /transkribieren/i })).toBeVisible(); + }); + + it('renders the stop-transcribe button when transcribeMode is true', async () => { + render(DocumentTopBar, { + props: baseProps({ + canWrite: true, + transcribeMode: true, + doc: { ...baseDoc, filePath: 'x', contentType: 'application/pdf' } + }) + }); + + await expect.element(page.getByRole('button', { name: /fertig/i })).toBeVisible(); + }); + + it('hides the edit link when transcribeMode is true', async () => { + render(DocumentTopBar, { + props: baseProps({ + canWrite: true, + transcribeMode: true, + doc: { ...baseDoc, filePath: 'x', contentType: 'application/pdf' } + }) + }); + + await expect.element(page.getByRole('link', { name: /bearbeiten/i })).not.toBeInTheDocument(); + }); + + it('renders the edit link when canWrite is true and not in transcribeMode', async () => { + render(DocumentTopBar, { props: baseProps({ canWrite: true }) }); + + await expect + .element(page.getByRole('link', { name: /bearbeiten/i })) + .toHaveAttribute('href', '/documents/d1/edit'); + }); + + it('does not render the edit link when canWrite is false', async () => { + render(DocumentTopBar, { props: baseProps() }); + + await expect.element(page.getByRole('link', { name: /bearbeiten/i })).not.toBeInTheDocument(); + }); + + it('renders the download link when filePath is present and not in transcribe mode', async () => { + render(DocumentTopBar, { + props: baseProps({ doc: { ...baseDoc, filePath: 'docs/x.pdf' }, fileUrl: '/api/docs/x' }) + }); + + await expect.element(page.getByTitle('Herunterladen')).toBeVisible(); + }); + + it('does not render the download link when filePath is null', async () => { + render(DocumentTopBar, { props: baseProps() }); + + await expect.element(page.getByTitle('Herunterladen')).not.toBeInTheDocument(); + }); + + it('opens the metadata drawer when the details toggle is clicked', async () => { + render(DocumentTopBar, { props: baseProps() }); + + await page.getByRole('button', { name: /^details$/i }).click(); + + await expect + .element(page.getByRole('button', { name: /^details$/i })) + .toHaveAttribute('aria-expanded', 'true'); + }); + + it('renders the mobile kebab menu trigger when filePath is present', async () => { + render(DocumentTopBar, { + props: baseProps({ doc: { ...baseDoc, filePath: 'docs/x.pdf' } }) + }); + + await expect.element(page.getByRole('button', { name: /weitere aktionen/i })).toBeVisible(); + }); + + it('does not render the mobile kebab menu when there is no filePath and no canWrite/PDF combo', async () => { + render(DocumentTopBar, { props: baseProps() }); + + await expect + .element(page.getByRole('button', { name: /weitere aktionen/i })) + .not.toBeInTheDocument(); + }); + + it('opens the mobile kebab menu when the trigger is clicked', async () => { + render(DocumentTopBar, { + props: baseProps({ doc: { ...baseDoc, filePath: 'docs/x.pdf' } }) + }); + + await page.getByRole('button', { name: /weitere aktionen/i }).click(); + + await expect + .element(page.getByRole('button', { name: /weitere aktionen/i })) + .toHaveAttribute('aria-expanded', 'true'); + }); + + it('renders the metadata drawer content when detailsOpen is toggled on', async () => { + render(DocumentTopBar, { props: baseProps() }); + + await page.getByRole('button', { name: /^details$/i }).click(); + + const drawer = document.querySelector('[data-topbar] > div:nth-child(2)'); + expect(drawer).not.toBeNull(); + }); +});