diff --git a/frontend/e2e/dashboard-enrichment-block.spec.ts b/frontend/e2e/dashboard-enrichment-block.spec.ts new file mode 100644 index 00000000..28aa4141 --- /dev/null +++ b/frontend/e2e/dashboard-enrichment-block.spec.ts @@ -0,0 +1,83 @@ +/** + * Dashboard enrichment-list-block (issue #296) — full upload → banner → CTA journey, + * plus axe sweep in light and dark mode at 320 / 768 / 1440 viewports. + * + * The uploaded PDFs are deleted in afterEach so this spec does not pollute + * the dev DB between runs. + */ +import AxeBuilder from '@axe-core/playwright'; +import { test, expect, type Page } from '@playwright/test'; +import { execSync } from 'child_process'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const VIEWPORTS = [ + { name: '320', width: 320, height: 720 }, + { name: '768', width: 768, height: 1024 }, + { name: '1440', width: 1440, height: 900 } +]; + +const psql = (sql: string) => + execSync( + `docker exec archive-db psql -U archive_user family_archive_db -c "${sql.replace(/"/g, '\\"')}"` + ); + +test.afterEach(() => { + // Remove any document whose filename matches the seeded sentinel — keeps the + // DB clean for subsequent test runs. + psql(`DELETE FROM documents WHERE original_filename IN ('minimal.pdf', 'minimal2.pdf');`); +}); + +async function uploadViaDropZone(page: Page, files: string[]) { + const inputLocator = page.locator('input[type="file"][accept*="pdf"]'); + await inputLocator.first().setInputFiles(files); +} + +test.describe('Dashboard enrichment block — upload → banner → CTA', () => { + test('banner appears after upload and CTA navigates to /enrich', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('[data-hydrated]'); + + const fixturePath = (name: string) => path.join(__dirname, 'fixtures', name); + await uploadViaDropZone(page, [fixturePath('minimal.pdf'), fixturePath('minimal2.pdf')]); + + const banner = page.getByRole('status').filter({ hasText: /hochgeladen/ }); + await expect(banner).toBeVisible(); + await expect(banner).toContainText(/2 Dokumente/); + + await banner.getByRole('link', { name: /ergänzen/i }).click(); + await expect(page).toHaveURL(/\/enrich(\/|$)/); + }); +}); + +test.describe('Dashboard enrichment block — axe sweep', () => { + for (const viewport of VIEWPORTS) { + for (const scheme of ['light', 'dark'] as const) { + test(`no wcag2a/wcag2aa violations at ${viewport.name}px (${scheme})`, async ({ + browser + }) => { + const context = await browser.newContext({ + colorScheme: scheme, + viewport: { width: viewport.width, height: viewport.height }, + storageState: path.join(__dirname, '.auth/user.json') + }); + const page = await context.newPage(); + await page.goto('/'); + await page.waitForSelector('[data-hydrated]'); + + const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); + + if (results.violations.length > 0) { + console.log( + `Violations on dashboard @ ${viewport.name}px ${scheme}:\n` + + results.violations.map((v) => `[${v.impact}] ${v.id}`).join('\n') + ); + } + expect(results.violations).toEqual([]); + await context.close(); + }); + } + } +});