{error}
- {#if doc.filePath} - - {m.doc_download_link()} - - {/if} -{m.doc_no_scan()}
-diff --git a/frontend/e2e/bottom-panel.spec.ts b/frontend/e2e/bottom-panel.spec.ts new file mode 100644 index 00000000..55c2d074 --- /dev/null +++ b/frontend/e2e/bottom-panel.spec.ts @@ -0,0 +1,180 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf'); + +/** + * Bottom panel E2E tests — issue #62. + * Verifies the new document detail layout: full-viewport viewer + floating bottom panel. + */ + +let pdfDocHref: string; +let noFileDocHref: string; + +test.describe('Document bottom panel', () => { + test.beforeAll(async ({ request }) => { + const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000'; + + // Create a document with a PDF and a date for metadata tests. + const createRes = await request.post('/api/documents', { + multipart: { title: 'E2E Bottom Panel Test', documentDate: '1945-05-08' } + }); + if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`); + const doc = await createRes.json(); + + const uploadRes = await request.put(`/api/documents/${doc.id}`, { + multipart: { + title: doc.title, + documentDate: '1945-05-08', + transcription: 'Dies ist eine vollständige Transkription des Dokuments für den E2E-Test.', + file: { + name: 'minimal.pdf', + mimeType: 'application/pdf', + buffer: fs.readFileSync(PDF_FIXTURE) + } + } + }); + if (!uploadRes.ok()) throw new Error(`Upload PDF failed: ${uploadRes.status()}`); + pdfDocHref = `${baseURL}/documents/${doc.id}`; + + // Create a document WITHOUT a file — panel should open to Metadaten by default. + const noFileRes = await request.post('/api/documents', { + multipart: { title: 'E2E Bottom Panel No-File Test' } + }); + if (!noFileRes.ok()) throw new Error(`Create no-file document failed: ${noFileRes.status()}`); + noFileDocHref = `${baseURL}/documents/${noFileRes.json().then ? (await noFileRes.json()).id : ''}`; + const noFileDoc = await noFileRes.json(); + noFileDocHref = `${baseURL}/documents/${noFileDoc.id}`; + }); + + test('bottom panel tab bar is visible and panel content is closed by default on a PDF document', async ({ + page + }) => { + test.setTimeout(30_000); + // Clear localStorage to ensure no previous panel state. + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + // Tab bar must always be visible. + await expect(page.getByRole('button', { name: 'Metadaten' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Transkription' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Diskussion' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Verlauf' })).toBeVisible(); + + // Panel content must NOT be visible when closed. + await expect(page.locator('[data-testid="bottom-panel-content"]')).not.toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-closed-default.png' }); + }); + + test('clicking Metadaten tab opens the panel and shows metadata content', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + await page.getByRole('button', { name: 'Metadaten' }).click(); + + // Panel content becomes visible. + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + + // Metadata section heading should be present. + await expect(page.getByText('Details', { exact: false })).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-metadata.png' }); + }); + + test('clicking Transkription tab shows transcription text', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + await page.getByRole('button', { name: 'Transkription' }).click(); + + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + await expect( + page.getByText('Dies ist eine vollständige Transkription', { exact: false }) + ).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-transcription.png' }); + }); + + test('clicking Diskussion tab shows the comment input', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + await page.getByRole('button', { name: 'Diskussion' }).click(); + + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + await expect(page.getByPlaceholder('Kommentar schreiben…')).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-discussion.png' }); + }); + + test('clicking × close button collapses the panel content', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + // Open the panel first. + await page.getByRole('button', { name: 'Metadaten' }).click(); + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + + // Close it. + await page.locator('[data-testid="panel-close-btn"]').click(); + await expect(page.locator('[data-testid="bottom-panel-content"]')).not.toBeVisible(); + + // Tab bar still visible after closing. + await expect(page.getByRole('button', { name: 'Metadaten' })).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-closed-after-x.png' }); + }); + + test('panel open state persists after page reload', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + // Open the panel to Diskussion. + await page.getByRole('button', { name: 'Diskussion' }).click(); + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + + // Reload — panel should re-open on the same tab. + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + await expect(page.getByPlaceholder('Kommentar schreiben…')).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-persisted.png' }); + }); + + test('document without a file opens panel to Metadaten by default', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(noFileDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + // Panel should be open to Metadaten by default when there is no file. + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + await expect(page.getByText('Details', { exact: false })).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-no-file-default.png' }); + }); +}); diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 86a31481..5d29f488 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -255,5 +255,15 @@ "comment_btn_reply": "Antworten", "comment_edited_label": "· bearbeitet", "comment_panel_title": "Kommentare", - "comment_panel_close": "Schließen" + "comment_panel_close": "Schließen", + "doc_panel_tab_metadata": "Metadaten", + "doc_panel_tab_transcription": "Transkription", + "doc_panel_tab_discussion": "Diskussion", + "doc_panel_tab_history": "Verlauf", + "doc_panel_annotate": "Annotieren", + "doc_panel_annotate_stop": "Fertig", + "doc_panel_annotation_thread_title": "Annotation", + "doc_panel_discussion_annotation_tab": "Annotation · Seite {page}", + "pdf_annotations_show": "Annotierungen anzeigen", + "pdf_annotations_hide": "Annotierungen verbergen" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index b9060841..94bf692f 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -255,5 +255,15 @@ "comment_btn_reply": "Reply", "comment_edited_label": "· edited", "comment_panel_title": "Comments", - "comment_panel_close": "Close" + "comment_panel_close": "Close", + "doc_panel_tab_metadata": "Metadata", + "doc_panel_tab_transcription": "Transcription", + "doc_panel_tab_discussion": "Discussion", + "doc_panel_tab_history": "History", + "doc_panel_annotate": "Annotate", + "doc_panel_annotate_stop": "Done", + "doc_panel_annotation_thread_title": "Annotation", + "doc_panel_discussion_annotation_tab": "Annotation · Page {page}", + "pdf_annotations_show": "Show annotations", + "pdf_annotations_hide": "Hide annotations" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index db645b43..e0a46eef 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -255,5 +255,15 @@ "comment_btn_reply": "Responder", "comment_edited_label": "· editado", "comment_panel_title": "Comentarios", - "comment_panel_close": "Cerrar" + "comment_panel_close": "Cerrar", + "doc_panel_tab_metadata": "Metadatos", + "doc_panel_tab_transcription": "Transcripción", + "doc_panel_tab_discussion": "Discusión", + "doc_panel_tab_history": "Historial", + "doc_panel_annotate": "Anotar", + "doc_panel_annotate_stop": "Listo", + "doc_panel_annotation_thread_title": "Anotación", + "doc_panel_discussion_annotation_tab": "Anotación · Página {page}", + "pdf_annotations_show": "Mostrar anotaciones", + "pdf_annotations_hide": "Ocultar anotaciones" } diff --git a/frontend/src/lib/components/AnnotationSidePanel.svelte b/frontend/src/lib/components/AnnotationSidePanel.svelte new file mode 100644 index 00000000..0b264414 --- /dev/null +++ b/frontend/src/lib/components/AnnotationSidePanel.svelte @@ -0,0 +1,65 @@ + + +
+ {compactMeta} +
+ {/if} +{error}
+ {#if doc.filePath} + + {m.doc_download_link()} + + {/if} +{m.doc_no_scan()}
+{m.history_loading()}
+ {:else if !historyLoaded} + + {:else if versions.length === 0} +{m.history_empty()}
+ {:else} + +{m.history_loading()}
+ {:else if noDiff} ++ {#each entry.parts as part, partIdx (partIdx)} + {#if part.added} + {part.value} + {:else if part.removed} + {part.value} + {:else} + {part.value} + {/if} + {/each} +
+ {:else if entry.kind === 'scalar'} +{m.history_loading()}
+ {:else if noDiff} ++ {#each entry.parts as part, partIdx (partIdx)} + {#if part.added} + {part.value} + {:else if part.removed} + {part.value} + {:else} + {part.value} + {/if} + {/each} +
+ {:else if entry.kind === 'scalar'} ++ {doc.sender.firstName} + {doc.sender.lastName} +
+ {#if doc.sender.alias} +{doc.sender.alias}
+ {/if} +—
+ {/if} + + {#if doc.summary} +{doc.summary}
++ {doc.transcription} +
+{error}
- {#if doc.filePath} - - {m.doc_download_link()} - - {/if} -{m.doc_no_scan()}
-