diff --git a/frontend/e2e/help-popover.spec.ts b/frontend/e2e/help-popover.spec.ts index bc0f77c8..4a6fcc06 100644 --- a/frontend/e2e/help-popover.spec.ts +++ b/frontend/e2e/help-popover.spec.ts @@ -8,21 +8,27 @@ test.describe('Help chip — Read/Edit panel header', () => { docId = await createEmptyDocument(request); }); + test.afterAll(async ({ request }) => { + await request.delete(`/api/documents/${docId}`); + }); + test('opens popover on click, closes on Esc, returns focus to chip', async ({ page }) => { await page.goto(`/documents/${docId}`); await page.getByRole('button', { name: 'Transkribieren' }).click(); - // Find and click the (?) help chip - const helpBtn = page.locator('button[aria-expanded]'); + // Use the accessible label of the HelpPopover trigger (transcription_mode_help_label) + const helpBtn = page.getByRole('button', { name: 'Lese- und Bearbeitungsmodus' }); await expect(helpBtn).toBeVisible({ timeout: 5000 }); await helpBtn.click(); - // Popover should open - await expect(page.locator('[role="tooltip"]')).toBeVisible(); + // Popover should open (role="region", not tooltip — click-triggered panels are regions) + await expect(page.getByRole('region', { name: 'Lese- und Bearbeitungsmodus' })).toBeVisible(); // Press Esc await page.keyboard.press('Escape'); - await expect(page.locator('[role="tooltip"]')).not.toBeVisible(); + await expect( + page.getByRole('region', { name: 'Lese- und Bearbeitungsmodus' }) + ).not.toBeVisible(); // Focus should have returned to the chip await expect(helpBtn).toBeFocused(); diff --git a/frontend/e2e/helpers/upload-empty-document.ts b/frontend/e2e/helpers/upload-empty-document.ts index caba3615..635f176b 100644 --- a/frontend/e2e/helpers/upload-empty-document.ts +++ b/frontend/e2e/helpers/upload-empty-document.ts @@ -1,10 +1,30 @@ import type { APIRequestContext } 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'); export async function createEmptyDocument(request: APIRequestContext): Promise { - const res = await request.post('/api/documents', { + const createRes = await request.post('/api/documents', { multipart: { title: 'E2E Transcribe Coach Test' } }); - if (!res.ok()) throw new Error(`Create document failed: ${res.status()}`); - const doc = await res.json(); - return doc.id as string; + if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`); + const doc = await createRes.json(); + const docId = doc.id as string; + + const uploadRes = await request.put(`/api/documents/${docId}`, { + multipart: { + title: doc.title, + file: { + name: 'minimal.pdf', + mimeType: 'application/pdf', + buffer: fs.readFileSync(PDF_FIXTURE) + } + } + }); + if (!uploadRes.ok()) throw new Error(`Upload PDF failed: ${uploadRes.status()}`); + + return docId; } diff --git a/frontend/e2e/richtlinien.spec.ts b/frontend/e2e/richtlinien.spec.ts index 1b5e52ea..5012fd5d 100644 --- a/frontend/e2e/richtlinien.spec.ts +++ b/frontend/e2e/richtlinien.spec.ts @@ -63,6 +63,12 @@ test.describe('Richtlinien page — print media', () => { await expect(nav).toBeHidden(); } + // .new-tab annotation spans must be hidden in print so "(öffnet in neuem Tab)" + // text does not clutter the printed output (the print CSS declares display:none) + for (const span of await page.locator('.new-tab').all()) { + await expect(span).toBeHidden(); + } + await page.screenshot({ path: 'test-results/e2e/richtlinien-print.png', fullPage: true }); }); }); diff --git a/frontend/e2e/transcribe-coach.spec.ts b/frontend/e2e/transcribe-coach.spec.ts index b54c7215..e6205ff5 100644 --- a/frontend/e2e/transcribe-coach.spec.ts +++ b/frontend/e2e/transcribe-coach.spec.ts @@ -2,6 +2,24 @@ import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; import { createEmptyDocument } from './helpers/upload-empty-document.js'; +async function createBlock( + request: Parameters[0], + docId: string +): Promise { + const res = await request.post(`/api/documents/${docId}/transcription-blocks`, { + data: { + pageNumber: 1, + x: 0.1, + y: 0.1, + width: 0.3, + height: 0.1, + text: 'Liebe Mutter,', + label: null + } + }); + if (!res.ok()) throw new Error(`Create block failed: ${res.status()}`); +} + function buildAxe(page: Parameters[0]['page']) { return new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']); } @@ -13,10 +31,13 @@ test.describe('Transcribe coach — empty state', () => { docId = await createEmptyDocument(request); }); + test.afterAll(async ({ request }) => { + await request.delete(`/api/documents/${docId}`); + }); + test('shows coach card (title, preamble, three step bodies, animation region)', async ({ page }) => { - await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto(`/documents/${docId}`); await page.getByRole('button', { name: 'Transkribieren' }).click(); @@ -31,14 +52,12 @@ test.describe('Transcribe coach — empty state', () => { }); test('training footer is NOT visible on empty doc', async ({ page }) => { - await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto(`/documents/${docId}`); await page.getByRole('button', { name: 'Transkribieren' }).click(); await expect(page.getByText('Für Training vormerken')).not.toBeVisible({ timeout: 3000 }); }); test('axe: panel empty state — light theme — no WCAG 2.1 AA violations', async ({ page }) => { - await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto(`/documents/${docId}`); await page.getByRole('button', { name: 'Transkribieren' }).click(); await expect(page.getByRole('heading', { level: 2, name: /Erste Transkription/ })).toBeVisible({ @@ -50,10 +69,9 @@ test.describe('Transcribe coach — empty state', () => { }); test('axe: panel empty state — dark theme — no WCAG 2.1 AA violations', async ({ page }) => { - await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto(`/documents/${docId}`); // Toggle dark theme - await page.getByRole('button', { name: /Farbmodus|theme/i }).click(); + await page.getByRole('button', { name: /dark mode/i }).click(); await page.getByRole('button', { name: 'Transkribieren' }).click(); await expect(page.getByRole('heading', { level: 2, name: /Erste Transkription/ })).toBeVisible({ timeout: 5000 @@ -63,3 +81,25 @@ test.describe('Transcribe coach — empty state', () => { expect(a11y.violations, JSON.stringify(a11y.violations, null, 2)).toHaveLength(0); }); }); + +test.describe('Transcribe coach — with blocks', () => { + let docId: string; + + test.beforeAll(async ({ request }) => { + docId = await createEmptyDocument(request); + await createBlock(request, docId); + }); + + test.afterAll(async ({ request }) => { + await request.delete(`/api/documents/${docId}`); + }); + + test('training footer IS visible when at least one block exists', async ({ page }) => { + await page.goto(`/documents/${docId}`); + await page.getByRole('button', { name: 'Transkribieren' }).click(); + // Wait for blocks to finish loading — block count confirms mode settled to 'read' + await expect(page.getByText(/1 Abschnitt/)).toBeVisible({ timeout: 5000 }); + await page.locator('[data-testid="mode-edit"]').click(); + await expect(page.getByText('Für Training vormerken')).toBeVisible({ timeout: 5000 }); + }); +}); diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 1b90d913..9824ccee 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -515,7 +515,6 @@ "scan_collapse": "Scan verkleinern", "transcription_empty_title": "Noch keine Transkription", "transcription_empty_desc": "Zeichne Bereiche auf dem Scan und tippe den Text ab, um eine Transkription zu erstellen.", - "transcription_empty_draw_hint": "Zeichnen Sie Bereiche auf dem Dokument, um mit der Transkription zu beginnen.", "transcription_panel_close": "Panel schließen", "person_alias_heading": "Namensverlauf", "person_alias_empty": "Noch keine Namensaenderungen erfasst.", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index f5862f0f..d999e1eb 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -515,7 +515,6 @@ "scan_collapse": "Collapse scan", "transcription_empty_title": "No transcription yet", "transcription_empty_desc": "Draw regions on the scan and type the text to create a transcription.", - "transcription_empty_draw_hint": "Draw regions on the document to start transcribing.", "transcription_panel_close": "Close panel", "person_alias_heading": "Name history", "person_alias_empty": "No name changes recorded yet.", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index c949ce2c..b3754eba 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -515,7 +515,6 @@ "scan_collapse": "Reducir escaneo", "transcription_empty_title": "Sin transcripcion", "transcription_empty_desc": "Dibuja regiones en el escaneo y escribe el texto para crear una transcripcion.", - "transcription_empty_draw_hint": "Dibuje regiones en el documento para comenzar a transcribir.", "transcription_panel_close": "Cerrar panel", "person_alias_heading": "Historial de nombres", "person_alias_empty": "Aun no se han registrado cambios de nombre.", diff --git a/frontend/src/lib/components/HelpPopover.svelte b/frontend/src/lib/components/HelpPopover.svelte index e1d118ca..2b96fd97 100644 --- a/frontend/src/lib/components/HelpPopover.svelte +++ b/frontend/src/lib/components/HelpPopover.svelte @@ -1,3 +1,10 @@ + +
+ {#if open}